-
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
[rack] add Rack middleware to trace generic Rack applications #111
Conversation
6f11857
to
b8886da
Compare
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.
2 nitpicks but LGTM
@@ -3,13 +3,13 @@ | |||
source "https://rubygems.org" | |||
|
|||
gem "elasticsearch-transport" | |||
gem "rack" |
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.
I'm surprised contrib_old.gemfile
does not show up in the list of updated files. I suspect running appraisal with some older Ruby (say 1.9) should generate it.
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.
# representation for this trace, so we attach the span in the Rack env | ||
# so that the underlying Rack App can provide and set more details | ||
request_span = @tracer.trace('rack.request', service: @service, span_type: Datadog::Ext::HTTP::TYPE) | ||
request.env[:datadog_request_span] = request_span |
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.
Nitpicking, but I'm hesitating with something like :datadog_rack_request_span
, even more explicit. Because here, it's obvious it's a Rack request. But, within a framework (eg Rails) that is accessing this afterwards to update it, request
has also a local meaning (the rails request). So in the end you have the Rails request updating the meta (resource) for the Rack request, and in the long run, I'd say having rack
in the name makes it obvious it belongs to the Rack world. But then, :datadog_rack_request_span
is 26 chars so somewhat long ;) I let you decide on this, just a thought.
b8886da
to
86bb860
Compare
…ice at initialization time
86bb860
to
315548e
Compare
class TraceMiddleware | ||
DEFAULT_CONFIG = { | ||
tracer: Datadog.tracer, | ||
default_service: 'rack' |
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.
usually we're relying in the more settings to set other things like the hostname
or the port
. Here I think it's better to stay clean. This is a low level instrumentation and the "traditional" way to use it, is together with another instrumentation (i.e. a library or a framework like Sinatra/Rails). If we provide a hostname
here, it means that the tracer is configured twice and I don't like that, at some point, duplicated configuration can have weird side effects based on the initialization order (who configures the tracer first?).
As explained in the docs, if you want to use this middleware with custom configs, just use the tracer.configure()
method before adding the middleware.
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.
Agree we should not overdo it. As in most cases this is probably handled by the outstanding framework (Rails, Sinatra...) this should stay very simple.
* [Ruby on Rails](#label-Ruby+on+Rails) | ||
* [Sidekiq](#label-Sidekiq) | ||
* [Sinatra](#label-Sinatra) | ||
* [Rack](#label-Rack) |
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.
changed the order so that Web frameworks are at the top of the list; if you prefer I can revert the order to an alphabetical one
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.
Still a little issue concerning error handling I think, otherwise, amazing work, this should improve Rails instrumentation a lot.
class TraceMiddleware | ||
DEFAULT_CONFIG = { | ||
tracer: Datadog.tracer, | ||
default_service: 'rack' |
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.
Agree we should not overdo it. As in most cases this is probably handled by the outstanding framework (Rails, Sinatra...) this should stay very simple.
@tracer.set_service_info( | ||
@service, | ||
'rack', | ||
Datadog::Ext::AppTypes::WEB |
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.
Side note, wondering if, at some point we could have something like a Proxy
type. It's a generic pattern, after all, just, handling & routing things for the servers doing the real job. But just a side note, this is very fine for now.
resource: nil, | ||
span_type: Datadog::Ext::HTTP::TYPE | ||
) | ||
request.env[:datadog_rack_request_span] = request_span |
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.
👍
request_span.resource = "#{request.request_method} #{status}".strip unless request_span.resource | ||
request_span.set_tag('http.method', request.request_method) if request_span.get_tag('http.method').nil? | ||
request_span.set_tag('http.url', request.path_info) if request_span.get_tag('http.url').nil? | ||
request_span.set_tag('http.status_code', status) if request_span.get_tag('http.status_code').nil? && status |
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.
Here I think we want to catch 5xx errors (something like https://github.com/DataDog/dd-trace-rb/blob/master/lib/ddtrace/contrib/rails/action_controller.rb#L56 maybe). Note that there's always a difference between "being a Ruby exception and being an error span" (#103 shows a different case, but it illustrates both are somewhat independent).
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.
map '/app/' do | ||
run(proc do |env| | ||
# this should be considered a web framework that can alter | ||
# the request span after routing / controller processing |
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.
Thanks for deep testing all this, thumbs up 👍 !
assert_equal('GET', span.get_tag('http.method')) | ||
assert_equal('404', span.get_tag('http.status_code')) | ||
assert_equal('/not/exists/', span.get_tag('http.url')) | ||
assert_equal(0, span.status) |
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.
👍
assert_equal(middleware.instance_eval { @tracer }, tracer) | ||
assert_equal(middleware.instance_eval { @service }, 'custom-rack') | ||
end | ||
end |
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.
Would probably add a test with a 500 error (typically some handling that would return a 5xx but not necessarily yield an exception, those corner cases always bite us at some point) just to make clear how it behaves in such a case.
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.
96397de
to
ea0d4c0
Compare
ea0d4c0
to
ba6686b
Compare
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.
GTM!
request_span.status = 1 | ||
# in any case we don't touch the stacktrace if it has been set | ||
if request_span.get_tag(Datadog::Ext::Errors::STACK).nil? | ||
request_span.set_tag(Datadog::Ext::Errors::STACK, caller().join("\n")) |
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.
Exactly.
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.
I don't think caller
is of much use here, this will only give the callstack up to this point which won't even include a single line of code that has been executed down the line in the middleware. I think that info is misleading, it's probably better to not have anything here.
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.
yes we were deciding to remove that even from Rails but since this is already a major change, we always prefer to be consistent rather than breaking the current behavior of the library. Anyway this is in the list and we'll provide an update when new changes are settled.
assert_equal('GET', span.get_tag('http.method')) | ||
assert_equal('500', span.get_tag('http.status_code')) | ||
assert_equal('/app/500/no_status/', span.get_tag('http.url')) | ||
assert_equal(1, span.status) |
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.
👍
|
||
# call the rest of the stack | ||
status, headers, response = @app.call(env) | ||
rescue StandardError => e |
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.
It might have been better to use Exception => e
since it's higher up in the hierarchy than StandardError
, to make sure this catches as much as possible.
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.
Actually we're not using Exception
in other parts so we kept that just to be consistent. We may update that later if a higher level of Exception
hierarchy must be caught. Thanks!
# this should be considered a web framework that can alter | ||
# the request span after routing / controller processing | ||
request_span = env[:datadog_rack_request_span] | ||
request_span.set_tag('error.stack', 'Handled exception') |
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.
Since this test is about the framework not having marked the trace as status = 1
, I would have also not have set the stacktrace to see how the code from the middleware handles that.
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.
the test is about checking this code path when an inconsistent behavior is executed (setting the stacktrace but not the status). We may add a new test anyway! thanks for clarifying that!
What it does
Adds a Rack middleware so that it can be used in generic Rack applications to trace the full request. This middleware can be used in applications such as Rails, so that calls are traced despite a Rails controller is used or not.
A secondary middleware will be added so that it's possible to keep track of the time spent in Rack middlewares before hitting the real application.