diff --git a/.travis.yml b/.travis.yml index 06c9c26..30055ec 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,4 +4,4 @@ language: ruby cache: bundler rvm: - 2.6.6 -before_install: gem install bundler \ No newline at end of file +before_install: gem install bundler diff --git a/README.md b/README.md index a4b4df6..a6ad3b9 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,12 @@ This gem currently supports only: 1. increment_counter 2. add_distribution_value 3. set_gauge +3. Datadog: Empty wrapper around the custom metric distributions + 1. count + 2. distribution + 3. set + 4. gauge + 5. histogram No dependencies are declared for this as the @@ -50,6 +56,7 @@ Each initialised logger is then registered to `EventTracer`. ```ruby EventTracer.register :base, base_logger EventTracer.register :appsignal, appsignal_logger +EventTracer.register :datadog, datadog_logger ``` As this is a registry, you can set it up with your own implemented wrapper as long as it responds to the following `LOG_TYPES` methods: `info, warn, error` @@ -119,6 +126,35 @@ EventTracer.info( ) # This calls .increment_counter on Appsignal once with additional tag # counter_1, 1, region: 'eu' + +**3. Datadog** + +Datadog via dogstatsd-ruby (4.8.1) is currently supported for the following metric functions available for the EventTracer's log methods + +- increment +- distribution +- set +- gauge +- histogram + +All other functions are exposed transparently to the underlying Appsignal class + +The interface for using the Appsignal wrapper is: + +Key | Secondary key | Secondary key type | Values +--------------|-------------|------------------|------- +datadog | count | Hash | Hash of key-value pairs featuring the metric name and the counter value to send +| | distribution | Hash | Hash of key-value pairs featuring the metric name and the distribution value to send +| | set | Hash | Hash of key-value pairs featuring the metric name and the set value to send +| | gauge | Hash | Hash of key-value pairs featuring the metric name and the gauge value to send +| | histogram | Hash | Hash of key-value pairs featuring the metric name and the histogram value to send + +```ruby +# Sample usage +EventTracer.info action: 'Action', message: 'Message', datadog: { count: { counter_1: 1, counter_2: { value: 2, tags: ['foo']} } } +# This calls .count on Datadog twice with the 2 sets of arguments +# counter_1, 1 +# counter_2, 2 ``` **Summary** @@ -127,7 +163,7 @@ In all the generated interface for `EventTracer` logging could look something li ```ruby EventTracer.info( - loggers: [:base, :appsignal, :custom_logging_service] + loggers: %(base appsignal custom_logging_service datadog), action: 'NewTransaction', message: "New transaction created by API", appsignal: { @@ -135,6 +171,12 @@ EventTracer.info( "distribution_metric_1" => 1000, "distribution_metric_2" => 2000 } + }, + datadog: { + distribution: { + "distribution_metric_1" => 1000, + "distribution_metric_2" => { value: 2000, tags: ['eu'] } + } } ) ``` diff --git a/event_tracer.gemspec b/event_tracer.gemspec index 4d530c2..3bec5a3 100644 --- a/event_tracer.gemspec +++ b/event_tracer.gemspec @@ -1,23 +1,22 @@ -# coding: utf-8 -lib = File.expand_path("../lib", __FILE__) +lib = File.expand_path('../lib', __FILE__) $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) -require "event_tracer/version" +require 'event_tracer/version' Gem::Specification.new do |spec| - spec.name = "event_tracer" + spec.name = 'event_tracer' spec.version = EventTracer::VERSION - spec.authors = ["melvrickgoh"] - spec.email = ["melvrickgoh@hotmail.com"] + spec.authors = ['melvrickgoh'] + spec.email = ['melvrickgoh@hotmail.com'] - spec.summary = %q{Thin wrapper for formatted logging/ metric services to be used as a single service} - spec.description = %q{Thin wrapper for formatted logging/ metric services to be used as a single service. External service(s) supported: Appsignal} - spec.homepage = "https://github.com/melvrickgoh/event_tracer" - spec.license = "MIT" + spec.summary = 'Thin wrapper for formatted logging/ metric services to be used as a single service' + spec.description = 'Thin wrapper for formatted logging/ metric services to be used as a single service. External service(s) supported: Appsignal' + spec.homepage = 'https://github.com/melvrickgoh/event_tracer' + spec.license = 'MIT' spec.files = Dir['lib/**/*.rb'] - spec.bindir = "exe" + spec.bindir = 'exe' spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } - spec.require_paths = ["lib/event_tracer", "lib"] + spec.require_paths = %w[lib/event_tracer lib] spec.add_development_dependency "bundler", "~> 2.1.4" spec.add_development_dependency "rake", "~> 10.0" diff --git a/lib/event_tracer/datadog_logger.rb b/lib/event_tracer/datadog_logger.rb new file mode 100644 index 0000000..dca8c37 --- /dev/null +++ b/lib/event_tracer/datadog_logger.rb @@ -0,0 +1,75 @@ +require_relative '../event_tracer' +require_relative './basic_decorator' +# NOTES +# Datadog interface to send our usual actions +# BasicDecorator adds a transparent interface on top of the datadog interface +# +# Usage: EventTracer.register :datadog, EventTracer::DataDogLogger.new(DataDog) +# data_dog_logger.info datadog: { count: { counter_1: 1, counter_2: 2 }, set: { gauge_1: 1 } } +# data_dog_logger.info datadog: { count: { counter_1: { value: 1, tags: ['tag1, tag2']} } } + +module EventTracer + class DatadogLogger < BasicDecorator + + class InvalidTagError < StandardError; end + + SUPPORTED_METRICS ||= %i[count set distribution gauge histogram].freeze + + LOG_TYPES.each do |log_type| + define_method log_type do |**args| + return LogResult.new(false, 'Invalid datadog config') unless args[:datadog]&.is_a?(Hash) + + applied_metrics(args[:datadog]).each do |metric| + metric_args = args[:datadog][metric] + return LogResult.new(false, "Datadog metric #{metric} invalid") unless metric_args.is_a?(Hash) + + send_metric metric, metric_args + end + + LogResult.new(true) + end + end + + private + + attr_reader :datadog, :decoratee + alias_method :datadog, :decoratee + + def applied_metrics(datadog_args) + datadog_args.keys.select { |metric| SUPPORTED_METRICS.include?(metric) } + end + + def send_metric(metric, payload) + payload.each do |increment, attribute| + if attribute.is_a?(Hash) + begin + datadog.public_send( + metric, + increment, + attribute.fetch(:value), + build_options(attribute[:tags]) + ) + rescue KeyError + raise InvalidTagError, "Datadog payload { #{increment}: #{attribute} } invalid" + end + else + datadog.public_send(metric, increment, attribute) + end + end + end + + def build_options(tags) + return {} unless tags + + formattted_tags = + if tags.is_a?(Array) + tags + else + tags.inject([]) do |acc, (tag, value)| + acc << "#{tag}:#{value}" + end + end + { tags: formattted_tags } + end + end +end diff --git a/lib/event_tracer/version.rb b/lib/event_tracer/version.rb index 873e3a9..14774e2 100644 --- a/lib/event_tracer/version.rb +++ b/lib/event_tracer/version.rb @@ -1,3 +1,3 @@ module EventTracer - VERSION = "0.1.3" + VERSION = '0.2.0'.freeze end diff --git a/spec/data_helpers/mock_datadog.rb b/spec/data_helpers/mock_datadog.rb new file mode 100644 index 0000000..bd9100d --- /dev/null +++ b/spec/data_helpers/mock_datadog.rb @@ -0,0 +1,21 @@ +class MockDatadog < Struct.new(:_) + def increment(*_args) + 'increment' + end + + def distribution(*_args) + 'distribution' + end + + def set(*_args) + 'set' + end + + def gauge(*_args) + 'gauge' + end + + def histogram(*_args) + 'histogram' + end +end diff --git a/spec/event_tracer/datadog_logger_spec.rb b/spec/event_tracer/datadog_logger_spec.rb new file mode 100644 index 0000000..3827697 --- /dev/null +++ b/spec/event_tracer/datadog_logger_spec.rb @@ -0,0 +1,176 @@ +require 'spec_helper' + +describe EventTracer::DatadogLogger do + + INVALID_PAYLOADS ||= [ + nil, + [], + Object.new, + 'string', + 10, + :invalid_payload + ].freeze + + let(:datadog_payload) { nil } + let(:mock_datadog) { MockDatadog.new } + + subject { EventTracer::DatadogLogger.new(mock_datadog) } + + EventTracer::LOG_TYPES.each do |log_type| + context "Log type: #{log_type}" do + let(:expected_call) { log_type } + + context 'processes_hashed_inputs' do + let(:datadog_payload) do + { + count: { 'Counter_1' => 1, 'Counter_2' => 2 }, + distribution: { 'Distribution_1' => 10 }, + set: { 'Set_1' => 100 }, + gauge: { 'Gauge_1' => 100 } + } + end + + it 'processes each hash keyset as a metric iteration' do + expect(mock_datadog).to receive(:count).with('Counter_1', 1) + expect(mock_datadog).to receive(:count).with('Counter_2', 2) + expect(mock_datadog).to receive(:distribution).with('Distribution_1', 10) + expect(mock_datadog).to receive(:set).with('Set_1', 100) + expect(mock_datadog).to receive(:gauge).with('Gauge_1', 100) + + result = subject.send(expected_call, datadog: datadog_payload) + + expect(result.success?).to eq true + expect(result.error).to eq nil + end + end + + context 'processes_hashed_inputs with tags in correct format' do + let(:datadog_payload) do + { + count: { 'Counter_1' => { value: 1 }, 'Counter_2' => { value: 2, tags: ['test']} }, + distribution: { 'Distribution_1' => { value: 10, tags: ['test']} }, + set: { 'Set_1' => { value: 100, tags: ['test'] } }, + gauge: { 'Gauge_1' => { value: 100, tags: ['test'] } } + } + end + + it 'processes each hash keyset as a metric iteration' do + expect(mock_datadog).to receive(:count).with('Counter_1', 1, {}) + expect(mock_datadog).to receive(:count).with('Counter_2', 2, tags: ['test']) + expect(mock_datadog).to receive(:distribution).with('Distribution_1', 10, tags: ['test']) + expect(mock_datadog).to receive(:set).with('Set_1', 100, tags: ['test']) + expect(mock_datadog).to receive(:gauge).with('Gauge_1', 100, tags: ['test']) + + result = subject.send(expected_call, datadog: datadog_payload) + + expect(result.success?).to eq true + expect(result.error).to eq nil + end + end + + context 'processes_hashed_inputs with tags as hash' do + let(:datadog_payload) do + { + count: { + 'Counter_1' => { value: 1 }, + 'Counter_2' => { value: 2, tags: { test: 'value' } } + }, + distribution: { 'Distribution_1' => { value: 10, tags: { test: 'value' } } }, + set: { 'Set_1' => { value: 100, tags: { test: 'value' } } }, + gauge: { 'Gauge_1' => { value: 100, tags: { test: 'value' } } } + } + end + + it 'processes each hash keyset as a metric iteration' do + expect(mock_datadog).to receive(:count).with('Counter_1', 1, {}) + expect(mock_datadog).to receive(:count).with('Counter_2', 2, tags: ['test:value']) + expect(mock_datadog).to receive(:distribution).with('Distribution_1', 10, tags: ['test:value']) + expect(mock_datadog).to receive(:set).with('Set_1', 100, tags: ['test:value']) + expect(mock_datadog).to receive(:gauge).with('Gauge_1', 100, tags: ['test:value']) + + result = subject.send(expected_call, datadog: datadog_payload) + + expect(result.success?).to eq true + expect(result.error).to eq nil + end + end + + context 'skip_processing_empty_datadog_args' do + let(:datadog_payload) { {} } + + it 'skips any metric processing' do + expect(mock_datadog).not_to receive(:count) + expect(mock_datadog).not_to receive(:distribution) + expect(mock_datadog).not_to receive(:histogram) + expect(mock_datadog).not_to receive(:set) + expect(mock_datadog).not_to receive(:gauge) + + result = subject.send(expected_call, datadog: datadog_payload) + + expect(result.success?).to eq true + expect(result.error).to eq nil + end + end + + context 'processes_hashed_inputs with value and tag is nil' do + let(:datadog_payload) do + { + count: { 'Counter_1' => { value: 1, tag: nil } } + } + end + + it 'processes each hash keyset as a metric iteration' do + expect(mock_datadog).to receive(:count).with('Counter_1', 1, {}) + + result = subject.send(expected_call, datadog: datadog_payload) + + expect(result.success?).to eq true + end + end + + context 'rejects_invalid_datadog_args' do + INVALID_PAYLOADS.each do |datadog_value| + context 'Invalid datadog top-level args' do + let(:datadog_payload) { datadog_value } + + it 'rejects the payload when invalid datadog values are given' do + expect(mock_datadog).not_to receive(:count) + expect(mock_datadog).not_to receive(:distribution) + expect(mock_datadog).not_to receive(:histogram) + expect(mock_datadog).not_to receive(:set) + expect(mock_datadog).not_to receive(:gauge) + + result = subject.send(expected_call, datadog: datadog_payload) + + expect(result.success?).to eq false + expect(result.error).to eq 'Invalid datadog config' + end + end + end + end + + context 'rejects_invalid_metric_args' do + EventTracer::DatadogLogger::SUPPORTED_METRICS.each do |metric| + INVALID_PAYLOADS.each do |payload| + context "Invalid metric values for #{metric}: #{payload}" do + let(:datadog_payload) { { metric => payload } } + + it 'rejects the payload when invalid datadog values are given' do + expect(mock_datadog).not_to receive(:count) + expect(mock_datadog).not_to receive(:distribution) + expect(mock_datadog).not_to receive(:histogram) + expect(mock_datadog).not_to receive(:set) + expect(mock_datadog).not_to receive(:gauge) + + result = subject.send(expected_call, datadog: datadog_payload) + + expect(result.success?).to eq false + expect(result.error).to eq "Datadog metric #{metric} invalid" + end + end + end + end + end + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index b007403..299fbac 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,11 +1,12 @@ -require "bundler/setup" -require "event_tracer" -require "data_helpers/mock_logger" -require "data_helpers/mock_appsignal" +require 'bundler/setup' +require 'event_tracer' +require 'data_helpers/mock_logger' +require 'data_helpers/mock_appsignal' +require 'data_helpers/mock_datadog' RSpec.configure do |config| # Enable flags like --only-failures and --next-failure - config.example_status_persistence_file_path = ".rspec_status" + config.example_status_persistence_file_path = '.rspec_status' config.expect_with :rspec do |c| c.syntax = :expect