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

Add ability to specify service names by domain #882

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Rakefile
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ namespace :spec do

RSpec::Core::RakeTask.new(:contrib) do |t, args|
# rubocop:disable Metrics/LineLength
t.pattern = 'spec/**/contrib/{analytics,configurable,extensions,integration,patchable,patcher,registerable,registry,configuration/*}_spec.rb'
t.pattern = 'spec/**/contrib/{analytics,configurable,configuration,extensions,integration,patchable,patcher,registerable,registry/*}_spec.rb'
t.rspec_opts = args.to_a.join(' ')
end

Expand Down
26 changes: 26 additions & 0 deletions docs/GettingStarted.md
Original file line number Diff line number Diff line change
Expand Up @@ -647,6 +647,12 @@ require 'ddtrace'

Datadog.configure do |c|
c.use :ethon, options

# optionally, specify a different service name for hostnames matching a regex
c.use :ethon, describes: /user-[^.]+\.example\.com/ do |ethon|
ethon.service_name = 'user.example.com'
ethon.split_by_domain = false # Only necessary if split_by_domain is true by default
end
end
```

Expand All @@ -657,6 +663,7 @@ Where `options` is an optional `Hash` that accepts the following parameters:
| `analytics_enabled` | Enable analytics for spans produced by this integration. `true` for on, `nil` to defer to global setting, `false` for off. | `false` |
| `distributed_tracing` | Enables [distributed tracing](#distributed-tracing) | `true` |
| `service_name` | Service name for `ethon` instrumentation. | `'ethon'` |
| `split_by_domain` | Uses the request domain as the service name when set to `true`. | `false` |
| `tracer` | `Datadog::Tracer` used to perform instrumentation. Usually you don't need to set this. | `Datadog.tracer` |

### Excon
Expand All @@ -670,6 +677,12 @@ require 'ddtrace'
# Configure default Excon tracing behavior
Datadog.configure do |c|
c.use :excon, options

# optionally, specify a different service name for hostnames matching a regex
c.use :excon, describes: /user-[^.]+\.example\.com/ do |excon|
excon.service_name = 'user.example.com'
excon.split_by_domain = false # Only necessary if split_by_domain is true by default
end
end

connection = Excon.new('https://example.com')
Expand Down Expand Up @@ -723,6 +736,12 @@ require 'ddtrace'
# Configure default Faraday tracing behavior
Datadog.configure do |c|
c.use :faraday, options

# optionally, specify a different service name for hostnames matching a regex
c.use :faraday, describes: /user-[^.]+\.example\.com/ do |faraday|
faraday.service_name = 'user.example.com'
faraday.split_by_domain = false # Only necessary if split_by_domain is true by default
end
end

# Configure Faraday tracing behavior for single connection
Expand Down Expand Up @@ -944,6 +963,12 @@ require 'ddtrace'

Datadog.configure do |c|
c.use :http, options

# optionally, specify a different service name for hostnames matching a regex
c.use :http, describes: /user-[^.]+\.example\.com/ do |http|
http.service_name = 'user.example.com'
http.split_by_domain = false # Only necessary if split_by_domain is true by default
end
end

Net::HTTP.start('127.0.0.1', 8080) do |http|
Expand All @@ -961,6 +986,7 @@ Where `options` is an optional `Hash` that accepts the following parameters:
| `analytics_enabled` | Enable analytics for spans produced by this integration. `true` for on, `nil` to defer to global setting, `false` for off. | `false` |
| `distributed_tracing` | Enables [distributed tracing](#distributed-tracing) | `true` |
| `service_name` | Service name used for `http` instrumentation | `'net/http'` |
| `split_by_domain` | Uses the request domain as the service name when set to `true`. | `false` |
| `tracer` | `Datadog::Tracer` used to perform instrumentation. Usually you don't need to set this. | `Datadog.tracer` |

If you wish to configure each connection object individually, you may use the `Datadog.configure` as it follows:
Expand Down
9 changes: 5 additions & 4 deletions lib/ddtrace/contrib/configurable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,13 @@ def configurations
end
end

def configure(key, options = {}, &block)
key = resolver.resolve(key || :default)
def configure(key = :default, options = {}, &block)
resolver.add_key(key) unless key == :default
delner marked this conversation as resolved.
Show resolved Hide resolved
resolved_key = resolver.resolve(key)

configurations[key].tap do |settings|
configurations[resolved_key].tap do |settings|
settings.configure(options, &block)
configurations[key] = settings
configurations[resolved_key] = settings
end
end

Expand Down
4 changes: 4 additions & 0 deletions lib/ddtrace/contrib/configuration/resolver.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ module Contrib
module Configuration
# Resolves a value to a configuration key
class Resolver
def add_key(key)
Copy link
Contributor

Choose a reason for hiding this comment

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

I think adding a second method here was inevitable, so I'm on board with this.

# noop here, override in your subclass to customize
end

def resolve(name)
name
end
Expand Down
31 changes: 31 additions & 0 deletions lib/ddtrace/contrib/configuration/resolvers/regexp_resolver.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
require 'ddtrace/contrib/configuration/resolver'

module Datadog
module Contrib
module Configuration
# Resolves a value to a configuration key
module Resolvers
# Matches strings against Regexps.
class RegexpResolver < Datadog::Contrib::Configuration::Resolver
def add_key(pattern)
patterns << pattern.is_a?(Regexp) ? pattern : /#{Regexp.quote(pattern)}/
Copy link
Contributor

Choose a reason for hiding this comment

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

Although its kind of nice to convert everything to a Regex for consistency I think there's a couple of problems with interpolating strings to Regexp:

  1. String values might match inadvertently, e.g. passing 'a' would match 'a' and 'aa'.
  2. There's an added expense to evaluating Regexp when doing == with a string might suffice.

I would recommend accepting any object, then in #resolve do a case statement such that if the pattern is a Regexp, do =~, otherwise coerce the pattern to a string and do ==.

end

def resolve(name)
return :default if name == :default

name = name.to_s
matching_pattern = patterns.find { |p| p =~ name }
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe this isn't a big deal, but should we change name to name.to_s? Coercion might prevent some type incompatibility issues such as with URI, etc.

matching_pattern || :default
end

private

def patterns
@patterns ||= Set.new
end
end
end
end
end
end
1 change: 1 addition & 0 deletions lib/ddtrace/contrib/ethon/configuration/settings.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ class Settings < Contrib::Configuration::Settings

option :distributed_tracing, default: true
option :service_name, default: Ext::SERVICE_NAME
option :split_by_domain, default: false
end
end
end
Expand Down
35 changes: 22 additions & 13 deletions lib/ddtrace/contrib/ethon/easy_patch.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
require 'ddtrace/ext/distributed'
require 'ddtrace/propagation/http_propagator'
require 'ddtrace/contrib/ethon/ext'
require 'ddtrace/contrib/http_annotation_helper'

module Datadog
module Contrib
Expand All @@ -14,7 +15,10 @@ def self.included(base)

# InstanceMethods - implementing instrumentation
module InstanceMethods
include Datadog::Contrib::HttpAnnotationHelper

def http_request(url, action_name, options = {})
load_datadog_configuration_for(url)
return super unless tracer_enabled?

# It's tricky to get HTTP method from libcurl
Expand All @@ -23,14 +27,13 @@ def http_request(url, action_name, options = {})
end

def headers=(headers)
return super unless tracer_enabled?
Copy link
Author

Choose a reason for hiding this comment

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

I had to remove the tracer_enabled? call here and in reset because, since we added the resolver pattern, you now have to have a hostname in order to determine the configuration (to decide if the tracer is enabled. theoretically you can now disable the tracer for requests matching /do-not-want.io/)... but you can set headers before supplying a URL to net/http.

This will have the side effect of creating a few DD instance variables on the net/http object even if the tracer is disabled.

Copy link
Contributor

Choose a reason for hiding this comment

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

Not sure I understand entirely; would it make sense to derive enabled? from the default config if the hostname isn't available? It might mean that not all settings are strictly respected per host, but maybe that's okay? Thoughts are most welcome.

Copy link
Author

Choose a reason for hiding this comment

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

My thought was that it's better to respect all settings per host and leave a few extra ivars on the request object rather than have a surprising situation where you disabled the tracer for certain hosts but they are still getting traced (which would appear to the casual observer to be a bug).

I can certainly derive enabled? from the default config if there's no hostname if you feel like the tradeoff isn't worth it.


# Store headers to call this method again when span is ready
@datadog_original_headers = headers
super
end

def perform
load_datadog_configuration_for(url)
return super unless tracer_enabled?
datadog_before_request
super
Expand Down Expand Up @@ -61,17 +64,17 @@ def complete
def reset
super
ensure
if tracer_enabled?
@datadog_span = nil
@datadog_method = nil
@datadog_original_headers = nil
end
@datadog_span = nil
@datadog_method = nil
@datadog_original_headers = nil
@datadog_configuration = nil
end

def datadog_before_request(parent_span: nil)
load_datadog_configuration_for(url)
@datadog_span = datadog_configuration[:tracer].trace(
Ext::SPAN_REQUEST,
service: datadog_configuration[:service_name],
service: uri ? service_name(uri.host, datadog_configuration) : datadog_configuration[:service_name],
span_type: Datadog::Ext::HTTP::TYPE_OUTBOUND
)
@datadog_span.parent = parent_span unless parent_span.nil?
Expand All @@ -91,9 +94,10 @@ def datadog_span_started?

private

attr_reader :datadog_configuration

def datadog_tag_request
span = @datadog_span
uri = URI.parse(url)
method = 'N/A'
if instance_variable_defined?(:@datadog_method) && !@datadog_method.nil?
method = @datadog_method.to_s
Expand All @@ -103,12 +107,11 @@ def datadog_tag_request
# Set analytics sample rate
Contrib::Analytics.set_sample_rate(span, analytics_sample_rate) if analytics_enabled?

return unless uri
span.set_tag(Datadog::Ext::HTTP::URL, uri.path)
span.set_tag(Datadog::Ext::HTTP::METHOD, method)
span.set_tag(Datadog::Ext::NET::TARGET_HOST, uri.host)
span.set_tag(Datadog::Ext::NET::TARGET_PORT, uri.port)
rescue URI::InvalidURIError
return
end

def set_span_error_message(message)
Expand All @@ -117,8 +120,14 @@ def set_span_error_message(message)
@datadog_span.set_tag(Datadog::Ext::Errors::MSG, message)
end

def datadog_configuration
Datadog.configuration[:ethon]
def uri
URI.parse(url)
# rubocop:disable Lint/HandleExceptions
rescue URI::InvalidURIError
end

def load_datadog_configuration_for(host = :default)
@datadog_configuration = Datadog.configuration[:ethon, host]
end

def tracer_enabled?
Expand Down
5 changes: 5 additions & 0 deletions lib/ddtrace/contrib/ethon/integration.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
require 'ddtrace/contrib/integration'
require 'ddtrace/contrib/ethon/configuration/settings'
require 'ddtrace/contrib/configuration/resolvers/regexp_resolver'
require 'ddtrace/contrib/ethon/patcher'

module Datadog
Expand All @@ -25,6 +26,10 @@ def default_configuration
def patcher
Patcher
end

def resolver
@resolver ||= Contrib::Configuration::Resolvers::RegexpResolver.new
end
end
end
end
Expand Down
5 changes: 5 additions & 0 deletions lib/ddtrace/contrib/excon/integration.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
require 'ddtrace/contrib/integration'
require 'ddtrace/contrib/excon/configuration/settings'
require 'ddtrace/contrib/configuration/resolvers/regexp_resolver'
require 'ddtrace/contrib/excon/patcher'

module Datadog
Expand All @@ -26,6 +27,10 @@ def default_configuration
def patcher
Patcher
end

def resolver
Copy link
Contributor

Choose a reason for hiding this comment

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

Not important right now, but maybe we could extract this behavior to some kind of HTTP module that can be recomposed into each integration that uses it. I don't think we have to do that right now though, as we probably should wait to get more behavior worth extracting first.

@resolver ||= Contrib::Configuration::Resolvers::RegexpResolver.new
end
end
end
end
Expand Down
21 changes: 12 additions & 9 deletions lib/ddtrace/contrib/excon/middleware.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,28 @@
require 'ddtrace/propagation/http_propagator'
require 'ddtrace/contrib/analytics'
require 'ddtrace/contrib/excon/ext'
require 'ddtrace/contrib/http_annotation_helper'

module Datadog
module Contrib
module Excon
# Middleware implements an excon-middleware for ddtrace instrumentation
class Middleware < ::Excon::Middleware::Base
include Datadog::Contrib::HttpAnnotationHelper

DEFAULT_ERROR_HANDLER = lambda do |response|
Datadog::Ext::HTTP::ERROR_RANGE.cover?(response[:status])
end

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

def request_call(datum)
begin
unless datum.key?(:datadog_span)
@options = build_request_options!(datum)
tracer.trace(Ext::SPAN_REQUEST).tap do |span|
datum[:datadog_span] = span
annotate!(span, datum)
Expand Down Expand Up @@ -99,13 +103,9 @@ 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.service = service_name(datum[:host], @options)
span.span_type = Datadog::Ext::HTTP::TYPE_OUTBOUND

# Set analytics sample rate
Expand Down Expand Up @@ -144,9 +144,12 @@ 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]
def build_request_options!(datum)
datadog_configuration(datum[:host]).options_hash.merge(@default_options)
end

def datadog_configuration(host = :default)
Datadog.configuration[:excon, host]
end
end
end
Expand Down
5 changes: 5 additions & 0 deletions lib/ddtrace/contrib/faraday/integration.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
require 'ddtrace/contrib/integration'
require 'ddtrace/contrib/configuration/resolvers/regexp_resolver'
require 'ddtrace/contrib/faraday/configuration/settings'
require 'ddtrace/contrib/faraday/patcher'

Expand Down Expand Up @@ -30,6 +31,10 @@ def default_configuration
def patcher
Patcher
end

def resolver
@resolver ||= Contrib::Configuration::Resolvers::RegexpResolver.new
end
end
end
end
Expand Down
Loading