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

[rest_client] Integration #460

Merged
merged 41 commits into from
Aug 6, 2018
Merged

Conversation

pawelchcki
Copy link
Contributor

@pawelchcki pawelchcki commented Jun 13, 2018

Add integration for rest_client

@pawelchcki pawelchcki added integrations Involves tracing integrations do-not-merge/WIP Not ready for merge feature Involves a product feature labels Jun 13, 2018
@pawelchcki pawelchcki self-assigned this Jun 13, 2018
@pawelchcki pawelchcki force-pushed the feature/add_rest_client_integration branch from 1982467 to d91d409 Compare June 14, 2018 16:54
@delner delner added this to the 0.14.0 milestone Jun 15, 2018
@pawelchcki pawelchcki removed the do-not-merge/WIP Not ready for merge label Jun 18, 2018
@pawelchcki pawelchcki changed the base branch from master to 0.14-dev June 21, 2018 10:42
Copy link
Contributor

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

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.

Copy link
Contributor Author

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

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.

Copy link
Contributor

@delner delner Jun 26, 2018

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

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.

Copy link
Contributor Author

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

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.

Copy link
Contributor Author

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

Copy link
Contributor

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.

Copy link
Contributor Author

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.

Copy link
Contributor

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

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

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.

Copy link
Contributor Author

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.

Copy link
Contributor

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

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

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

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.

Copy link
Contributor Author

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

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

@delner delner mentioned this pull request Jun 27, 2018
@pawelchcki pawelchcki force-pushed the feature/add_rest_client_integration branch from ecda69b to af5ab36 Compare June 28, 2018 11:56
@pawelchcki pawelchcki removed their assignment Jun 28, 2018
Copy link
Contributor

@delner delner left a 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(
Copy link
Contributor

@delner delner Jun 28, 2018

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

Copy link
Contributor Author

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.

Copy link
Contributor

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.

c.use :rest_client, service_name: 'rest_client' # global service name
end

RestClient.get('http://example.com')
Copy link
Contributor

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.

Copy link
Contributor

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.

@pawelchcki pawelchcki force-pushed the feature/add_rest_client_integration branch from b94cb47 to bf344e6 Compare June 29, 2018 09:39
@pawelchcki pawelchcki force-pushed the feature/add_rest_client_integration branch from aec9cf3 to aea0a92 Compare June 29, 2018 18:58
@delner delner assigned pawelchcki and unassigned delner Jul 11, 2018
@pawelchcki pawelchcki force-pushed the feature/add_rest_client_integration branch from d84642f to 61a1d7d Compare July 13, 2018 21:04
@pawelchcki pawelchcki force-pushed the feature/add_rest_client_integration branch from 61a1d7d to 28bdcf7 Compare July 13, 2018 21:06
@pawelchcki
Copy link
Contributor Author

@delner This PR is ready for another batch of reviews

Copy link
Contributor

@delner delner left a 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.

@pawelchcki pawelchcki merged commit 2221f04 into 0.14-dev Aug 6, 2018
@pawelchcki pawelchcki deleted the feature/add_rest_client_integration branch August 6, 2018 15:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature Involves a product feature integrations Involves tracing integrations
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants