Skip to content

Commit

Permalink
Merge pull request #211 from walterking/excon_instrumentation
Browse files Browse the repository at this point in the history
Add Excon instrumentation
  • Loading branch information
delner committed May 8, 2018
2 parents f30db7f + d782131 commit 589f9ff
Show file tree
Hide file tree
Showing 9 changed files with 496 additions and 6 deletions.
2 changes: 2 additions & 0 deletions Appraisals
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ if RUBY_VERSION >= '2.2.2' && RUBY_PLATFORM != 'java'
appraise 'contrib' do
gem 'active_model_serializers', '>= 0.10.0'
gem 'elasticsearch-transport'
gem 'excon'
gem 'mongo', '< 2.5'
gem 'graphql'
gem 'grape'
Expand All @@ -139,6 +140,7 @@ else
appraise 'contrib-old' do
gem 'active_model_serializers', '~> 0.9.0'
gem 'elasticsearch-transport'
gem 'excon'
gem 'mongo', '< 2.5'
gem 'redis', '< 4.0'
gem 'hiredis'
Expand Down
3 changes: 3 additions & 0 deletions Rakefile
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ namespace :spec do
:aws,
:dalli,
:elasticsearch,
:excon,
:faraday,
:grape,
:graphql,
Expand Down Expand Up @@ -225,6 +226,7 @@ task :ci do
sh 'rvm $MRI_VERSIONS --verbose do appraisal contrib rake spec:active_record'
sh 'rvm $MRI_VERSIONS --verbose do appraisal contrib rake spec:active_support'
sh 'rvm $MRI_VERSIONS --verbose do appraisal contrib rake spec:dalli'
sh 'rvm $MRI_VERSIONS --verbose do appraisal contrib rake spec:excon'
sh 'rvm $MRI_VERSIONS --verbose do appraisal contrib rake spec:faraday'
sh 'rvm $MRI_VERSIONS --verbose do appraisal contrib rake spec:graphql'
sh 'rvm $MRI_VERSIONS --verbose do appraisal contrib rake spec:mongodb'
Expand All @@ -237,6 +239,7 @@ task :ci do
sh 'rvm $MRI_OLD_VERSIONS --verbose do appraisal contrib-old rake spec:active_record'
sh 'rvm $MRI_OLD_VERSIONS --verbose do appraisal contrib-old rake spec:active_support'
sh 'rvm $MRI_OLD_VERSIONS --verbose do appraisal contrib-old rake spec:dalli'
sh 'rvm $MRI_OLD_VERSIONS --verbose do appraisal contrib-old rake spec:excon'
sh 'rvm $MRI_OLD_VERSIONS --verbose do appraisal contrib-old rake spec:faraday'
sh 'rvm $MRI_OLD_VERSIONS --verbose do appraisal contrib-old rake spec:mongodb'
sh 'rvm $MRI_OLD_VERSIONS --verbose do appraisal contrib-old rake spec:redis'
Expand Down
68 changes: 62 additions & 6 deletions docs/GettingStarted.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ For descriptions of terminology used in APM, take a look at the [official docume
- [AWS](#aws)
- [Dalli](#dalli)
- [Elastic Search](#elastic-search)
- [Excon](#excon)
- [Faraday](#faraday)
- [gRPC](#grpc)
- [Grape](#grape)
Expand Down Expand Up @@ -246,6 +247,7 @@ For a list of available integrations, and their configuration options, please re
| AWS | `aws` | `>= 2.0` | *[Link](#aws)* | *[Link](https://github.com/aws/aws-sdk-ruby)* |
| Dalli | `dalli` | `>= 2.7` | *[Link](#dalli)* | *[Link](https://github.com/petergoldstein/dalli)* |
| Elastic Search | `elasticsearch` | `>= 6.0` | *[Link](#elastic-search)* | *[Link](https://github.com/elastic/elasticsearch-ruby)* |
| Excon | `excon` | `>= 0.62` | *[Link](#excon)* | *[Link](https://github.com/excon/excon)* |
| Faraday | `faraday` | `>= 0.14` | *[Link](#faraday)* | *[Link](https://github.com/lostisland/faraday)* |
| gRPC | `grpc` | `>= 1.10` | *[Link](#grpc)* | *[Link](https://github.com/grpc/grpc/tree/master/src/rubyc)* |
| Grape | `grape` | `>= 1.0` | *[Link](#grape)* | *[Link](https://github.com/ruby-grape/grape)* |
Expand Down Expand Up @@ -357,6 +359,58 @@ Where `options` is an optional `Hash` that accepts the following parameters:
| ``service_name`` | Service name used for `elasticsearch` instrumentation | elasticsearch |
| ``quantize`` | Hash containing options for quantization. May include `:show` with an Array of keys to not quantize (or `:all` to skip quantization), or `:exclude` with Array of keys to exclude entirely. | {} |
### Excon
The `excon` integration is available through the `ddtrace` middleware:
```ruby
require 'excon'
require 'ddtrace'
# Configure default Excon tracing behavior
Datadog.configure do |c|
c.use :excon, service_name: 'excon'
end
connection = Excon.new('https://example.com')
connection.get
```
Where `options` is an optional `Hash` that accepts the following parameters:
| Key | Description | Default |
| --- | --- | --- |
| `service_name` | Service name for Excon instrumentation. When provided to middleware for a specific connection, it applies only to that connection object. | `'excon'` |
| `split_by_domain` | Uses the request domain as the service name when set to `true`. | `false` |
| `distributed_tracing` | Enables [distributed tracing](#distributed-tracing) | `false` |
| `error_handler` | A `Proc` that accepts a `response` parameter. If it evaluates to a *truthy* value, the trace span is marked as an error. By default only sets 5XX responses as errors. | `nil` |
| `tracer` | A `Datadog::Tracer` instance used to instrument the application. Usually you don't need to set that. | `Datadog.tracer` |

**Configuring connections to use different settings**

If you use multiple connections with Excon, you can give each of them different settings by configuring their constructors with middleware:

```ruby
# Wrap the Datadog tracing middleware around the default middleware stack
Excon.new(
'http://example.com',
middlewares: Datadog::Contrib::Excon::Middleware.with(options).around_default_stack
)
# Insert the middleware into a custom middleware stack.
# NOTE: Trace middleware must be inserted after ResponseParser!
Excon.new(
'http://example.com',
middlewares: [
Excon::Middleware::ResponseParser,
Datadog::Contrib::Excon::Middleware.with(options),
Excon::Middleware::Idempotent
]
)
```

Where `options` is a Hash that contains any of the parameters listed in the table above.

### Faraday

The `faraday` integration is available through the `ddtrace` middleware:
Expand All @@ -379,12 +433,13 @@ connection.get('/foo')

Where `options` is an optional `Hash` that accepts the following parameters:

| Key | Default | Description |
| Key | Description | Default |
| --- | --- | --- |
| `service_name` | Global service name (default: `faraday`) | Service name for this specific connection object. |
| `split_by_domain` | `false` | Uses the request domain as the service name when set to `true`. |
| `distributed_tracing` | `false` | Propagates tracing context along the HTTP request when set to `true`. |
| `error_handler` | ``5xx`` evaluated as errors | A callable object that receives a single argument – the request environment. If it evaluates to a *truthy* value, the trace span is marked as an error. |
| `service_name` | Service name for Faraday instrumentation. When provided to middleware for a specific connection, it applies only to that connection object. | `'faraday'` |
| `split_by_domain` | Uses the request domain as the service name when set to `true`. | `false` |
| `distributed_tracing` | Enables [distributed tracing](#distributed-tracing) | `false` |
| `error_handler` | A `Proc` that accepts a `response` parameter. If it evaluates to a *truthy* value, the trace span is marked as an error. By default only sets 5XX responses as errors. | ``5xx`` evaluated as errors |
| `tracer` | A `Datadog::Tracer` instance used to instrument the application. Usually you don't need to set that. | `Datadog.tracer` |
### gRPC
Expand Down Expand Up @@ -570,7 +625,7 @@ Where `options` is an optional `Hash` that accepts the following parameters:
| Key | Description | Default |
| --- | --- | --- |
| ``service_name`` | Service name used for `http` instrumentation | http |
| ``distributed_tracing`` | Enables distributed tracing | ``false`` |
| ``distributed_tracing`` | Enables [distributed tracing](#distributed-tracing) | ``false`` |
If you wish to configure each connection object individually, you may use the ``Datadog.configure`` as it follows:
Expand Down Expand Up @@ -1101,6 +1156,7 @@ Many integrations included in `ddtrace` support distributed tracing. Distributed
For more details on how to activate distributed tracing for integrations, see their documentation:
- [Excon](#excon)
- [Faraday](#faraday)
- [Net/HTTP](#nethttp)
- [Rack](#rack)
Expand Down
1 change: 1 addition & 0 deletions gemfiles/contrib.gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ source "https://rubygems.org"
gem "pry-nav", git: "https://github.com/nixme/pry-nav.git", branch: "master"
gem "active_model_serializers", ">= 0.10.0"
gem "elasticsearch-transport"
gem "excon"
gem "mongo", "< 2.5"
gem "graphql"
gem "grape"
Expand Down
1 change: 1 addition & 0 deletions gemfiles/contrib_old.gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ source "https://rubygems.org"
gem "pry-nav", git: "https://github.com/nixme/pry-nav.git", branch: "master"
gem "active_model_serializers", "~> 0.9.0"
gem "elasticsearch-transport"
gem "excon"
gem "mongo", "< 2.5"
gem "redis", "< 4.0"
gem "hiredis"
Expand Down
1 change: 1 addition & 0 deletions lib/ddtrace.rb
Original file line number Diff line number Diff line change
Expand Up @@ -74,4 +74,5 @@ def configure(target = configuration, opts = {})
require 'ddtrace/contrib/resque/patcher'
require 'ddtrace/contrib/racecar/patcher'
require 'ddtrace/contrib/sidekiq/patcher'
require 'ddtrace/contrib/excon/patcher'
require 'ddtrace/monkey'
140 changes: 140 additions & 0 deletions lib/ddtrace/contrib/excon/middleware.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
require 'excon'
require 'ddtrace/ext/http'
require 'ddtrace/ext/net'
require 'ddtrace/ext/distributed'
require 'ddtrace/propagation/http_propagator'

module Datadog
module Contrib
module Excon
# Middleware implements an excon-middleware for ddtrace instrumentation
class Middleware < ::Excon::Middleware::Base
SPAN_NAME = 'excon.request'.freeze
DEFAULT_ERROR_HANDLER = lambda do |response|
Ext::HTTP::ERROR_RANGE.cover?(response[:status])
end

def initialize(stack, options = {})
super(stack)
@options = Datadog.configuration[:excon].merge(options)
end

def request_call(datum)
begin
unless datum.key?(:datadog_span)
tracer.trace(SPAN_NAME).tap do |span|
datum[:datadog_span] = span
annotate!(span, datum)
propagate!(span, datum) if distributed_tracing?
end
end
rescue StandardError => e
Datadog::Tracer.log.debug(e.message)
end

@stack.request_call(datum)
end

def response_call(datum)
@stack.response_call(datum).tap do |d|
handle_response(d)
end
end

def error_call(datum)
@stack.error_call(datum).tap do |d|
handle_response(d)
end
end

# Returns a child class of this trace middleware
# With options given as defaults.
def self.with(options = {})
Class.new(self) do
@options = options

# rubocop:disable Style/TrivialAccessors
def self.options
@options
end

def initialize(stack)
super(stack, self.class.options)
end
end
end

# Returns a copy of the default stack with the trace middleware injected
def self.around_default_stack
::Excon.defaults[:middlewares].dup.tap do |default_stack|
# If the default stack contains a version of the trace middleware already...
existing_trace_middleware = default_stack.find { |m| m <= Middleware }
default_stack.delete(existing_trace_middleware) if existing_trace_middleware

# Inject after the ResponseParser middleware
response_middleware_index = default_stack.index(::Excon::Middleware::ResponseParser).to_i
default_stack.insert(response_middleware_index + 1, self)
end
end

private

def tracer
@options[:tracer]
end

def distributed_tracing?
@options[:distributed_tracing] == true && tracer.enabled
end

def error_handler
@options[:error_handler] || DEFAULT_ERROR_HANDLER
end

def split_by_domain?
@options[:split_by_domain] == true
end

def annotate!(span, datum)
span.resource = datum[:method].to_s.upcase
span.service = service_name(datum)
span.span_type = Ext::HTTP::TYPE
span.set_tag(Ext::HTTP::URL, datum[:path])
span.set_tag(Ext::HTTP::METHOD, datum[:method].to_s.upcase)
span.set_tag(Ext::NET::TARGET_HOST, datum[:host])
span.set_tag(Ext::NET::TARGET_PORT, datum[:port].to_s)
end

def handle_response(datum)
if datum.key?(:datadog_span)
datum[:datadog_span].tap do |span|
return span if span.finished?

if datum.key?(:response)
response = datum[:response]
if error_handler.call(response)
span.set_error(["Error #{response[:status]}", response[:body]])
end
span.set_tag(Ext::HTTP::STATUS_CODE, response[:status])
end
span.set_error(datum[:error]) if datum.key?(:error)
span.finish
datum.delete(:datadog_span)
end
end
rescue StandardError => e
Datadog::Tracer.log.debug(e.message)
end

def propagate!(span, datum)
Datadog::HTTPPropagator.inject!(span.context, datum[:headers])
end

def service_name(datum)
# TODO: Change this to implement more sensible multiplexing
split_by_domain? ? datum[:host] : @options[:service_name]
end
end
end
end
end
50 changes: 50 additions & 0 deletions lib/ddtrace/contrib/excon/patcher.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
require 'ddtrace/ext/app_types'

module Datadog
module Contrib
module Excon
# Responsible for hooking the instrumentation into Excon
module Patcher
include Base

DEFAULT_SERVICE = 'excon'.freeze

register_as :excon
option :tracer, default: Datadog.tracer
option :service_name, default: DEFAULT_SERVICE
option :distributed_tracing, default: false
option :split_by_domain, default: false
option :error_handler, default: nil

@patched = false

module_function

def patch
return @patched if patched? || !compatible?

require 'ddtrace/contrib/excon/middleware'

add_middleware

@patched = true
rescue => e
Tracer.log.error("Unable to apply Excon integration: #{e}")
@patched
end

def patched?
@patched
end

def compatible?
defined?(::Excon)
end

def add_middleware
::Excon.defaults[:middlewares] = Middleware.around_default_stack
end
end
end
end
end
Loading

0 comments on commit 589f9ff

Please sign in to comment.