From fd6d2c0bf0a91079d78a6fab1ac2e7aa77a1a64a Mon Sep 17 00:00:00 2001 From: Nicolas Martyanoff Date: Fri, 24 Feb 2017 17:25:06 +0100 Subject: [PATCH] add sidekiq integration --- .rubocop.yml | 3 + Appraisals | 1 + Rakefile | 6 +- docs/GettingStarted.md | 45 ++++++++++++++ lib/ddtrace/contrib/sidekiq/tracer.rb | 52 ++++++++++++++++ lib/ddtrace/ext/app_types.rb | 1 + test/contrib/sidekiq/disabled_tracer_test.rb | 26 ++++++++ test/contrib/sidekiq/tracer_test.rb | 63 ++++++++++++++++++++ test/contrib/sidekiq/tracer_test_base.rb | 27 +++++++++ 9 files changed, 222 insertions(+), 2 deletions(-) create mode 100644 lib/ddtrace/contrib/sidekiq/tracer.rb create mode 100644 test/contrib/sidekiq/disabled_tracer_test.rb create mode 100644 test/contrib/sidekiq/tracer_test.rb create mode 100644 test/contrib/sidekiq/tracer_test_base.rb diff --git a/.rubocop.yml b/.rubocop.yml index 1c131b7f411..cd6d046c316 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -47,3 +47,6 @@ Metrics/CyclomaticComplexity: Metrics/PerceivedComplexity: Max: 15 + +Lint/UnusedMethodArgument: + Enabled: false diff --git a/Appraisals b/Appraisals index 756d811f7f2..d451567d49c 100644 --- a/Appraisals +++ b/Appraisals @@ -66,4 +66,5 @@ appraise "contrib" do gem "hiredis" gem "rack-test" gem "sinatra" + gem "sidekiq" end diff --git a/Rakefile b/Rakefile index d5a69751387..d5f4a45e0f8 100644 --- a/Rakefile +++ b/Rakefile @@ -6,7 +6,7 @@ require 'appraisal' require 'yard' namespace :test do - task all: [:main, :rails, :railsredis, :elasticsearch, :http, :redis, :sinatra, :monkey] + task all: [:main, :rails, :railsredis, :elasticsearch, :http, :redis, :sidekiq, :sinatra, :monkey] Rake::TestTask.new(:main) do |t| t.libs << %w(test lib) @@ -30,7 +30,7 @@ namespace :test do t.test_files = FileList['test/contrib/rails/**/*redis*_test.rb'] end - [:elasticsearch, :http, :redis, :sinatra].each do |contrib| + [:elasticsearch, :http, :redis, :sinatra, :sidekiq].each do |contrib| Rake::TestTask.new(contrib) do |t| t.libs << %w(test lib) t.test_files = FileList["test/contrib/#{contrib}/*_test.rb"] @@ -49,6 +49,7 @@ Rake::TestTask.new(:benchmark) do |t| end RuboCop::RakeTask.new(:rubocop) do |t| + t.options << ['-D'] t.patterns = ['lib/**/*.rb', 'test/**/*.rb', 'Gemfile', 'Rakefile'] end @@ -114,6 +115,7 @@ task :ci do sh 'rvm $MRI_VERSIONS --verbose do appraisal contrib rake test:http' sh 'rvm $MRI_VERSIONS --verbose do appraisal contrib rake test:redis' sh 'rvm $MRI_VERSIONS --verbose do appraisal contrib rake test:sinatra' + sh 'rvm $MRI_VERSIONS --verbose do appraisal contrib rake test:sidekiq' when 2 sh 'rvm $RAILS_VERSIONS --verbose do appraisal rails3-postgres rake test:rails' sh 'rvm $RAILS_VERSIONS --verbose do appraisal rails3-mysql2 rake test:rails' diff --git a/docs/GettingStarted.md b/docs/GettingStarted.md index 6a01ab1d1f8..0e3fed823fe 100644 --- a/docs/GettingStarted.md +++ b/docs/GettingStarted.md @@ -175,6 +175,47 @@ Net::HTTP module. content = Net::HTTP.get(URI('http://127.0.0.1/index.html')) +### Sidekiq + +The Sidekiq integration is a server-side middleware which will trace job +executions. It can be added as any other Sidekiq middleware: + + require 'sidekiq' + require 'ddtrace' + require 'ddtrace/contrib/sidekiq/tracer' + + Sidekiq.configure_server do |config| + config.server_middleware do |chain| + chain.add(Datadog::Contrib::Sidekiq::Tracer, debug: true) + end + end + +#### Configure the tracer + +To modify the default configuration, simply pass arguments to the middleware. +For example, to change the default service name and activate the debug mode: + + Sidekiq.configure_server do |config| + config.server_middleware do |chain| + chain.add(Datadog::Contrib::Sidekiq::Tracer, + default_service: 'my_app', debug: true) + end + end + +Available settings are: + +* ``enabled``: define if the ``tracer`` is enabled or not. If set to + ``false``, the code is still instrumented but no spans are sent to the local + trace agent. +* ``default_service``: set the service name used when tracing application + requests. Defaults to ``sidekiq``. +* ``tracer``: set the tracer to use. Usually you don't need to change that + value unless you're already using a different initialized tracer somewhere + else. +* ``debug``: set to ``true`` to enable debug logging. +* ``trace_agent_hostname``: set the hostname of the trace agent. +* ``trace_agent_port``: set the port the trace agent is listening on. + ## Advanced usage ### Manual Instrumentation @@ -360,6 +401,10 @@ The currently supported web server are: Currently we are supporting Sinatra >= 1.4.0. +#### Sidekiq versions + +Currently we are supporting Sidekiq >= 4.0.0. + ### Glossary * ``Service``: The name of a set of processes that do the same job. Some examples are ``datadog-web-app`` or ``datadog-metrics-db``. diff --git a/lib/ddtrace/contrib/sidekiq/tracer.rb b/lib/ddtrace/contrib/sidekiq/tracer.rb new file mode 100644 index 00000000000..0a9a17904b1 --- /dev/null +++ b/lib/ddtrace/contrib/sidekiq/tracer.rb @@ -0,0 +1,52 @@ + +require 'sidekiq/api' + +require 'ddtrace/ext/app_types' + +sidekiq_vs = Gem::Version.new(Sidekiq::VERSION) +sidekiq_min_vs = Gem::Version.new('4.0.0') +if sidekiq_vs < sidekiq_min_vs + raise "sidekiq version #{sidekiq_vs} is not supported yet " \ + + "(supporting versions >=#{sidekiq_min_vs})" +end + +Datadog::Tracer.log.info("activating instrumentation for sidekiq #{sidekiq_vs}") + +module Datadog + module Contrib + module Sidekiq + # Middleware is a Sidekiq server-side middleware which traces executed + # jobs. + class Tracer + def initialize(options) + @enabled = options.fetch(:enabled, true) + @default_service = options.fetch(:default_service, 'sidekiq') + @tracer = options.fetch(:tracer, Datadog.tracer) + @debug = options.fetch(:debug, false) + @trace_agent_hostname = options.fetch(:trace_agent_hostname, + Datadog::Writer::HOSTNAME) + @trace_agent_port = options.fetch(:trace_agent_port, + Datadog::Writer::PORT) + + Datadog::Tracer.debug_logging = @debug + + @tracer.enabled = @enabled + @tracer.set_service_info(@default_service, 'sidekiq', + Datadog::Ext::AppTypes::JOB_PROCESSOR) + end + + def call(worker, job, queue) + return yield unless @enabled + + @tracer.trace('sidekiq.job', + service: @default_service, span_type: 'job') do |span| + span.resource = job['class'] + span.set_tag('sidekiq.job.queue', job['queue']) + + yield + end + end + end + end + end +end diff --git a/lib/ddtrace/ext/app_types.rb b/lib/ddtrace/ext/app_types.rb index 80977b07e6c..c9a92fb7ecc 100644 --- a/lib/ddtrace/ext/app_types.rb +++ b/lib/ddtrace/ext/app_types.rb @@ -4,6 +4,7 @@ module AppTypes WEB = 'web'.freeze DB = 'db'.freeze CACHE = 'cache'.freeze + JOB_PROCESSOR = 'job_processor'.freeze end end end diff --git a/test/contrib/sidekiq/disabled_tracer_test.rb b/test/contrib/sidekiq/disabled_tracer_test.rb new file mode 100644 index 00000000000..a21339c04bf --- /dev/null +++ b/test/contrib/sidekiq/disabled_tracer_test.rb @@ -0,0 +1,26 @@ + +require 'contrib/sidekiq/tracer_test_base' + +class DisabledTracerTest < TracerTestBase + class EmptyWorker + include Sidekiq::Worker + + def perform; end + end + + def setup + super + + Sidekiq::Testing.server_middleware do |chain| + chain.add(Datadog::Contrib::Sidekiq::Tracer, + tracer: @tracer, enabled: false) + end + end + + def test_empty + EmptyWorker.perform_async() + + spans = @writer.spans() + assert_equal(0, spans.length) + end +end diff --git a/test/contrib/sidekiq/tracer_test.rb b/test/contrib/sidekiq/tracer_test.rb new file mode 100644 index 00000000000..306b1ad61b2 --- /dev/null +++ b/test/contrib/sidekiq/tracer_test.rb @@ -0,0 +1,63 @@ + +require 'contrib/sidekiq/tracer_test_base' + +class TracerTest < TracerTestBase + class TestError < StandardError; end + + class EmptyWorker + include Sidekiq::Worker + + def perform(); end + end + + class ErrorWorker + include Sidekiq::Worker + + def perform + raise TestError, 'job error' + end + end + + def setup + super + + Sidekiq::Testing.server_middleware do |chain| + chain.add(Datadog::Contrib::Sidekiq::Tracer, + tracer: @tracer, enabled: true) + end + end + + def test_empty + EmptyWorker.perform_async() + + spans = @writer.spans() + assert_equal(1, spans.length) + + span = spans[0] + assert_equal('sidekiq', span.service) + assert_equal('TracerTest::EmptyWorker', span.resource) + assert_equal('default', span.get_tag('sidekiq.job.queue')) + assert_equal(0, span.status) + assert_nil(span.parent) + end + + # rubocop:disable Lint/HandleExceptions + def test_error + begin + ErrorWorker.perform_async() + rescue TestError + end + + spans = @writer.spans() + assert_equal(1, spans.length) + + span = spans[0] + assert_equal('sidekiq', span.service) + assert_equal('TracerTest::ErrorWorker', span.resource) + assert_equal('default', span.get_tag('sidekiq.job.queue')) + assert_equal(1, span.status) + assert_equal('job error', span.get_tag(Datadog::Ext::Errors::MSG)) + assert_equal('TracerTest::TestError', span.get_tag(Datadog::Ext::Errors::TYPE)) + assert_nil(span.parent) + end +end diff --git a/test/contrib/sidekiq/tracer_test_base.rb b/test/contrib/sidekiq/tracer_test_base.rb new file mode 100644 index 00000000000..5975793d4cc --- /dev/null +++ b/test/contrib/sidekiq/tracer_test_base.rb @@ -0,0 +1,27 @@ + +require 'sidekiq/testing' +require 'ddtrace' +require 'ddtrace/contrib/sidekiq/tracer' +require 'helper' + +class TracerTestBase < Minitest::Test + REDIS_HOST = '127.0.0.1'.freeze() + REDIS_PORT = 46379 + + def setup + @writer = FauxWriter.new() + @tracer = Datadog::Tracer.new(writer: @writer) + + redis_url = "redis://#{REDIS_HOST}:#{REDIS_PORT}" + + Sidekiq.configure_client do |config| + config.redis = { url: redis_url } + end + + Sidekiq.configure_server do |config| + config.redis = { url: redis_url } + end + + Sidekiq::Testing.inline! + end +end