-
Notifications
You must be signed in to change notification settings - Fork 377
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
Changes from all commits
Commits
Show all changes
16 commits
Select commit
Hold shift + click to select a range
6236baa
[grape] add appraisal grape
92c6560
[grape] provide the first draft implementation
5d7acff
[grape] bootstrap testing suite for standalone Grape
f1673dc
[grape] provide patch() implementation; Monkey module patches Grape
9fa4b73
[grape] instrumentation uses the PIN object; Grape works even without…
ff6aa96
[grape] handle exceptions and errors in Grape endpoints
f18c765
[grape] patch Grape before defining tests; ensure that the render met…
ef0f288
[grape] Grape integration plays well with the Rack middleware
65e4bc5
[grape] rubocop cosmetics
289de6a
[grape] support for Grape 0.19 requires Ruby > 2.1
76e596b
[rails] auto_instrument grape if enabled
4aa47e2
[grape] ensure that spans are closed
1c39800
[grape] update Monkey tests
3f8afa4
[grape] change endpoint comment
d2334b3
[grape] add Grape documentation
45dc54b
[docs] moved Grape to other libraries
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.