-
Notifications
You must be signed in to change notification settings - Fork 369
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
[rest_client] Integration #460
Conversation
1982467
to
d91d409
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.
Good start to this.
Some questions about tests, Pins, compatibility, and simplifying the trace code a bit (if possible.)
Also, there wasn't any place to add a comment, but we need to add this to the documentation as well.
This also needs to be rebased to address merge conflicts.
@@ -56,7 +56,8 @@ namespace :spec do | |||
:resque, | |||
:sidekiq, | |||
:sinatra, | |||
:sucker_punch | |||
:sucker_punch, | |||
:rest_client |
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.
We added the Rake task here, but we aren't actually running rest_client
tests in CI. We should add calls to this task in the ci
task below.
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.
Oh! Forgot about that. Thanks for pointing it out!
end | ||
|
||
def add_pin(klass) | ||
Pin.new(get_option(:service_name), app: NAME, app_type: Ext::AppTypes::WEB).onto(klass) |
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.
Interesting... why put the Pin
on the class, when it could be on the object?
If we put it on the object, would it make it possible for users to configure an individual request to be traced differently? e.g. with a different service_name
? Usually this kind of feature is in great demand, so we might want to make sure it's supported.
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.
Follow-up to this: I see you defined #datadog_pin
in the instance methods for ::RestClient::Request
. The pin on the instance should support requests to be individually configured. But why is this pin then added to the class? Is this not redundant?
REQUEST_TRACE_NAME = 'rest_client.request'.freeze | ||
|
||
def self.included(base) | ||
base.prepend(InstanceMethods) |
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.
What version of Ruby is supported? prepend
is not available in 1.9.3, and this will fail for 2.0 because it's a call to a private method. If we plan on supporting 2.0+, we should change this to send(:prepend, InstanceMethods)
.
We should also add some kind of compatibility check before we reach this point, presumably in the patcher class.
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.
Added the shim to support ruby 1.9
end | ||
|
||
def datadog_trace_request | ||
span = datadog_pin.tracer.trace(REQUEST_TRACE_NAME, service: datadog_pin.service_name) |
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.
Why do we use this asynchronous form instead of block form? The block form should cover most of what you're doing here implicitly, and permit you to do the customizations you require.
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.
Indeed block form would have been nicer, however the problem is with
rescue ::RestClient::ExceptionWithResponse => e
...
raise e
Since in block form this will be traced as error regardless of the http code.
I.e. RestClient throws this exception on every http code other than 1xx-2xx
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.
Could you rescue that error inside the block form, the set the error or re-raise if necessary? We do something like this in Rails. It's not exactly the same, but maybe there's some inspiration to draw from 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.
I need to re-raise this error not to break any one use of rest-client.
We could modify tracer.trace do ... end
to allow it to somehow skip span.set_error(e)
step.
But so far I don't have a good idea how to approach this.
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.
Right, that makes sense. Maybe we should consider that as a new feature to add in the future: some kind of configurable error handling block. Something like:
class MyHttpClient
def get(url)
Datadog.tracer.trace('my.request', on_error: method(:handle_http_error)) do |span|
# Do some work, raise an HttpResponseError
end
end
def handle_http_error(span, error)
# Set errors is you wish, or do any custom logging, etc...
span.set_error(error) if Ext::HTTP::ERROR_RANGE.cover?(error.http_code)
end
end
We can consider that another time though. In this PR, maybe let's stick to what you have.
span.set_tag(Ext::HTTP::STATUS_CODE, response.code) | ||
response | ||
rescue ::RestClient::ExceptionWithResponse => e | ||
span.set_error(e) if Ext::HTTP::ERROR_RANGE.cover?(e.http_code) |
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.
This is thoughtful. 👍
end | ||
|
||
WebMock.disable_net_connect! | ||
WebMock.enable! |
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.
Is WebMock necessary? Surely we don't want it to make any outgoing HTTP requests, but is there some mock we can put in at the RSpec level to negate the need to depend on this? If not, it's okay, and we can keep it: just wondering if there was a way of keeping this simpler.
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.
We would have to mock quite a few internals in rest_client
to do so.
Webmock allows us to treat those as a black box, and write more integration style tests.
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.
Hmmm, I prefer unit tests where we're clear about the inputs/outputs (makes it easier to break things apart), however, I can see the value of an integration test here. This is probably okay.
subject(:span) { tracer.writer.spans.first } | ||
|
||
context 'response is successfull' do | ||
before do |
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.
Minor non-blocking, but short, single line blocks like this could be reduced to the more compact before { request }
form.
end | ||
end | ||
|
||
it_behaves_like 'instrumented request' |
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 like the use of shared examples. 👍
it 'propagates the headers' do | ||
request | ||
|
||
span = tracer.writer.spans.first |
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 could bump this span =
to a lazy let(:span)
block, which would not be evaluated until you reference it, allowing you to reduce repetition.
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.
done
service = datadog_configuration[:service_name] | ||
tracer = datadog_configuration[:tracer] | ||
|
||
Datadog::Pin.new(service, app: Patcher::NAME, app_type: Datadog::Ext::AppTypes::WEB, tracer: tracer) |
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.
Minor non-blocking style thing, but this could benefit by breaking into multiple lines e.g.:
Datadog::Pin.new(
#...
)
…ient_integration # Conflicts: # Appraisals # Rakefile # gemfiles/contrib.gemfile
ecda69b
to
af5ab36
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.
There's a possible memory issue in here regarding Pins, and we might want to change our strategy accordingly.
service = datadog_configuration[:service_name] | ||
tracer = datadog_configuration[:tracer] | ||
|
||
Datadog::Pin.new( |
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.
Thinking harder on this, initializing a Datadog::Pin
for every request might be a bad idea, since it might end up allocating memory that gets tossed out anyways, but at scale could be problematic. That considered, we do need to allow a Pin
to be added and configured, such that a user could override the behavior for a specific request, if they so chose.
I think maybe we need to change the strategy a little here: above where we use these configuration values from thePin
, I think we should decompose and encapsulate each of those options, to use the Pin
if it exists, or otherwise to fallback to the global configuration (only if the Pin
does not exist, not if the Pin
is simply missing a value.)
This might look something like:
def datadog_trace_request
datadog_configuration(:tracer).trace(REQUEST_TRACE_NAME) do
# Do some work...
end
end
def datadog_configuration(key)
respond_to?(:datadog_pin) ? datadog_pin.send(key) : Datadog.configuration[:rest_client][key]
end
# Do not define #datadog_pin.
# If user does `Pin.new(service_name: 'custom-service').onto(request)`
# then the pin will become available. Otherwise we don't want to create a Pin.
# `Datadog.configure(request, service_name: 'custom-service')` will probably not work,
# because it will never add a pin to an object: only configure an existing one.
# See https://github.com/DataDog/dd-trace-rb/blob/master/lib/ddtrace/pin.rb
# https://github.com/DataDog/dd-trace-rb/blob/master/lib/ddtrace/configuration/pin_setup.rb
# https://github.com/DataDog/dd-trace-rb/blob/master/lib/ddtrace.rb
# def datadog_pin
# nil
# 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.
Lets work on this optimization in separate PR's as there are a few approaches around Pin
Like PinSetup
and Datadog.configure
and it would be good to unify them in the future.
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're right, we probably shouldn't modify the Pin
or PinSetup
classes in this PR. However, I don't think creating pins here, even temporarily, is a satisfactory solution, given the possible performance impact.
So I would suggest we drop pin support for now altogether, and use global configuration only instead, until we can get the groundwork in for a satisfactory multiplexing solution in place: either by having optimized the use of Pins, or by leveraging the new integration configuration to drive multiplexing instead.
docs/GettingStarted.md
Outdated
c.use :rest_client, service_name: 'rest_client' # global service name | ||
end | ||
|
||
RestClient.get('http://example.com') |
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.
We should add an example of how to reconfigure a Request to be traced differently, using its own settings instead of global ones. This implementation will depend on how our configuration strategy works for this integration. See my other comment for why that might still change.
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.
Also, not here, but we should add test examples that demonstrate such configuration works independently of global configuration, without conflict.
b94cb47
to
bf344e6
Compare
aec9cf3
to
aea0a92
Compare
d84642f
to
61a1d7d
Compare
61a1d7d
to
28bdcf7
Compare
@delner This PR is ready for another batch of reviews |
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.
These changes look good. Thanks for you hard work on this!
Might I suggest either rebasing or squashing these commits though as they merge? There are 41 commits, many of which are quite small, which pollutes commit history a bit. Would be nice to clean these up a bit.
Add integration for rest_client