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

[rack] add Rack middleware to trace generic Rack applications #111

Merged
merged 11 commits into from
Apr 19, 2017

Conversation

palazzem
Copy link
Contributor

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.

@palazzem palazzem added the integrations Involves tracing integrations label Apr 13, 2017
@palazzem palazzem added this to the 0.7.0 milestone Apr 13, 2017
@palazzem palazzem requested a review from ufoot April 13, 2017 23:08
@palazzem palazzem force-pushed the palazzem/rack-instrumentation branch from 6f11857 to b8886da Compare April 13, 2017 23:26
Copy link
Contributor

@ufoot ufoot left a 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"
Copy link
Contributor

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.

Copy link
Contributor Author

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
Copy link
Contributor

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.

@palazzem palazzem force-pushed the palazzem/rack-instrumentation branch from b8886da to 86bb860 Compare April 14, 2017 10:13
@palazzem palazzem force-pushed the palazzem/rack-instrumentation branch from 86bb860 to 315548e Compare April 19, 2017 09:57
@palazzem palazzem changed the title [rack] add Rack middlewares to trace generic Rack applications [rack] add Rack middleware to trace generic Rack applications Apr 19, 2017
class TraceMiddleware
DEFAULT_CONFIG = {
tracer: Datadog.tracer,
default_service: 'rack'
Copy link
Contributor Author

@palazzem palazzem Apr 19, 2017

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.

Copy link
Contributor

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)
Copy link
Contributor Author

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

Copy link
Contributor

@ufoot ufoot left a 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'
Copy link
Contributor

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
Copy link
Contributor

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
Copy link
Contributor

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
Copy link
Contributor

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).

Copy link
Contributor Author

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
Copy link
Contributor

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)
Copy link
Contributor

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
Copy link
Contributor

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@palazzem palazzem force-pushed the palazzem/rack-instrumentation branch from 96397de to ea0d4c0 Compare April 19, 2017 12:22
@palazzem palazzem force-pushed the palazzem/rack-instrumentation branch from ea0d4c0 to ba6686b Compare April 19, 2017 12:31
Copy link
Contributor

@ufoot ufoot left a 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"))
Copy link
Contributor

Choose a reason for hiding this comment

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

Exactly.

Copy link
Contributor

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.

Copy link
Contributor Author

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)
Copy link
Contributor

Choose a reason for hiding this comment

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

👍

@palazzem palazzem merged commit 9b31a5b into master Apr 19, 2017
@palazzem palazzem deleted the palazzem/rack-instrumentation branch April 20, 2017 16:51

# call the rest of the stack
status, headers, response = @app.call(env)
rescue StandardError => e
Copy link
Contributor

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.

Copy link
Contributor Author

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')
Copy link
Contributor

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.

Copy link
Contributor Author

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!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
integrations Involves tracing integrations
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants