Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[grape] add Grape integration for API endpoints #117

Merged
merged 16 commits into from
Apr 21, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Appraisals
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
3 changes: 2 additions & 1 deletion Rakefile
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down Expand Up @@ -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'
Expand Down
25 changes: 25 additions & 0 deletions docs/GettingStarted.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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...)
Expand Down
1 change: 1 addition & 0 deletions gemfiles/contrib.gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
source "https://rubygems.org"

gem "elasticsearch-transport"
gem "grape"
gem "rack"
gem "rack-test"
gem "redis"
Expand Down
3 changes: 2 additions & 1 deletion lib/ddtrace.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
164 changes: 164 additions & 0 deletions lib/ddtrace/contrib/grape/endpoint.rb
Original file line number Diff line number Diff line change
@@ -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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doing it this way limits us to "one Grape service per process" as this is shared app wide AFAIK. We can live with it, but it's worth noting. I suspect in most cases it's OK, several apps running on the same server could still override the default with their own respective services names, and after all, it looks like Grape is like this by design.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You usually have one Grape for each application (and so process). If this is not true, it means that you're mounting 2 different Grape applications in the same application. If we want to support that, it means that we should even support having 2 Rails applications running together in the same process.

The major issue, is that supporting this is not so easy. We don't have instances here and adding PIN references in all endpoints and subscribers is quite complex I think.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup, not worth the complexity of attaching the PIN to instances, I'm just saying -> let's keep aware we have this limitation.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea, considering that the standard case is having one Grape application with namespaces. We still not cover some corner case by the way.

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
73 changes: 73 additions & 0 deletions lib/ddtrace/contrib/grape/patcher.rb
Original file line number Diff line number Diff line change
@@ -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
18 changes: 18 additions & 0 deletions lib/ddtrace/contrib/rails/framework.rb
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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,
Expand Down Expand Up @@ -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]
Expand Down
Loading