diff --git a/Appraisals b/Appraisals index 7d44854299f..7cd18fa9a24 100644 --- a/Appraisals +++ b/Appraisals @@ -92,6 +92,7 @@ end if RUBY_VERSION >= '2.2.2' appraise 'contrib' do gem 'elasticsearch-transport' + gem 'grape' gem 'rack' gem 'rack-test' gem 'redis' diff --git a/Rakefile b/Rakefile index 1983688f8f0..bf483a0bf6c 100644 --- a/Rakefile +++ b/Rakefile @@ -44,7 +44,7 @@ namespace :test do t.test_files = FileList['test/contrib/rails/**/*active_job*_test.rb'] end - [:elasticsearch, :http, :redis, :sinatra, :sidekiq, :rack].each do |contrib| + [:elasticsearch, :http, :redis, :sinatra, :sidekiq, :rack, :grape].each do |contrib| Rake::TestTask.new(contrib) do |t| t.libs << %w[test lib] t.test_files = FileList["test/contrib/#{contrib}/*_test.rb"] @@ -133,6 +133,7 @@ task :ci do sh 'rvm $MRI_VERSIONS --verbose do appraisal contrib rake test:sinatra' sh 'rvm $MRI_VERSIONS --verbose do appraisal contrib rake test:sidekiq' sh 'rvm $MRI_VERSIONS --verbose do appraisal contrib rake test:rack' + sh 'rvm $MRI_VERSIONS --verbose do appraisal contrib rake test:grape' sh 'rvm $MRI_OLD_VERSIONS --verbose do appraisal contrib-old rake test:monkey' sh 'rvm $MRI_OLD_VERSIONS --verbose do appraisal contrib-old rake test:elasticsearch' sh 'rvm $MRI_OLD_VERSIONS --verbose do appraisal contrib-old rake test:http' diff --git a/docs/GettingStarted.md b/docs/GettingStarted.md index 55d52e0e658..347463f3556 100644 --- a/docs/GettingStarted.md +++ b/docs/GettingStarted.md @@ -27,6 +27,7 @@ provides auto instrumentation for the following web frameworks and libraries: * [Sidekiq](#label-Sidekiq) * [Sinatra](#label-Sinatra) * [Rack](#label-Rack) +* [Grape](#label-Grape) * [Active Record](#label-Active+Record) * [Elastic Search](#label-Elastic+Search) * [Net/HTTP](#label-Net/HTTP) @@ -84,11 +85,15 @@ Available settings are: with a condition, to enable the auto-instrumentation only for particular environments (production, staging, etc...). * ``auto_instrument_redis``: if set to ``true`` Redis calls will be traced as such. Calls to Redis cache may be still instrumented but you will not have the detail of low-level Redis calls. +* ``auto_instrument_grape``: if set to ``true`` and you're using a Grape application, all calls to your endpoints are + traced, including filters execution. * ``default_service``: set the service name used when tracing application requests. Defaults to ``rails-app`` * ``default_controller_service``: set the service name used when tracing a Rails action controller. Defaults to ``rails-controller`` * ``default_cache_service``: set the cache service name used when tracing cache activity. Defaults to ``rails-cache`` * ``default_database_service``: set the database service name used when tracing database activity. Defaults to the current adapter name, so if you're using PostgreSQL it will be ``postgres``. +* ``default_grape_service``: set the service name used when tracing a Grape application mounted in your Rails router. + Defaults to ``grape`` * ``template_base_path``: used when the template name is parsed in the auto instrumented code. If you don't store your templates in the ``views/`` folder, you may need to change this value * ``tracer``: is the global tracer used by the tracing application. Usually you don't need to change that value @@ -176,6 +181,26 @@ Available settings are: ## Other libraries +### Grape + +The Grape integration adds the instrumentation to Grape endpoints and filters. This integration can work side by side +with other integrations like Rack and Rails. To activate your integration, use the ``patch_module`` function before +defining your Grape application: + + # api.rb + require 'grape' + require 'ddtrace' + + Datadog::Monkey.patch_module(:grape) + + # then define your application + class RackTestingAPI < Grape::API + desc 'main endpoint' + get :success do + 'Hello world!' + end + end + ### Active Record Most of the time, Active Record is set up as part of a web framework (Rails, Sinatra...) diff --git a/gemfiles/contrib.gemfile b/gemfiles/contrib.gemfile index 597148a6006..e35b63aeac3 100644 --- a/gemfiles/contrib.gemfile +++ b/gemfiles/contrib.gemfile @@ -3,6 +3,7 @@ source "https://rubygems.org" gem "elasticsearch-transport" +gem "grape" gem "rack" gem "rack-test" gem "redis" diff --git a/lib/ddtrace.rb b/lib/ddtrace.rb index 63891c0017f..c0996e71193 100644 --- a/lib/ddtrace.rb +++ b/lib/ddtrace.rb @@ -40,7 +40,8 @@ class Railtie < Rails::Railtie Datadog::Contrib::Rails::Framework.configure(config: app.config) Datadog::Contrib::Rails::Framework.auto_instrument() Datadog::Contrib::Rails::Framework.auto_instrument_redis() - # + Datadog::Contrib::Rails::Framework.auto_instrument_grape() + # override Rack Middleware configurations with Rails options.update(::Rails.configuration.datadog_trace) end diff --git a/lib/ddtrace/contrib/grape/endpoint.rb b/lib/ddtrace/contrib/grape/endpoint.rb new file mode 100644 index 00000000000..c297c7d4a9a --- /dev/null +++ b/lib/ddtrace/contrib/grape/endpoint.rb @@ -0,0 +1,164 @@ +require 'ddtrace/ext/http' +require 'ddtrace/ext/errors' + +module Datadog + module Contrib + module Grape + # rubocop:disable Metrics/ModuleLength + # Endpoint module includes a list of subscribers to create + # traces when a Grape endpoint is hit + module Endpoint + KEY_RUN = 'datadog_grape_endpoint_run'.freeze + KEY_RENDER = 'datadog_grape_endpoint_render'.freeze + + def self.subscribe + # Grape is instrumented only if it's available + return unless defined?(::Grape) && defined?(::ActiveSupport::Notifications) + + # subscribe when a Grape endpoint is hit + ::ActiveSupport::Notifications.subscribe('endpoint_run.grape.start_process') do |*args| + endpoint_start_process(*args) + end + ::ActiveSupport::Notifications.subscribe('endpoint_run.grape') do |*args| + endpoint_run(*args) + end + ::ActiveSupport::Notifications.subscribe('endpoint_render.grape.start_render') do |*args| + endpoint_start_render(*args) + end + ::ActiveSupport::Notifications.subscribe('endpoint_render.grape') do |*args| + endpoint_render(*args) + end + ::ActiveSupport::Notifications.subscribe('endpoint_run_filters.grape') do |*args| + endpoint_run_filters(*args) + end + end + + def self.endpoint_start_process(*) + return if Thread.current[KEY_RUN] + + # retrieve the tracer from the PIN object + pin = Datadog::Pin.get_from(::Grape) + return unless pin && pin.enabled? + + # store the beginning of a trace + tracer = pin.tracer + service = pin.service + type = Datadog::Ext::HTTP::TYPE + tracer.trace('grape.endpoint_run', service: service, span_type: type) + + Thread.current[KEY_RUN] = true + rescue StandardError => e + Datadog::Tracer.log.error(e.message) + end + + def self.endpoint_run(name, start, finish, id, payload) + return unless Thread.current[KEY_RUN] + Thread.current[KEY_RUN] = false + + # retrieve the tracer from the PIN object + pin = Datadog::Pin.get_from(::Grape) + return unless pin && pin.enabled? + + tracer = pin.tracer + span = tracer.active_span() + return unless span + + begin + # collect endpoint details + api_view = payload[:endpoint].options[:for].to_s + path = payload[:endpoint].options[:path].join('/') + resource = "#{api_view}##{path}" + span.resource = resource + + # set the request span resource if it's a `rack.request` span + request_span = payload[:env][:datadog_rack_request_span] + if !request_span.nil? && request_span.name == 'rack.request' + request_span.resource = resource + end + + # catch thrown exceptions + span.set_error(payload[:exception_object]) unless payload[:exception_object].nil? + + # ovverride the current span with this notification values + span.set_tag('grape.route.endpoint', api_view) + span.set_tag('grape.route.path', path) + ensure + span.start_time = start + span.finish_at(finish) + end + rescue StandardError => e + Datadog::Tracer.log.error(e.message) + end + + def self.endpoint_start_render(*) + return if Thread.current[KEY_RENDER] + + # retrieve the tracer from the PIN object + pin = Datadog::Pin.get_from(::Grape) + return unless pin && pin.enabled? + + # store the beginning of a trace + tracer = pin.tracer + service = pin.service + type = Datadog::Ext::HTTP::TYPE + tracer.trace('grape.endpoint_render', service: service, span_type: type) + + Thread.current[KEY_RENDER] = true + rescue StandardError => e + Datadog::Tracer.log.error(e.message) + end + + def self.endpoint_render(name, start, finish, id, payload) + return unless Thread.current[KEY_RENDER] + Thread.current[KEY_RENDER] = false + + # retrieve the tracer from the PIN object + pin = Datadog::Pin.get_from(::Grape) + return unless pin && pin.enabled? + + tracer = pin.tracer + span = tracer.active_span() + return unless span + + # catch thrown exceptions + begin + span.set_error(payload[:exception_object]) unless payload[:exception_object].nil? + ensure + span.start_time = start + span.finish_at(finish) + end + rescue StandardError => e + Datadog::Tracer.log.error(e.message) + end + + def self.endpoint_run_filters(name, start, finish, id, payload) + # retrieve the tracer from the PIN object + pin = Datadog::Pin.get_from(::Grape) + return unless pin && pin.enabled? + + # safe-guard to prevent submitting empty filters + zero_length = (finish - start).zero? + filters = payload[:filters] + type = payload[:type] + return if (!filters || filters.empty?) || !type || zero_length + + tracer = pin.tracer + service = pin.service + type = Datadog::Ext::HTTP::TYPE + span = tracer.trace('grape.endpoint_run_filters', service: service, span_type: type) + + begin + # catch thrown exceptions + span.set_error(payload[:exception_object]) unless payload[:exception_object].nil? + span.set_tag('grape.filter.type', type.to_s) + ensure + span.start_time = start + span.finish_at(finish) + end + rescue StandardError => e + Datadog::Tracer.log.error(e.message) + end + end + end + end +end diff --git a/lib/ddtrace/contrib/grape/patcher.rb b/lib/ddtrace/contrib/grape/patcher.rb new file mode 100644 index 00000000000..f24e7aa82ac --- /dev/null +++ b/lib/ddtrace/contrib/grape/patcher.rb @@ -0,0 +1,73 @@ +module Datadog + module Contrib + module Grape + SERVICE = 'grape'.freeze + + # Patcher that introduces more instrumentation for Grape endpoints, so that + # new signals are executed at the beginning of each step (filters, render and run) + module Patcher + @patched = false + + module_function + + def patched? + @patched + end + + def patch + if !@patched && defined?(::Grape) + begin + # do not require these by default, but only when actually patching + require 'ddtrace' + require 'ddtrace/ext/app_types' + require 'ddtrace/contrib/grape/endpoint' + + @patched = true + # patch all endpoints + patch_endpoint_run() + patch_endpoint_render() + + # attach a PIN object globally and set the service once + pin = Datadog::Pin.new(SERVICE, app: 'grape', app_type: Datadog::Ext::AppTypes::WEB) + pin.onto(::Grape) + if pin.tracer && pin.service + pin.tracer.set_service_info(pin.service, 'grape', pin.app_type) + end + + # subscribe to ActiveSupport events + Datadog::Contrib::Grape::Endpoint.subscribe() + rescue StandardError => e + Datadog::Tracer.log.error("Unable to apply Grape integration: #{e}") + end + end + @patched + end + + def patch_endpoint_run + ::Grape::Endpoint.class_eval do + alias_method :run_without_datadog, :run + def run(*args) + ::ActiveSupport::Notifications.instrument('endpoint_run.grape.start_process') + run_without_datadog(*args) + end + end + end + + def patch_endpoint_render + ::Grape::Endpoint.class_eval do + class << self + alias_method :generate_api_method_without_datadog, :generate_api_method + def generate_api_method(*params, &block) + method_api = generate_api_method_without_datadog(*params, &block) + proc do |*args| + ::ActiveSupport::Notifications.instrument('endpoint_render.grape.start_render') + method_api.call(*args) + end + end + end + end + end + end + end + end +end diff --git a/lib/ddtrace/contrib/rails/framework.rb b/lib/ddtrace/contrib/rails/framework.rb index bdeb65e30be..e6c0b662b4f 100644 --- a/lib/ddtrace/contrib/rails/framework.rb +++ b/lib/ddtrace/contrib/rails/framework.rb @@ -1,6 +1,9 @@ +require 'ddtrace/pin' require 'ddtrace/ext/app_types' +require 'ddtrace/contrib/grape/endpoint' require 'ddtrace/contrib/rack/middlewares' + require 'ddtrace/contrib/rails/core_extensions' require 'ddtrace/contrib/rails/action_controller' require 'ddtrace/contrib/rails/action_view' @@ -21,9 +24,11 @@ module Framework enabled: true, auto_instrument: false, auto_instrument_redis: false, + auto_instrument_grape: false, default_service: 'rails-app', default_controller_service: 'rails-controller', default_cache_service: 'rails-cache', + default_grape_service: 'grape', template_base_path: 'views/', tracer: Datadog.tracer, debug: false, @@ -123,6 +128,19 @@ def self.auto_instrument_redis end end + def self.auto_instrument_grape + return unless ::Rails.configuration.datadog_trace[:auto_instrument_grape] + + # patch the Grape library so that endpoints are traced + Datadog::Monkey.patch_module(:grape) + + # update the Grape pin object + pin = Datadog::Pin.get_from(::Grape) + return unless pin && pin.enabled? + pin.tracer = ::Rails.configuration.datadog_trace[:tracer] + pin.service = ::Rails.configuration.datadog_trace[:default_grape_service] + end + # automatically instrument all Rails component def self.auto_instrument return unless ::Rails.configuration.datadog_trace[:auto_instrument] diff --git a/lib/ddtrace/monkey.rb b/lib/ddtrace/monkey.rb index 5c6a8892984..adcfcdf1039 100644 --- a/lib/ddtrace/monkey.rb +++ b/lib/ddtrace/monkey.rb @@ -5,14 +5,21 @@ # patching code, which is required on demand, when patching. require 'ddtrace/contrib/active_record/patcher' require 'ddtrace/contrib/elasticsearch/patcher' -require 'ddtrace/contrib/http/patcher' +require 'ddtrace/contrib/grape/patcher' require 'ddtrace/contrib/redis/patcher' +require 'ddtrace/contrib/http/patcher' module Datadog # Monkey is used for monkey-patching 3rd party libs. module Monkey @patched = [] - @autopatch_modules = { elasticsearch: true, http: true, redis: true, active_record: false } + @autopatch_modules = { + elasticsearch: true, + http: true, + redis: true, + grape: true, + active_record: false + } # Patchers should expose 2 methods: # - patch, which applies our patch if needed. Should be idempotent, # can be call twice but should just do nothing the second time. @@ -21,6 +28,7 @@ module Monkey @patchers = { elasticsearch: Datadog::Contrib::Elasticsearch::Patcher, http: Datadog::Contrib::HTTP::Patcher, redis: Datadog::Contrib::Redis::Patcher, + grape: Datadog::Contrib::Grape::Patcher, active_record: Datadog::Contrib::ActiveRecord::Patcher } @mutex = Mutex.new diff --git a/test/contrib/grape/app.rb b/test/contrib/grape/app.rb new file mode 100644 index 00000000000..7eff7f5bb31 --- /dev/null +++ b/test/contrib/grape/app.rb @@ -0,0 +1,64 @@ +require 'helper' +require 'rack/test' + +require 'grape' +require 'ddtrace/pin' +require 'ddtrace/contrib/grape/patcher' + +# patch Grape before the application +Datadog::Contrib::Grape::Patcher.patch() + +class TestingAPI < Grape::API + namespace :base do + desc 'Returns a success message' + get :success do + 'OK' + end + + desc 'Returns an error' + get :hard_failure do + raise StandardError, 'Ouch!' + end + end + + namespace :filtered do + before do + sleep(0.01) + end + + after do + sleep(0.01) + end + + desc 'Returns a success message before and after filter processing' + get :before_after do + 'OK' + end + end + + namespace :filtered_exception do + before do + raise StandardError, 'Ouch!' + end + + desc 'Returns an error in the filter' + get :before do + 'OK' + end + end +end + +class BaseAPITest < MiniTest::Test + include Rack::Test::Methods + + def app + TestingAPI + end + + def setup + # use a dummy tracer + @tracer = get_test_tracer() + pin = Datadog::Pin.get_from(::Grape) + pin.tracer = @tracer + end +end diff --git a/test/contrib/grape/rack_app.rb b/test/contrib/grape/rack_app.rb new file mode 100644 index 00000000000..5730fb12802 --- /dev/null +++ b/test/contrib/grape/rack_app.rb @@ -0,0 +1,48 @@ +require 'grape' +require 'helper' + +require 'ddtrace' +require 'ddtrace/pin' +require 'ddtrace/contrib/grape/patcher' +require 'ddtrace/contrib/rack/middlewares' + +require 'rack/test' + +# patch Grape before the application +Datadog::Contrib::Grape::Patcher.patch() + +class RackTestingAPI < Grape::API + desc 'Returns a success message' + get :success do + 'OK' + end + + desc 'Returns an error' + get :hard_failure do + raise StandardError, 'Ouch!' + end +end + +class BaseRackAPITest < MiniTest::Test + include Rack::Test::Methods + + def app + tracer = @tracer + + # create a custom Rack application with the Rack middleware and a Grape API + Rack::Builder.new do + use Datadog::Contrib::Rack::TraceMiddleware, tracer: tracer + map '/api/' do + run RackTestingAPI + end + end.to_app + end + + def setup + # use a dummy tracer + @tracer = get_test_tracer() + pin = Datadog::Pin.get_from(::Grape) + pin.tracer = @tracer + super + end +end diff --git a/test/contrib/grape/rack_integration_test.rb b/test/contrib/grape/rack_integration_test.rb new file mode 100644 index 00000000000..7084d9d68f4 --- /dev/null +++ b/test/contrib/grape/rack_integration_test.rb @@ -0,0 +1,92 @@ +require 'contrib/grape/rack_app' + +# rubocop:disable Metrics/AbcSize +class TracedRackAPITest < BaseRackAPITest + def test_traced_api_with_rack + # it should play well with the Rack integration + get '/api/success' + assert last_response.ok? + assert_equal('OK', last_response.body) + + spans = @tracer.writer.spans() + assert_equal(spans.length, 3) + render = spans[0] + run = spans[1] + rack = spans[2] + + assert_equal(render.name, 'grape.endpoint_render') + assert_equal(render.span_type, 'http') + assert_equal(render.service, 'grape') + assert_equal(render.resource, 'grape.endpoint_render') + assert_equal(render.status, 0) + assert_equal(render.parent, run) + + assert_equal(run.name, 'grape.endpoint_run') + assert_equal(run.span_type, 'http') + assert_equal(run.service, 'grape') + assert_equal(run.resource, 'RackTestingAPI#success') + assert_equal(run.status, 0) + assert_equal(run.parent, rack) + + assert_equal(rack.name, 'rack.request') + assert_equal(rack.span_type, 'http') + assert_equal(rack.service, 'rack') + assert_equal(rack.resource, 'RackTestingAPI#success') + assert_equal(rack.status, 0) + assert_nil(rack.parent) + end + + def test_traced_api_failure_with_rack + # it should play well with the Rack integration even if an + # exception is thrown + assert_raises do + get '/api/hard_failure' + end + + spans = @tracer.writer.spans() + assert_equal(spans.length, 3) + render = spans[0] + run = spans[1] + rack = spans[2] + + assert_equal(render.name, 'grape.endpoint_render') + assert_equal(render.span_type, 'http') + assert_equal(render.service, 'grape') + assert_equal(render.resource, 'grape.endpoint_render') + assert_equal(render.status, 1) + assert_equal(render.get_tag('error.type'), 'StandardError') + assert_equal(render.get_tag('error.msg'), 'Ouch!') + assert_includes(render.get_tag('error.stack'), '') + assert_equal(render.parent, run) + + assert_equal(run.name, 'grape.endpoint_run') + assert_equal(run.span_type, 'http') + assert_equal(run.service, 'grape') + assert_equal(run.resource, 'RackTestingAPI#hard_failure') + assert_equal(run.status, 1) + assert_equal(run.parent, rack) + + assert_equal(rack.name, 'rack.request') + assert_equal(rack.span_type, 'http') + assert_equal(rack.service, 'rack') + assert_equal(rack.resource, 'RackTestingAPI#hard_failure') + assert_equal(rack.status, 1) + assert_nil(rack.parent) + end + + def test_traced_api_404_with_rack + # it should not impact the Rack integration that must work as usual + get '/api/not_existing' + + spans = @tracer.writer.spans() + assert_equal(spans.length, 1) + rack = spans[0] + + assert_equal(rack.name, 'rack.request') + assert_equal(rack.span_type, 'http') + assert_equal(rack.service, 'rack') + assert_equal(rack.resource, 'GET 404') + assert_equal(rack.status, 0) + assert_nil(rack.parent) + end +end diff --git a/test/contrib/grape/request_test.rb b/test/contrib/grape/request_test.rb new file mode 100644 index 00000000000..7b004a0a916 --- /dev/null +++ b/test/contrib/grape/request_test.rb @@ -0,0 +1,135 @@ +require 'contrib/grape/app' + +# rubocop:disable Metrics/AbcSize +class TracedAPITest < BaseAPITest + def test_traced_api_success + # it should trace the endpoint body + get '/base/success' + assert last_response.ok? + assert_equal('OK', last_response.body) + + spans = @tracer.writer.spans() + assert_equal(spans.length, 2) + render = spans[0] + run = spans[1] + + assert_equal(render.name, 'grape.endpoint_render') + assert_equal(render.span_type, 'http') + assert_equal(render.service, 'grape') + assert_equal(render.resource, 'grape.endpoint_render') + assert_equal(render.status, 0) + assert_equal(render.parent, run) + + assert_equal(run.name, 'grape.endpoint_run') + assert_equal(run.span_type, 'http') + assert_equal(run.service, 'grape') + assert_equal(run.resource, 'TestingAPI#success') + assert_equal(run.status, 0) + assert_nil(run.parent) + end + + def test_traced_api_exception + # it should handle exceptions + assert_raises do + get '/base/hard_failure' + end + + spans = @tracer.writer.spans() + assert_equal(spans.length, 2) + render = spans[0] + run = spans[1] + + assert_equal(render.name, 'grape.endpoint_render') + assert_equal(render.span_type, 'http') + assert_equal(render.service, 'grape') + assert_equal(render.resource, 'grape.endpoint_render') + assert_equal(render.status, 1) + assert_equal(render.get_tag('error.type'), 'StandardError') + assert_equal(render.get_tag('error.msg'), 'Ouch!') + assert_includes(render.get_tag('error.stack'), '') + assert_equal(render.parent, run) + + assert_equal(run.name, 'grape.endpoint_run') + assert_equal(run.span_type, 'http') + assert_equal(run.service, 'grape') + assert_equal(run.resource, 'TestingAPI#hard_failure') + assert_equal(run.status, 1) + assert_equal(run.get_tag('error.type'), 'StandardError') + assert_equal(run.get_tag('error.msg'), 'Ouch!') + assert_includes(run.get_tag('error.stack'), '') + assert_nil(run.parent) + end + + def test_traced_api_before_after_filters + # it should trace the endpoint body and all before/after filters + get '/filtered/before_after' + assert last_response.ok? + assert_equal('OK', last_response.body) + + spans = @tracer.writer.spans() + assert_equal(spans.length, 4) + before = spans[0] + render = spans[1] + after = spans[2] + run = spans[3] + + assert_equal(before.name, 'grape.endpoint_run_filters') + assert_equal(before.span_type, 'http') + assert_equal(before.service, 'grape') + assert_equal(before.resource, 'grape.endpoint_run_filters') + assert_equal(before.status, 0) + assert_equal(before.parent, run) + assert(before.to_hash[:duration] > 0.01) + + assert_equal(render.name, 'grape.endpoint_render') + assert_equal(render.span_type, 'http') + assert_equal(render.service, 'grape') + assert_equal(render.resource, 'grape.endpoint_render') + assert_equal(render.status, 0) + assert_equal(render.parent, run) + + assert_equal(after.name, 'grape.endpoint_run_filters') + assert_equal(after.span_type, 'http') + assert_equal(after.service, 'grape') + assert_equal(after.resource, 'grape.endpoint_run_filters') + assert_equal(after.status, 0) + assert_equal(after.parent, run) + assert(after.to_hash[:duration] > 0.01) + + assert_equal('grape.endpoint_run', run.name) + assert_equal('http', run.span_type) + assert_equal('grape', run.service) + assert_equal('TestingAPI#before_after', run.resource) + assert_equal(0, run.status) + assert_nil(run.parent) + end + + def test_traced_api_before_after_filters_exceptions + # it should trace the endpoint even if a filter raises an exception + assert_raises do + get '/filtered_exception/before' + end + + spans = @tracer.writer.spans() + assert_equal(spans.length, 2) + before = spans[0] + run = spans[1] + + assert_equal(before.name, 'grape.endpoint_run_filters') + assert_equal(before.span_type, 'http') + assert_equal(before.service, 'grape') + assert_equal(before.resource, 'grape.endpoint_run_filters') + assert_equal(before.status, 1) + assert_equal(before.get_tag('error.type'), 'StandardError') + assert_equal(before.get_tag('error.msg'), 'Ouch!') + assert_includes(before.get_tag('error.stack'), '') + assert_equal(before.parent, run) + + assert_equal(run.name, 'grape.endpoint_run') + assert_equal(run.span_type, 'http') + assert_equal(run.service, 'grape') + assert_equal(run.resource, 'TestingAPI#before') + assert_equal(run.status, 1) + assert_nil(run.parent) + end +end diff --git a/test/monkey_test.rb b/test/monkey_test.rb index 2cc3ca35a9c..9b273714f43 100644 --- a/test/monkey_test.rb +++ b/test/monkey_test.rb @@ -7,55 +7,65 @@ class MonkeyTest < Minitest::Test def test_autopatch_modules - assert_equal({ elasticsearch: true, http: true, redis: true, active_record: false }, Datadog::Monkey.autopatch_modules) + assert_equal( + { elasticsearch: true, http: true, redis: true, grape: true, active_record: false }, + Datadog::Monkey.autopatch_modules + ) end # rubocop:disable Metrics/AbcSize # rubocop:disable Metrics/LineLength + # rubocop:disable Metrics/MethodLength def test_patch_module # because of this test, this should be a separate rake task, # else the module could have been already imported in some other test assert_equal(false, Datadog::Contrib::Elasticsearch::Patcher.patched?) assert_equal(false, Datadog::Contrib::HTTP::Patcher.patched?) assert_equal(false, Datadog::Contrib::Redis::Patcher.patched?) + assert_equal(false, Datadog::Contrib::Grape::Patcher.patched?) assert_equal(false, Datadog::Contrib::ActiveRecord::Patcher.patched?) - assert_equal({ elasticsearch: false, http: false, redis: false, active_record: false }, Datadog::Monkey.get_patched_modules()) + assert_equal({ elasticsearch: false, http: false, redis: false, grape: false, active_record: false }, Datadog::Monkey.get_patched_modules()) Datadog::Monkey.patch_module(:redis) assert_equal(false, Datadog::Contrib::Elasticsearch::Patcher.patched?) assert_equal(false, Datadog::Contrib::HTTP::Patcher.patched?) assert_equal(true, Datadog::Contrib::Redis::Patcher.patched?) + assert_equal(false, Datadog::Contrib::Grape::Patcher.patched?) assert_equal(false, Datadog::Contrib::ActiveRecord::Patcher.patched?) - assert_equal({ elasticsearch: false, http: false, redis: true, active_record: false }, Datadog::Monkey.get_patched_modules()) + assert_equal({ elasticsearch: false, http: false, redis: true, grape: false, active_record: false }, Datadog::Monkey.get_patched_modules()) # now do it again to check it's idempotent Datadog::Monkey.patch_module(:redis) assert_equal(false, Datadog::Contrib::Elasticsearch::Patcher.patched?) assert_equal(false, Datadog::Contrib::HTTP::Patcher.patched?) assert_equal(true, Datadog::Contrib::Redis::Patcher.patched?) + assert_equal(false, Datadog::Contrib::Grape::Patcher.patched?) assert_equal(false, Datadog::Contrib::ActiveRecord::Patcher.patched?) - assert_equal({ elasticsearch: false, http: false, redis: true, active_record: false }, Datadog::Monkey.get_patched_modules()) + assert_equal({ elasticsearch: false, http: false, redis: true, grape: false, active_record: false }, Datadog::Monkey.get_patched_modules()) Datadog::Monkey.patch(elasticsearch: true, redis: true) assert_equal(true, Datadog::Contrib::Elasticsearch::Patcher.patched?) assert_equal(false, Datadog::Contrib::HTTP::Patcher.patched?) assert_equal(true, Datadog::Contrib::Redis::Patcher.patched?) + assert_equal(false, Datadog::Contrib::Grape::Patcher.patched?) assert_equal(false, Datadog::Contrib::ActiveRecord::Patcher.patched?) - assert_equal({ elasticsearch: true, http: false, redis: true, active_record: false }, Datadog::Monkey.get_patched_modules()) + assert_equal({ elasticsearch: true, http: false, redis: true, grape: false, active_record: false }, Datadog::Monkey.get_patched_modules()) # verify that active_record is not auto patched by default Datadog::Monkey.patch_all() assert_equal(true, Datadog::Contrib::Elasticsearch::Patcher.patched?) assert_equal(true, Datadog::Contrib::HTTP::Patcher.patched?) assert_equal(true, Datadog::Contrib::Redis::Patcher.patched?) + assert_equal(false, Datadog::Contrib::Grape::Patcher.patched?) assert_equal(false, Datadog::Contrib::ActiveRecord::Patcher.patched?) - assert_equal({ elasticsearch: true, http: true, redis: true, active_record: false }, Datadog::Monkey.get_patched_modules()) + assert_equal({ elasticsearch: true, http: true, redis: true, grape: false, active_record: false }, Datadog::Monkey.get_patched_modules()) Datadog::Monkey.patch_module(:active_record) assert_equal(true, Datadog::Contrib::Elasticsearch::Patcher.patched?) assert_equal(true, Datadog::Contrib::HTTP::Patcher.patched?) assert_equal(true, Datadog::Contrib::Redis::Patcher.patched?) + assert_equal(false, Datadog::Contrib::Grape::Patcher.patched?) assert_equal(true, Datadog::Contrib::ActiveRecord::Patcher.patched?) - assert_equal({ elasticsearch: true, http: true, redis: true, active_record: true }, Datadog::Monkey.get_patched_modules()) + assert_equal({ elasticsearch: true, http: true, redis: true, grape: false, active_record: true }, Datadog::Monkey.get_patched_modules()) end end