diff --git a/CHANGELOG.md b/CHANGELOG.md index c844a92e63f..cd43bf66400 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,17 @@ ## [Unreleased (beta)] +## [0.16.0] - 2018-09-18 + +Release notes: https://github.com/DataDog/dd-trace-rb/releases/tag/v0.16.0 + +Git diff: https://github.com/DataDog/dd-trace-rb/compare/v0.15.0...v0.16.0 + +### Added + +- OpenTracing support (#517) +- `middleware` option for disabling Rails trace middleware. (#552) + ## [0.15.0] - 2018-09-12 Release notes: https://github.com/DataDog/dd-trace-rb/releases/tag/v0.15.0 @@ -500,8 +511,9 @@ Release notes: https://github.com/DataDog/dd-trace-rb/releases/tag/v0.3.1 Git diff: https://github.com/DataDog/dd-trace-rb/compare/v0.3.0...v0.3.1 -[Unreleased (stable)]: https://github.com/DataDog/dd-trace-rb/compare/v0.15.0...master -[Unreleased (beta)]: https://github.com/DataDog/dd-trace-rb/compare/v0.15.0...0.16-dev +[Unreleased (stable)]: https://github.com/DataDog/dd-trace-rb/compare/v0.16.0...master +[Unreleased (beta)]: https://github.com/DataDog/dd-trace-rb/compare/v0.16.0...0.17-dev +[0.15.0]: https://github.com/DataDog/dd-trace-rb/compare/v0.15.0...v0.16.0 [0.15.0]: https://github.com/DataDog/dd-trace-rb/compare/v0.14.2...v0.15.0 [0.14.2]: https://github.com/DataDog/dd-trace-rb/compare/v0.14.1...v0.14.2 [0.14.1]: https://github.com/DataDog/dd-trace-rb/compare/v0.14.0...v0.14.1 diff --git a/Rakefile b/Rakefile index 28101461e54..5aefdb7f474 100644 --- a/Rakefile +++ b/Rakefile @@ -15,7 +15,11 @@ namespace :spec do RSpec::Core::RakeTask.new(:main) do |t| t.pattern = 'spec/**/*_spec.rb' - t.exclude_pattern = 'spec/**/{contrib,benchmark,redis}/**/*_spec.rb' + t.exclude_pattern = 'spec/**/{contrib,benchmark,redis,opentracer}/**/*_spec.rb' + end + + RSpec::Core::RakeTask.new(:opentracer) do |t| + t.pattern = 'spec/ddtrace/opentracer/**/*_spec.rb' end RSpec::Core::RakeTask.new(:rails) do |t| @@ -311,6 +315,7 @@ task :ci do sh 'bundle exec rake test:main' sh 'bundle exec rake spec:main' sh 'bundle exec rake spec:contrib' + sh 'bundle exec rake spec:opentracer' if RUBY_PLATFORM != 'java' # Contrib minitests @@ -366,6 +371,7 @@ task :ci do sh 'bundle exec rake test:main' sh 'bundle exec rake spec:main' sh 'bundle exec rake spec:contrib' + sh 'bundle exec rake spec:opentracer' if RUBY_PLATFORM != 'java' # Contrib minitests @@ -432,6 +438,7 @@ task :ci do sh 'bundle exec rake test:main' sh 'bundle exec rake spec:main' sh 'bundle exec rake spec:contrib' + sh 'bundle exec rake spec:opentracer' if RUBY_PLATFORM != 'java' # Contrib minitests @@ -497,6 +504,7 @@ task :ci do sh 'bundle exec rake test:main' sh 'bundle exec rake spec:main' sh 'bundle exec rake spec:contrib' + sh 'bundle exec rake spec:opentracer' if RUBY_PLATFORM != 'java' # Contrib minitests diff --git a/ddtrace.gemspec b/ddtrace.gemspec index c8683e571c0..8c46ece92dc 100644 --- a/ddtrace.gemspec +++ b/ddtrace.gemspec @@ -33,6 +33,8 @@ Gem::Specification.new do |spec| spec.require_paths = ['lib'] spec.add_dependency 'msgpack' + # TODO: Move this to Appraisals? + spec.add_dependency 'opentracing', '>= 0.4.1' spec.add_development_dependency 'rake', '>= 10.5' spec.add_development_dependency 'rubocop', '= 0.49.1' if RUBY_VERSION >= '2.1.0' diff --git a/docs/GettingStarted.md b/docs/GettingStarted.md index 82fb069f149..c0efa608e14 100644 --- a/docs/GettingStarted.md +++ b/docs/GettingStarted.md @@ -21,6 +21,7 @@ For descriptions of terminology used in APM, take a look at the [official docume - [Installation](#installation) - [Quickstart for Rails applications](#quickstart-for-rails-applications) - [Quickstart for Ruby applications](#quickstart-for-ruby-applications) + - [Quickstart for OpenTracing](#quickstart-for-opentracing) - [Manual instrumentation](#manual-instrumentation) - [Integration instrumentation](#integration-instrumentation) - [Active Record](#active-record) @@ -59,6 +60,7 @@ For descriptions of terminology used in APM, take a look at the [official docume - [Processing pipeline](#processing-pipeline) - [Filtering](#filtering) - [Processing](#processing) + - [OpenTracing](#opentracing) ## Compatibility @@ -75,10 +77,6 @@ For descriptions of terminology used in APM, take a look at the [official docume | | | 2.4 | Full | | JRuby | http://jruby.org/ | 9.1.5 | Experimental | -*Full* support indicates all tracer features are available. - -*Experimental* indicates most features should be available, but unverified. - **Supported web servers**: | Type | Documentation | Version | Support type | @@ -87,6 +85,16 @@ For descriptions of terminology used in APM, take a look at the [official docume | Unicorn | https://bogomips.org/unicorn/ | 4.8+ / 5.1+ | Full | | Passenger | https://www.phusionpassenger.com/ | 5.0+ | Full | +**Supported tracing frameworks**: + +| Type | Documentation | Version | Support type | +| ----------- | ----------------------------------------------- | --------------------- | ------------ | +| OpenTracing | https://github.com/opentracing/opentracing-ruby | 0.4.1+ (w/ Ruby 2.1+) | Experimental | + +*Full* support indicates all tracer features are available. + +*Experimental* indicates most features should be available, but unverified. + ## Installation The following steps will help you quickly start tracing your Ruby application. @@ -136,6 +144,36 @@ The Ruby APM tracer sends trace data through the Datadog Agent. 1. Activate integration instrumentation (see [Integration instrumentation](#integration-instrumentation)) 2. Add manual instrumentation around your code (see [Manual instrumentation](#manual-instrumentation)) +### Quickstart for OpenTracing + +1. Install the gem with `gem install ddtrace` +2. To your OpenTracing configuration file, add the following: + + ```ruby + require 'opentracing' + require 'ddtrace' + require 'ddtrace/opentracer' + + # Activate the Datadog tracer for OpenTracing + OpenTracing.global_tracer = Datadog::OpenTracer::Tracer.new + ``` + +3. (Optional) Add a configuration block to your Ruby application to configure Datadog with: + + ```ruby + Datadog.configure do |c| + # Configure the Datadog tracer here. + # Activate integrations, change tracer settings, etc... + # By default without additional configuration, + # no additional integrations will be traced, only + # what you have instrumented with OpenTracing. + end + ``` + +4. (Optional) Add or activate additional instrumentation by doing either of the following: + 1. Activate Datadog integration instrumentation (see [Integration instrumentation](#integration-instrumentation)) + 2. Add Datadog manual instrumentation around your code (see [Manual instrumentation](#manual-instrumentation)) + ### Final steps for installation After setting up, your services will appear on the [APM services page](https://app.datadoghq.com/apm/services) within a few minutes. Learn more about [using the APM UI][visualization docs]. @@ -869,6 +907,7 @@ Where `options` is an optional `Hash` that accepts the following parameters: | ``database_service`` | Database service name used when tracing database activity | ``-`` | | ``exception_controller`` | Class or Module which identifies a custom exception controller class. Tracer provides improved error behavior when it can identify custom exception controllers. By default, without this option, it 'guesses' what a custom exception controller looks like. Providing this option aids this identification. | ``nil`` | | ``distributed_tracing`` | Enables [distributed tracing](#distributed-tracing) so that this service trace is connected with a trace of another service if tracing headers are received | `false` | +| ``middleware`` | Add the trace middleware to the Rails application. Set to `false` if you don't want the middleware to load. | `true` | | ``middleware_names`` | Enables any short-circuited middleware requests to display the middleware name as resource for the trace. | `false` | | ``template_base_path`` | Used when the template name is parsed. If you don't store your templates in the ``views/`` folder, you may need to change this value | ``views/`` | | ``tracer`` | A ``Datadog::Tracer`` instance used to instrument the application. Usually you don't need to set that. | ``Datadog.tracer`` | @@ -1510,3 +1549,32 @@ Datadog::Pipeline.before_flush( Datadog::Pipeline::SpanProcessor.new { |span| span.resource.gsub!(/password=.*/, '') } ) ``` + +### OpenTracing + +For setting up Datadog with OpenTracing, see out [Quickstart for OpenTracing](#quickstart-for-opentracing) section for details. + +**Configuring Datadog tracer settings** + +The underlying Datadog tracer can be configured by passing options (which match `Datadog::Tracer`) when configuring the global tracer: + +```ruby +# Where `options` is a Hash of options provided to Datadog::Tracer +OpenTracing.global_tracer = Datadog::OpenTracer::Tracer.new(options) +``` + +It can also be configured by using `Datadog.configure` described in the [Tracer settings](#tracer-settings) section. + +**Activating and configuring integrations** + +By default, configuring OpenTracing with Datadog will not automatically activate any additional instrumentation provided by Datadog. You will only receive spans and traces from OpenTracing instrumentation you have in your application. + +However, additional instrumentation provided by Datadog can be activated alongside OpenTracing using `Datadog.configure`, which can be used to further enhance your tracing. To activate this, see [Integration instrumentation](#integration-instrumentation) for more details. + +**Supported serialization formats** + +| Type | Supported? | Additional information | +| ------------------------------ | ---------- | ---------------------- | +| `OpenTracing::FORMAT_TEXT_MAP` | Yes | | +| `OpenTracing::FORMAT_RACK` | Yes | Because of the loss of resolution in the Rack format, please note that baggage items with names containing either upper case characters or `-` will be converted to lower case and `_` in a round-trip respectively. We recommend avoiding these characters, or accommodating accordingly on the receiving end. | +| `OpenTracing::FORMAT_BINARY` | No | | diff --git a/lib/ddtrace/contrib/rack/patcher.rb b/lib/ddtrace/contrib/rack/patcher.rb index 9bde547e541..441fc0cb639 100644 --- a/lib/ddtrace/contrib/rack/patcher.rb +++ b/lib/ddtrace/contrib/rack/patcher.rb @@ -39,7 +39,8 @@ def patch @patched = true end - if !@middleware_patched && get_option(:middleware_names) + if (!instance_variable_defined?(:@middleware_patched) || !@middleware_patched) \ + && get_option(:middleware_names) if get_option(:application) enable_middleware_names @middleware_patched = true diff --git a/lib/ddtrace/contrib/rails/patcher.rb b/lib/ddtrace/contrib/rails/patcher.rb index 19e4f64be10..0aad1630635 100644 --- a/lib/ddtrace/contrib/rails/patcher.rb +++ b/lib/ddtrace/contrib/rails/patcher.rb @@ -1,4 +1,7 @@ require 'ddtrace/contrib/rails/utils' +require 'ddtrace/contrib/rails/framework' +require 'ddtrace/contrib/rails/middlewares' +require 'ddtrace/contrib/rack/middlewares' module Datadog module Contrib @@ -17,6 +20,7 @@ module Patcher Datadog.configuration[:active_record][:service_name] = value end end + option :middleware, default: true option :middleware_names, default: false option :distributed_tracing, default: false option :template_base_path, default: 'views/' @@ -28,7 +32,41 @@ module Patcher class << self def patch return @patched if patched? || !compatible? - require_relative 'framework' + + # Add a callback hook to add the trace middleware before the application initializes. + # Otherwise the middleware stack will be frozen. + do_once(:rails_before_initialize_hook) do + ::ActiveSupport.on_load(:before_initialize) do + # Sometimes we don't want to activate middleware e.g. OpenTracing, etc. + if Datadog.configuration[:rails][:middleware] + # Add trace middleware + config.middleware.insert_before(0, Datadog::Contrib::Rack::TraceMiddleware) + + # Insert right after Rails exception handling middleware, because if it's before, + # it catches and swallows the error. If it's too far after, custom middleware can find itself + # between, and raise exceptions that don't end up getting tagged on the request properly. + # e.g lost stack trace. + config.middleware.insert_after( + ActionDispatch::ShowExceptions, + Datadog::Contrib::Rails::ExceptionMiddleware + ) + end + end + end + + # Add a callback hook to finish configuring the tracer after the application is initialized. + # We need to wait for some things, like application name, middleware stack, etc. + do_once(:rails_after_initialize_hook) do + ::ActiveSupport.on_load(:after_initialize) do + Datadog::Contrib::Rails::Framework.setup + + # Add instrumentation to Rails components + Datadog::Contrib::Rails::ActionController.instrument + Datadog::Contrib::Rails::ActionView.instrument + Datadog::Contrib::Rails::ActiveSupport.instrument + end + end + @patched = true rescue => e Datadog::Tracer.log.error("Unable to apply Rails integration: #{e}") @@ -49,5 +87,3 @@ def compatible? end end end - -require 'ddtrace/contrib/rails/railtie' if Datadog.registry[:rails].compatible? diff --git a/lib/ddtrace/contrib/rails/railtie.rb b/lib/ddtrace/contrib/rails/railtie.rb index 42345b1583a..bb2a0ccdafc 100644 --- a/lib/ddtrace/contrib/rails/railtie.rb +++ b/lib/ddtrace/contrib/rails/railtie.rb @@ -5,11 +5,14 @@ module Datadog # Railtie class initializes class Railtie < Rails::Railtie - config.app_middleware.insert_before(0, Datadog::Contrib::Rack::TraceMiddleware) - # Insert right after Rails exception handling middleware, because if it's before, - # it catches and swallows the error. If it's too far after, custom middleware can find itself - # between, and raise exceptions that don't end up getting tagged on the request properly (e.g lost stack trace.) - config.app_middleware.insert_after(ActionDispatch::ShowExceptions, Datadog::Contrib::Rails::ExceptionMiddleware) + # Add the trace middleware to the application stack + initializer 'datadog.add_middleware' do |app| + app.middleware.insert_before(0, Datadog::Contrib::Rack::TraceMiddleware) + # Insert right after Rails exception handling middleware, because if it's before, + # it catches and swallows the error. If it's too far after, custom middleware can find itself + # between, and raise exceptions that don't end up getting tagged on the request properly (e.g lost stack trace.) + app.middleware.insert_after(ActionDispatch::ShowExceptions, Datadog::Contrib::Rails::ExceptionMiddleware) + end config.after_initialize do Datadog::Contrib::Rails::Framework.setup diff --git a/lib/ddtrace/opentracer.rb b/lib/ddtrace/opentracer.rb new file mode 100644 index 00000000000..75092b632bb --- /dev/null +++ b/lib/ddtrace/opentracer.rb @@ -0,0 +1,40 @@ +module Datadog + # Namespace for ddtrace OpenTracing implementation + module OpenTracer + module_function + + def supported? + Gem::Version.new(RUBY_VERSION) >= Gem::Version.new('2.1') + end + + def load_opentracer + require 'opentracing' + require 'opentracing/carrier' + require 'ddtrace' + require 'ddtrace/opentracer/carrier' + require 'ddtrace/opentracer/tracer' + require 'ddtrace/opentracer/span' + require 'ddtrace/opentracer/span_context' + require 'ddtrace/opentracer/span_context_factory' + require 'ddtrace/opentracer/scope' + require 'ddtrace/opentracer/scope_manager' + require 'ddtrace/opentracer/thread_local_scope' + require 'ddtrace/opentracer/thread_local_scope_manager' + require 'ddtrace/opentracer/distributed_headers' + require 'ddtrace/opentracer/propagator' + require 'ddtrace/opentracer/text_map_propagator' + require 'ddtrace/opentracer/binary_propagator' + require 'ddtrace/opentracer/rack_propagator' + require 'ddtrace/opentracer/global_tracer' + + # Modify the OpenTracing module functions + OpenTracing.module_eval do + class << self + prepend Datadog::OpenTracer::GlobalTracer + end + end + end + + load_opentracer if supported? + end +end diff --git a/lib/ddtrace/opentracer/binary_propagator.rb b/lib/ddtrace/opentracer/binary_propagator.rb new file mode 100644 index 00000000000..e70683d4f46 --- /dev/null +++ b/lib/ddtrace/opentracer/binary_propagator.rb @@ -0,0 +1,24 @@ +module Datadog + module OpenTracer + # OpenTracing propagator for Datadog::OpenTracer::Tracer + module BinaryPropagator + extend Propagator + + # Inject a SpanContext into the given carrier + # + # @param span_context [SpanContext] + # @param carrier [Carrier] A carrier object of Binary type + def self.inject(span_context, carrier) + nil + end + + # Extract a SpanContext in Binary format from the given carrier. + # + # @param carrier [Carrier] A carrier object of Binary type + # @return [SpanContext, nil] the extracted SpanContext or nil if none could be found + def self.extract(carrier) + SpanContext::NOOP_INSTANCE + end + end + end +end diff --git a/lib/ddtrace/opentracer/carrier.rb b/lib/ddtrace/opentracer/carrier.rb new file mode 100644 index 00000000000..9bd95c43843 --- /dev/null +++ b/lib/ddtrace/opentracer/carrier.rb @@ -0,0 +1,6 @@ +module Datadog + module OpenTracer + class Carrier < ::OpenTracing::Carrier + end + end +end diff --git a/lib/ddtrace/opentracer/distributed_headers.rb b/lib/ddtrace/opentracer/distributed_headers.rb new file mode 100644 index 00000000000..ab52d0ea541 --- /dev/null +++ b/lib/ddtrace/opentracer/distributed_headers.rb @@ -0,0 +1,46 @@ +require 'ddtrace/span' +require 'ddtrace/ext/distributed' + +module Datadog + module OpenTracer + # DistributedHeaders provides easy access and validation to headers + class DistributedHeaders + include Datadog::Ext::DistributedTracing + + def initialize(carrier) + @carrier = carrier + end + + def valid? + # Sampling priority is optional. + !trace_id.nil? && !parent_id.nil? + end + + def trace_id + id HTTP_HEADER_TRACE_ID + end + + def parent_id + id HTTP_HEADER_PARENT_ID + end + + def sampling_priority + hdr = @carrier[HTTP_HEADER_SAMPLING_PRIORITY] + # It's important to make a difference between no header, + # and a header defined to zero. + return unless hdr + value = hdr.to_i + return if value < 0 + value + end + + private + + def id(header) + value = @carrier[header].to_i + return if value.zero? || value >= Datadog::Span::MAX_ID + value < 0 ? value + 0x1_0000_0000_0000_0000 : value + end + end + end +end diff --git a/lib/ddtrace/opentracer/global_tracer.rb b/lib/ddtrace/opentracer/global_tracer.rb new file mode 100644 index 00000000000..cecf67180ba --- /dev/null +++ b/lib/ddtrace/opentracer/global_tracer.rb @@ -0,0 +1,15 @@ +module Datadog + module OpenTracer + # Patch for OpenTracing module + module GlobalTracer + def global_tracer=(tracer) + super.tap do + if tracer.class <= Datadog::OpenTracer::Tracer + # Update the Datadog global tracer, too. + Datadog.instance_variable_set(:@tracer, tracer.datadog_tracer) + end + end + end + end + end +end diff --git a/lib/ddtrace/opentracer/propagator.rb b/lib/ddtrace/opentracer/propagator.rb new file mode 100644 index 00000000000..77820e9a4f1 --- /dev/null +++ b/lib/ddtrace/opentracer/propagator.rb @@ -0,0 +1,22 @@ +module Datadog + module OpenTracer + # OpenTracing propagator for Datadog::OpenTracer::Tracer + module Propagator + # Inject a SpanContext into the given carrier + # + # @param span_context [SpanContext] + # @param carrier [Carrier] A carrier object of the type dictated by the specified `format` + def inject(span_context, carrier) + raise NotImplementedError + end + + # Extract a SpanContext in the given format from the given carrier. + # + # @param carrier [Carrier] A carrier object of the type dictated by the specified `format` + # @return [SpanContext, nil] the extracted SpanContext or nil if none could be found + def extract(carrier) + raise NotImplementedError + end + end + end +end diff --git a/lib/ddtrace/opentracer/rack_propagator.rb b/lib/ddtrace/opentracer/rack_propagator.rb new file mode 100644 index 00000000000..b9b36f02f2f --- /dev/null +++ b/lib/ddtrace/opentracer/rack_propagator.rb @@ -0,0 +1,60 @@ +require 'ddtrace/propagation/http_propagator' + +module Datadog + module OpenTracer + # OpenTracing propagator for Datadog::OpenTracer::Tracer + module RackPropagator + extend Propagator + extend Datadog::Ext::DistributedTracing + include Datadog::Ext::DistributedTracing + + BAGGAGE_PREFIX = 'ot-baggage-'.freeze + BAGGAGE_PREFIX_FORMATTED = 'HTTP_OT_BAGGAGE_'.freeze + + class << self + # Inject a SpanContext into the given carrier + # + # @param span_context [SpanContext] + # @param carrier [Carrier] A carrier object of Rack type + def inject(span_context, carrier) + # Inject Datadog trace properties + Datadog::HTTPPropagator.inject!(span_context.datadog_context, carrier) + + # Inject baggage + span_context.baggage.each do |key, value| + carrier[BAGGAGE_PREFIX + key] = value + end + + nil + end + + # Extract a SpanContext in Rack format from the given carrier. + # + # @param carrier [Carrier] A carrier object of Rack type + # @return [SpanContext, nil] the extracted SpanContext or nil if none could be found + def extract(carrier) + # First extract & build a Datadog context + datadog_context = Datadog::HTTPPropagator.extract(carrier) + + # Then extract any other baggage + baggage = {} + carrier.each do |key, value| + baggage[header_to_baggage(key)] = value if baggage_header?(key) + end + + SpanContextFactory.build(datadog_context: datadog_context, baggage: baggage) + end + + private + + def baggage_header?(header) + header.to_s.start_with?(BAGGAGE_PREFIX_FORMATTED) + end + + def header_to_baggage(key) + key[BAGGAGE_PREFIX_FORMATTED.length, key.length].downcase + end + end + end + end +end diff --git a/lib/ddtrace/opentracer/scope.rb b/lib/ddtrace/opentracer/scope.rb new file mode 100644 index 00000000000..a78c8bc3904 --- /dev/null +++ b/lib/ddtrace/opentracer/scope.rb @@ -0,0 +1,15 @@ +module Datadog + module OpenTracer + # OpenTracing adapter for scope + class Scope < ::OpenTracing::Scope + attr_reader \ + :manager, + :span + + def initialize(manager:, span:) + @manager = manager + @span = span + end + end + end +end diff --git a/lib/ddtrace/opentracer/scope_manager.rb b/lib/ddtrace/opentracer/scope_manager.rb new file mode 100644 index 00000000000..887a40cbd62 --- /dev/null +++ b/lib/ddtrace/opentracer/scope_manager.rb @@ -0,0 +1,6 @@ +module Datadog + module OpenTracer + class ScopeManager < ::OpenTracing::ScopeManager + end + end +end diff --git a/lib/ddtrace/opentracer/span.rb b/lib/ddtrace/opentracer/span.rb new file mode 100644 index 00000000000..e03c7dbb550 --- /dev/null +++ b/lib/ddtrace/opentracer/span.rb @@ -0,0 +1,90 @@ +module Datadog + module OpenTracer + # OpenTracing adapter for Datadog::Span + class Span < ::OpenTracing::Span + attr_reader \ + :datadog_span + + def initialize(datadog_span:, span_context:) + @datadog_span = datadog_span + @span_context = span_context + end + + # Set the name of the operation + # + # @param [String] name + def operation_name=(name) + datadog_span.name = name + end + + # Span Context + # + # @return [SpanContext] + def context + @span_context + end + + # Set a tag value on this span + # @param key [String] the key of the tag + # @param value [String, Numeric, Boolean] the value of the tag. If it's not + # a String, Numeric, or Boolean it will be encoded with to_s + def set_tag(key, value) + tap { datadog_span.set_tag(key, value) } + end + + # Set a baggage item on the span + # @param key [String] the key of the baggage item + # @param value [String] the value of the baggage item + def set_baggage_item(key, value) + tap do + # SpanContext is immutable, so to make changes + # build a new span context. + @span_context = SpanContextFactory.clone( + span_context: context, + baggage: { key => value } + ) + end + end + + # Get a baggage item + # @param key [String] the key of the baggage item + # @return [String] value of the baggage item + def get_baggage_item(key) + context.baggage[key] + end + + # @deprecated Use {#log_kv} instead. + # Reason: event is an optional standard log field defined in spec and not required. Also, + # method name {#log_kv} is more consistent with other language implementations such as Python and Go. + # + # Add a log entry to this span + # @param event [String] event name for the log + # @param timestamp [Time] time of the log + # @param fields [Hash] Additional information to log + def log(event: nil, timestamp: Time.now, **fields) + super # Log deprecation warning + + # If the fields specify an error + if fields.key?(:'error.object') + datadog_span.set_error(fields[:'error.object']) + end + end + + # Add a log entry to this span + # @param timestamp [Time] time of the log + # @param fields [Hash] Additional information to log + def log_kv(timestamp: Time.now, **fields) + # If the fields specify an error + if fields.key?(:'error.object') + datadog_span.set_error(fields[:'error.object']) + end + end + + # Finish the {Span} + # @param end_time [Time] custom end time, if not now + def finish(end_time: Time.now) + datadog_span.finish(end_time) + end + end + end +end diff --git a/lib/ddtrace/opentracer/span_context.rb b/lib/ddtrace/opentracer/span_context.rb new file mode 100644 index 00000000000..7424ca68e5d --- /dev/null +++ b/lib/ddtrace/opentracer/span_context.rb @@ -0,0 +1,14 @@ +module Datadog + module OpenTracer + # OpenTracing adapter for SpanContext + class SpanContext < ::OpenTracing::SpanContext + attr_reader \ + :datadog_context + + def initialize(datadog_context:, baggage: {}) + @datadog_context = datadog_context + @baggage = baggage.freeze + end + end + end +end diff --git a/lib/ddtrace/opentracer/span_context_factory.rb b/lib/ddtrace/opentracer/span_context_factory.rb new file mode 100644 index 00000000000..0af256ed73b --- /dev/null +++ b/lib/ddtrace/opentracer/span_context_factory.rb @@ -0,0 +1,23 @@ +module Datadog + module OpenTracer + # Creates new Datadog::OpenTracer::SpanContext + module SpanContextFactory + module_function + + def build(datadog_context:, baggage: {}) + SpanContext.new( + datadog_context: datadog_context, + baggage: baggage.dup + ) + end + + def clone(span_context:, baggage: {}) + SpanContext.new( + datadog_context: span_context.datadog_context, + # Merge baggage from previous SpanContext + baggage: span_context.baggage.merge(baggage) + ) + end + end + end +end diff --git a/lib/ddtrace/opentracer/text_map_propagator.rb b/lib/ddtrace/opentracer/text_map_propagator.rb new file mode 100644 index 00000000000..a7030e306e9 --- /dev/null +++ b/lib/ddtrace/opentracer/text_map_propagator.rb @@ -0,0 +1,73 @@ +require 'ddtrace/ext/distributed' + +module Datadog + module OpenTracer + # OpenTracing propagator for Datadog::OpenTracer::Tracer + module TextMapPropagator + extend Propagator + extend Datadog::Ext::DistributedTracing + include Datadog::Ext::DistributedTracing + + BAGGAGE_PREFIX = 'ot-baggage-'.freeze + + class << self + # Inject a SpanContext into the given carrier + # + # @param span_context [SpanContext] + # @param carrier [Carrier] A carrier object of Rack type + def inject(span_context, carrier) + # Inject Datadog trace properties + span_context.datadog_context.tap do |datadog_context| + carrier[HTTP_HEADER_TRACE_ID] = datadog_context.trace_id + carrier[HTTP_HEADER_PARENT_ID] = datadog_context.span_id + carrier[HTTP_HEADER_SAMPLING_PRIORITY] = datadog_context.sampling_priority + end + + # Inject baggage + span_context.baggage.each do |key, value| + carrier[BAGGAGE_PREFIX + key] = value + end + + nil + end + + # Extract a SpanContext in TextMap format from the given carrier. + # + # @param carrier [Carrier] A carrier object of TextMap type + # @return [SpanContext, nil] the extracted SpanContext or nil if none could be found + def extract(carrier) + # First extract & build a Datadog context + headers = DistributedHeaders.new(carrier) + + datadog_context = if headers.valid? + Datadog::Context.new( + trace_id: headers.trace_id, + span_id: headers.parent_id, + sampling_priority: headers.sampling_priority + ) + else + Datadog::Context.new + end + + # Then extract any other baggage + baggage = {} + carrier.each do |key, value| + baggage[item_to_baggage(key)] = value if baggage_item?(key) + end + + SpanContextFactory.build(datadog_context: datadog_context, baggage: baggage) + end + + private + + def baggage_item?(item) + item.to_s.start_with?(BAGGAGE_PREFIX) + end + + def item_to_baggage(key) + key[BAGGAGE_PREFIX.length, key.length] + end + end + end + end +end diff --git a/lib/ddtrace/opentracer/thread_local_scope.rb b/lib/ddtrace/opentracer/thread_local_scope.rb new file mode 100644 index 00000000000..cb70f37d3e0 --- /dev/null +++ b/lib/ddtrace/opentracer/thread_local_scope.rb @@ -0,0 +1,30 @@ +module Datadog + module OpenTracer + # OpenTracing adapter for thread local scopes + class ThreadLocalScope < Scope + attr_reader \ + :finish_on_close + + def initialize( + manager:, + span:, + finish_on_close: true + ) + super(manager: manager, span: span) + @finish_on_close = finish_on_close + @previous_scope = manager.active + end + + # Mark the end of the active period for the current thread and Scope, + # updating the ScopeManager#active in the process. + # + # NOTE: Calling close more than once on a single Scope instance leads to + # undefined behavior. + def close + return unless equal?(manager.active) + span.finish if finish_on_close + manager.send(:set_scope, @previous_scope) + end + end + end +end diff --git a/lib/ddtrace/opentracer/thread_local_scope_manager.rb b/lib/ddtrace/opentracer/thread_local_scope_manager.rb new file mode 100644 index 00000000000..04b42a170e4 --- /dev/null +++ b/lib/ddtrace/opentracer/thread_local_scope_manager.rb @@ -0,0 +1,40 @@ +module Datadog + module OpenTracer + # OpenTracing adapter for thread local scope management + class ThreadLocalScopeManager < ScopeManager + # Make a span instance active. + # + # @param span [Span] the Span that should become active + # @param finish_on_close [Boolean] whether the Span should automatically be + # finished when Scope#close is called + # @return [Scope] instance to control the end of the active period for the + # Span. It is a programming error to neglect to call Scope#close on the + # returned instance. + def activate(span, finish_on_close: true) + ThreadLocalScope.new( + manager: self, + span: span, + finish_on_close: finish_on_close + ).tap do |scope| + set_scope(scope) + end + end + + # @return [Scope] the currently active Scope which can be used to access the + # currently active Span. + # + # If there is a non-null Scope, its wrapped Span becomes an implicit parent + # (as Reference#CHILD_OF) of any newly-created Span at Tracer#start_active_span + # or Tracer#start_span time. + def active + Thread.current[object_id.to_s] + end + + private + + def set_scope(scope) + Thread.current[object_id.to_s] = scope + end + end + end +end diff --git a/lib/ddtrace/opentracer/tracer.rb b/lib/ddtrace/opentracer/tracer.rb new file mode 100644 index 00000000000..e61320fcca2 --- /dev/null +++ b/lib/ddtrace/opentracer/tracer.rb @@ -0,0 +1,208 @@ +require 'ddtrace/tracer' + +module Datadog + module OpenTracer + # OpenTracing adapter for Datadog::Tracer + class Tracer < ::OpenTracing::Tracer + extend Forwardable + + attr_reader \ + :datadog_tracer + + def_delegators \ + :datadog_tracer, + :configure + + def initialize(options = {}) + super() + @datadog_tracer = Datadog::Tracer.new(options) + end + + # @return [ScopeManager] the current ScopeManager. + def scope_manager + @scope_manager ||= ThreadLocalScopeManager.new + end + + # Returns a newly started and activated Scope. + # + # If the Tracer's ScopeManager#active is not nil, no explicit references + # are provided, and `ignore_active_scope` is false, then an inferred + # References#CHILD_OF reference is created to the ScopeManager#active's + # SpanContext when start_active is invoked. + # + # @param operation_name [String] The operation name for the Span + # @param child_of [SpanContext, Span] SpanContext that acts as a parent to + # the newly-started Span. If a Span instance is provided, its + # context is automatically substituted. See [Reference] for more + # information. + # + # If specified, the `references` parameter must be omitted. + # @param references [Array] An array of reference + # objects that identify one or more parent SpanContexts. + # @param start_time [Time] When the Span started, if not now + # @param tags [Hash] Tags to assign to the Span at start time + # @param ignore_active_scope [Boolean] whether to create an implicit + # References#CHILD_OF reference to the ScopeManager#active. + # @param finish_on_close [Boolean] whether span should automatically be + # finished when Scope#close is called + # @yield [Scope] If an optional block is passed to start_active it will + # yield the newly-started Scope. If `finish_on_close` is true then the + # Span will be finished automatically after the block is executed. + # @return [Scope] The newly-started and activated Scope + def start_active_span(operation_name, + child_of: nil, + references: nil, + start_time: Time.now, + tags: nil, + ignore_active_scope: false, + finish_on_close: true) + + # When meant to automatically determine the parent, + # Use the active scope first, otherwise fall back to any + # context generated by Datadog, so as to append to it and gain + # the benefit of any out-of-the-box tracing from Datadog preceding + # the OpenTracer::Tracer. + # + # We do this here instead of in #start_span because #start_span generates + # spans that are not assigned to a scope, a.k.a not supposed to be used by + # subsequent spans implicitly. By using the existing Datadog context, the span + # effectively ends up "assigned to a scope", by virtue of being added to the + # Context. Hence, it would behave more like an active span, which is why it + # should only be here. + unless child_of || ignore_active_scope + child_of = if scope_manager.active + scope_manager.active.span.context + else + SpanContextFactory.build(datadog_context: datadog_tracer.call_context) + end + end + + # Create the span, and auto-add it to the Datadog context. + span = start_span( + operation_name, + child_of: child_of, + references: references, + start_time: start_time, + tags: tags, + ignore_active_scope: ignore_active_scope + ) + + # Overwrite the tracer context with the OpenTracing managed context. + # This is mostly for the benefit of any out-of-the-box tracing from Datadog, + # such that spans generated by that tracing will be attached to the OpenTracer + # parent span. + datadog_tracer.provider.context = span.datadog_span.context + + scope_manager.activate(span, finish_on_close: finish_on_close).tap do |scope| + if block_given? + begin + yield(scope) + ensure + scope.close + end + end + end + end + + # Like #start_active_span, but the returned Span has not been registered via the + # ScopeManager. + # + # @param operation_name [String] The operation name for the Span + # @param child_of [SpanContext, Span] SpanContext that acts as a parent to + # the newly-started Span. If a Span instance is provided, its + # context is automatically substituted. See [Reference] for more + # information. + # + # If specified, the `references` parameter must be omitted. + # @param references [Array] An array of reference + # objects that identify one or more parent SpanContexts. + # @param start_time [Time] When the Span started, if not now + # @param tags [Hash] Tags to assign to the Span at start time + # @param ignore_active_scope [Boolean] whether to create an implicit + # References#CHILD_OF reference to the ScopeManager#active. + # @return [Span] the newly-started Span instance, which has not been + # automatically registered via the ScopeManager + def start_span(operation_name, + child_of: nil, + references: nil, + start_time: Time.now, + tags: nil, + ignore_active_scope: false) + + # Derive the OpenTracer::SpanContext to inherit from. + parent_span_context = inherited_span_context(child_of, ignore_active_scope: ignore_active_scope) + + # Retrieve Datadog::Context from parent SpanContext. + datadog_context = parent_span_context.nil? ? nil : parent_span_context.datadog_context + + # Build the new Datadog span + datadog_span = datadog_tracer.start_span( + operation_name, + child_of: datadog_context, + start_time: start_time, + tags: tags || {} + ) + + # Build or extend the OpenTracer::SpanContext + span_context = if parent_span_context + SpanContextFactory.clone(span_context: parent_span_context) + else + SpanContextFactory.build(datadog_context: datadog_span.context) + end + + # Wrap the Datadog span and OpenTracer::Span context in a OpenTracer::Span + Span.new(datadog_span: datadog_span, span_context: span_context) + end + + # Inject a SpanContext into the given carrier + # + # @param span_context [SpanContext] + # @param format [OpenTracing::FORMAT_TEXT_MAP, OpenTracing::FORMAT_BINARY, OpenTracing::FORMAT_RACK] + # @param carrier [Carrier] A carrier object of the type dictated by the specified `format` + def inject(span_context, format, carrier) + case format + when OpenTracing::FORMAT_TEXT_MAP + TextMapPropagator.inject(span_context, carrier) + when OpenTracing::FORMAT_BINARY + BinaryPropagator.inject(span_context, carrier) + when OpenTracing::FORMAT_RACK + RackPropagator.inject(span_context, carrier) + else + warn 'Unknown inject format' + end + end + + # Extract a SpanContext in the given format from the given carrier. + # + # @param format [OpenTracing::FORMAT_TEXT_MAP, OpenTracing::FORMAT_BINARY, OpenTracing::FORMAT_RACK] + # @param carrier [Carrier] A carrier object of the type dictated by the specified `format` + # @return [SpanContext, nil] the extracted SpanContext or nil if none could be found + def extract(format, carrier) + case format + when OpenTracing::FORMAT_TEXT_MAP + TextMapPropagator.extract(carrier) + when OpenTracing::FORMAT_BINARY + BinaryPropagator.extract(carrier) + when OpenTracing::FORMAT_RACK + RackPropagator.extract(carrier) + else + warn 'Unknown extract format' + nil + end + end + + private + + def inherited_span_context(parent, ignore_active_scope: false) + case parent + when Span + parent.context + when SpanContext + parent + else + ignore_active_scope ? nil : scope_manager.active && scope_manager.active.span.context + end + end + end + end +end diff --git a/lib/ddtrace/version.rb b/lib/ddtrace/version.rb index 8d3be30a8b2..8b20b4da40a 100644 --- a/lib/ddtrace/version.rb +++ b/lib/ddtrace/version.rb @@ -1,7 +1,7 @@ module Datadog module VERSION MAJOR = 0 - MINOR = 15 + MINOR = 16 PATCH = 0 PRE = nil diff --git a/spec/ddtrace/contrib/rails/railtie_spec.rb b/spec/ddtrace/contrib/rails/railtie_spec.rb new file mode 100644 index 00000000000..2570bab73f7 --- /dev/null +++ b/spec/ddtrace/contrib/rails/railtie_spec.rb @@ -0,0 +1,60 @@ +require 'ddtrace/contrib/rails/rails_helper' +require 'ddtrace/contrib/rails/framework' +require 'ddtrace/contrib/rails/middlewares' +require 'ddtrace/contrib/rack/middlewares' + +RSpec.describe 'Rails application' do + before(:each) { skip 'Test not compatible with Rails < 4.0' if Rails.version < '4.0' } + include_context 'Rails test application' + + let(:tracer) { ::Datadog::Tracer.new(writer: FauxWriter.new) } + + let(:routes) { { '/' => 'test#index' } } + let(:controllers) { [controller] } + + let(:controller) do + stub_const('TestController', Class.new(ActionController::Base) do + def index + head :ok + end + end) + end + + RSpec::Matchers.define :have_kind_of_middleware do |expected| + match do |actual| + while actual + return true if actual.class <= expected + without_warnings { actual = actual.instance_variable_get(:@app) } + end + false + end + end + + before(:each) do + Datadog.registry[:rails].instance_variable_set(:@patched, false) + Datadog.configure do |c| + c.tracer hostname: ENV.fetch('TEST_DDAGENT_HOST', 'localhost') + c.use :rails, rails_options if use_rails + end + end + + let(:use_rails) { true } + let(:rails_options) { { tracer: tracer } } + + describe 'with Rails integration #middleware option' do + context 'set to true' do + let(:rails_options) { super().merge(middleware: true) } + + it { expect(app).to have_kind_of_middleware(Datadog::Contrib::Rack::TraceMiddleware) } + it { expect(app).to have_kind_of_middleware(Datadog::Contrib::Rails::ExceptionMiddleware) } + end + + context 'set to false' do + let(:rails_options) { super().merge(middleware: false) } + after(:each) { Datadog.configuration[:rails][:middleware] = true } + + it { expect(app).to_not have_kind_of_middleware(Datadog::Contrib::Rack::TraceMiddleware) } + it { expect(app).to_not have_kind_of_middleware(Datadog::Contrib::Rails::ExceptionMiddleware) } + end + end +end diff --git a/spec/ddtrace/contrib/rails/support/application.rb b/spec/ddtrace/contrib/rails/support/application.rb index bfd5e3a8b3e..fa0a00a2c4c 100644 --- a/spec/ddtrace/contrib/rails/support/application.rb +++ b/spec/ddtrace/contrib/rails/support/application.rb @@ -3,6 +3,11 @@ RSpec.shared_context 'Rails test application' do include_context 'Rails base application' + before do + Datadog.registry[:rails].instance_variable_set(:@patched, false) + reset_rails_configuration! + end + let(:app) do initialize_app! rails_test_application.instance diff --git a/spec/ddtrace/contrib/rails/support/rails3.rb b/spec/ddtrace/contrib/rails/support/rails3.rb index d66a887f959..e4eea2f5f4c 100644 --- a/spec/ddtrace/contrib/rails/support/rails3.rb +++ b/spec/ddtrace/contrib/rails/support/rails3.rb @@ -30,7 +30,6 @@ def inherited(base) include_context 'Rails models' let(:rails_base_application) do - reset_rails_configuration! during_init = initialize_block klass = Class.new(Rails::Application) do redis_cache = [:redis_store, { url: ENV['REDIS_URL'] }] @@ -113,16 +112,10 @@ def draw_test_routes!(mapper) def reset_rails_configuration! Rails.class_variable_set(:@@application, nil) Rails::Application.class_variable_set(:@@instance, nil) - Rails::Railtie::Configuration.class_variable_set(:@@app_middleware, app_middleware) + if Rails::Railtie::Configuration.class_variable_defined?(:@@app_middleware) + Rails::Railtie::Configuration.class_variable_set(:@@app_middleware, Rails::Configuration::MiddlewareStackProxy.new) + end Rails::Railtie::Configuration.class_variable_set(:@@app_generators, nil) Rails::Railtie::Configuration.class_variable_set(:@@to_prepare_blocks, nil) end - - def app_middleware - current = Rails::Railtie::Configuration.class_variable_get(:@@app_middleware) - Datadog::Contrib::Rails::Test::Configuration.fetch(:app_middleware, current).dup.tap do |copy| - copy.instance_variable_set(:@operations, (copy.instance_variable_get(:@operations) || []).dup) - copy.instance_variable_set(:@delete_operations, (copy.instance_variable_get(:@delete_operations) || []).dup) - end - end end diff --git a/spec/ddtrace/contrib/rails/support/rails4.rb b/spec/ddtrace/contrib/rails/support/rails4.rb index ab5853767b9..4fbb2bf9d1d 100644 --- a/spec/ddtrace/contrib/rails/support/rails4.rb +++ b/spec/ddtrace/contrib/rails/support/rails4.rb @@ -16,7 +16,6 @@ include_context 'Rails models' let(:rails_base_application) do - reset_rails_configuration! klass = Class.new(Rails::Application) do def config.database_configuration parsed = super @@ -108,16 +107,10 @@ def reset_rails_configuration! Rails::Railtie::Configuration.class_variable_set(:@@eager_load_namespaces, nil) Rails::Railtie::Configuration.class_variable_set(:@@watchable_files, nil) Rails::Railtie::Configuration.class_variable_set(:@@watchable_dirs, nil) - Rails::Railtie::Configuration.class_variable_set(:@@app_middleware, app_middleware) + if Rails::Railtie::Configuration.class_variable_defined?(:@@app_middleware) + Rails::Railtie::Configuration.class_variable_set(:@@app_middleware, Rails::Configuration::MiddlewareStackProxy.new) + end Rails::Railtie::Configuration.class_variable_set(:@@app_generators, nil) Rails::Railtie::Configuration.class_variable_set(:@@to_prepare_blocks, nil) end - - def app_middleware - current = Rails::Railtie::Configuration.class_variable_get(:@@app_middleware) - Datadog::Contrib::Rails::Test::Configuration.fetch(:app_middleware, current).dup.tap do |copy| - copy.instance_variable_set(:@operations, (copy.instance_variable_get(:@operations) || []).dup) - copy.instance_variable_set(:@delete_operations, (copy.instance_variable_get(:@delete_operations) || []).dup) - end - end end diff --git a/spec/ddtrace/contrib/rails/support/rails5.rb b/spec/ddtrace/contrib/rails/support/rails5.rb index 8ef15c238bb..c5a97401652 100644 --- a/spec/ddtrace/contrib/rails/support/rails5.rb +++ b/spec/ddtrace/contrib/rails/support/rails5.rb @@ -16,7 +16,6 @@ include_context 'Rails models' let(:rails_base_application) do - reset_rails_configuration! klass = Class.new(Rails::Application) do def config.database_configuration parsed = super @@ -86,16 +85,10 @@ def reset_rails_configuration! Rails::Railtie::Configuration.class_variable_set(:@@eager_load_namespaces, nil) Rails::Railtie::Configuration.class_variable_set(:@@watchable_files, nil) Rails::Railtie::Configuration.class_variable_set(:@@watchable_dirs, nil) - Rails::Railtie::Configuration.class_variable_set(:@@app_middleware, app_middleware) + if Rails::Railtie::Configuration.class_variable_defined?(:@@app_middleware) + Rails::Railtie::Configuration.class_variable_set(:@@app_middleware, Rails::Configuration::MiddlewareStackProxy.new) + end Rails::Railtie::Configuration.class_variable_set(:@@app_generators, nil) Rails::Railtie::Configuration.class_variable_set(:@@to_prepare_blocks, nil) end - - def app_middleware - current = Rails::Railtie::Configuration.class_variable_get(:@@app_middleware) - Datadog::Contrib::Rails::Test::Configuration.fetch(:app_middleware, current).dup.tap do |copy| - copy.instance_variable_set(:@operations, (copy.instance_variable_get(:@operations) || []).dup) - copy.instance_variable_set(:@delete_operations, (copy.instance_variable_get(:@delete_operations) || []).dup) - end - end end diff --git a/spec/ddtrace/opentracer/binary_propagator_spec.rb b/spec/ddtrace/opentracer/binary_propagator_spec.rb new file mode 100644 index 00000000000..8a7eb035050 --- /dev/null +++ b/spec/ddtrace/opentracer/binary_propagator_spec.rb @@ -0,0 +1,23 @@ +require 'spec_helper' + +require 'ddtrace/opentracer' +require 'ddtrace/opentracer/helper' + +if Datadog::OpenTracer.supported? + RSpec.describe Datadog::OpenTracer::BinaryPropagator do + include_context 'OpenTracing helpers' + + describe '#inject' do + subject { described_class.inject(span_context, carrier) } + let(:span_context) { instance_double(Datadog::OpenTracer::SpanContext) } + let(:carrier) { instance_double(Datadog::OpenTracer::Carrier) } + it { is_expected.to be nil } + end + + describe '#extract' do + subject(:span_context) { described_class.extract(carrier) } + let(:carrier) { instance_double(Datadog::OpenTracer::Carrier) } + it { is_expected.to be(Datadog::OpenTracer::SpanContext::NOOP_INSTANCE) } + end + end +end diff --git a/spec/ddtrace/opentracer/carrier_spec.rb b/spec/ddtrace/opentracer/carrier_spec.rb new file mode 100644 index 00000000000..bc220c038da --- /dev/null +++ b/spec/ddtrace/opentracer/carrier_spec.rb @@ -0,0 +1,31 @@ +require 'spec_helper' + +require 'ddtrace/opentracer' +require 'ddtrace/opentracer/helper' + +if Datadog::OpenTracer.supported? + RSpec.describe Datadog::OpenTracer::Carrier do + include_context 'OpenTracing helpers' + + subject(:carrier) { described_class.new } + + describe '#[]' do + subject(:span) { carrier[key] } + let(:key) { 'key' } + it { is_expected.to be nil } + end + + describe '#[]=' do + subject(:result) { carrier[key] = value } + let(:key) { 'key' } + let(:value) { 'value' } + it { is_expected.to eq(value) } + end + + describe '#each' do + subject(:result) { carrier.each(&block) } + let(:block) { proc { |key, value| } } + it { is_expected.to be nil } + end + end +end diff --git a/spec/ddtrace/opentracer/distributed_headers_spec.rb b/spec/ddtrace/opentracer/distributed_headers_spec.rb new file mode 100644 index 00000000000..f77e7db8d97 --- /dev/null +++ b/spec/ddtrace/opentracer/distributed_headers_spec.rb @@ -0,0 +1,135 @@ +require 'spec_helper' + +require 'ddtrace/opentracer' +require 'ddtrace/opentracer/helper' + +if Datadog::OpenTracer.supported? + RSpec.describe Datadog::OpenTracer::DistributedHeaders do + include_context 'OpenTracing helpers' + + subject(:headers) { described_class.new(carrier) } + let(:carrier) { instance_double(Datadog::OpenTracer::Carrier) } + + describe '#valid?' do + subject(:valid) { headers.valid? } + + before(:each) do + allow(carrier).to receive(:[]) + .with(described_class::HTTP_HEADER_TRACE_ID) + .and_return(trace_id) + + allow(carrier).to receive(:[]) + .with(described_class::HTTP_HEADER_PARENT_ID) + .and_return(parent_id) + end + + context 'when #trace_id is missing' do + let(:trace_id) { nil } + let(:parent_id) { (Datadog::Span::MAX_ID + 1).to_s } + it { is_expected.to be false } + end + + context 'when #parent_id is missing' do + let(:trace_id) { (Datadog::Span::MAX_ID + 1).to_s } + let(:parent_id) { nil } + it { is_expected.to be false } + end + + context 'when both #trace_id and #parent_id are present' do + let(:trace_id) { (Datadog::Span::MAX_ID - 1).to_s } + let(:parent_id) { (Datadog::Span::MAX_ID - 1).to_s } + it { is_expected.to be true } + end + end + + describe '#trace_id' do + subject(:trace_id) { headers.trace_id } + + before(:each) do + allow(carrier).to receive(:[]) + .with(described_class::HTTP_HEADER_TRACE_ID) + .and_return(value) + end + + context 'when the header is missing' do + let(:value) { nil } + end + + context 'when the header is present' do + context 'but the value is out of range' do + let(:value) { (Datadog::Span::MAX_ID + 1).to_s } + it { is_expected.to be nil } + end + + context 'and the value is in range' do + let(:value) { (Datadog::Span::MAX_ID - 1).to_s } + it { is_expected.to eq value.to_i } + + context 'as a negative signed integer' do + # Convert signed int to unsigned int. + let(:value) { -8809075535603237910.to_s } + it { is_expected.to eq 9637668538106313706 } + end + end + end + end + + describe '#parent_id' do + subject(:trace_id) { headers.parent_id } + + before(:each) do + allow(carrier).to receive(:[]) + .with(described_class::HTTP_HEADER_PARENT_ID) + .and_return(value) + end + + context 'when the header is missing' do + let(:value) { nil } + end + + context 'when the header is present' do + context 'but the value is out of range' do + let(:value) { (Datadog::Span::MAX_ID + 1).to_s } + it { is_expected.to be nil } + end + + context 'and the value is in range' do + let(:value) { (Datadog::Span::MAX_ID - 1).to_s } + it { is_expected.to eq value.to_i } + + context 'as a negative signed integer' do + # Convert signed int to unsigned int. + let(:value) { -8809075535603237910.to_s } + it { is_expected.to eq 9637668538106313706 } + end + end + end + end + + describe '#sampling_priority' do + subject(:trace_id) { headers.sampling_priority } + + before(:each) do + allow(carrier).to receive(:[]) + .with(described_class::HTTP_HEADER_SAMPLING_PRIORITY) + .and_return(value) + end + + context 'when the header is missing' do + let(:value) { nil } + end + + context 'when the header is present' do + context 'but the value is out of range' do + let(:value) { '-1' } + it { is_expected.to be nil } + end + + context 'and the value is in range' do + let(:value) { '1' } + it { is_expected.to eq value.to_i } + end + end + end + end +end diff --git a/spec/ddtrace/opentracer/global_tracer_spec.rb b/spec/ddtrace/opentracer/global_tracer_spec.rb new file mode 100644 index 00000000000..0954dbc4860 --- /dev/null +++ b/spec/ddtrace/opentracer/global_tracer_spec.rb @@ -0,0 +1,36 @@ +require 'spec_helper' + +require 'ddtrace' +require 'ddtrace/opentracer' +require 'ddtrace/opentracer/helper' + +if Datadog::OpenTracer.supported? + RSpec.describe Datadog::OpenTracer::GlobalTracer do + include_context 'OpenTracing helpers' + + context 'when included into OpenTracing' do + describe '#global_tracer=' do + subject(:global_tracer) { OpenTracing.global_tracer = tracer } + after(:each) { Datadog.instance_variable_set(:@tracer, Datadog::Tracer.new) } + + context 'when given a Datadog::OpenTracer::Tracer' do + let(:tracer) { Datadog::OpenTracer::Tracer.new } + + it do + expect(global_tracer).to be(tracer) + expect(Datadog.tracer).to be(tracer.datadog_tracer) + end + end + + context 'when given some unknown kind of tracer' do + let(:tracer) { double('other tracer') } + + it do + expect(global_tracer).to be(tracer) + expect(Datadog.tracer).to_not be(tracer) + end + end + end + end + end +end diff --git a/spec/ddtrace/opentracer/helper.rb b/spec/ddtrace/opentracer/helper.rb new file mode 100644 index 00000000000..63a336467f3 --- /dev/null +++ b/spec/ddtrace/opentracer/helper.rb @@ -0,0 +1,5 @@ +RSpec.shared_context 'OpenTracing helpers' do + before(:each) do + skip 'OpenTracing not supported' unless Datadog::OpenTracer.supported? + end +end diff --git a/spec/ddtrace/opentracer/propagation_integration_spec.rb b/spec/ddtrace/opentracer/propagation_integration_spec.rb new file mode 100644 index 00000000000..2d9a004beea --- /dev/null +++ b/spec/ddtrace/opentracer/propagation_integration_spec.rb @@ -0,0 +1,288 @@ +require 'spec_helper' + +require 'ddtrace/opentracer' +require 'ddtrace/opentracer/helper' + +if Datadog::OpenTracer.supported? + RSpec.describe 'OpenTracer context propagation' do + include_context 'OpenTracing helpers' + + subject(:tracer) { Datadog::OpenTracer::Tracer.new(writer: FauxWriter.new) } + let(:datadog_tracer) { tracer.datadog_tracer } + let(:datadog_spans) { datadog_tracer.writer.spans(:keep) } + + def sampling_priority_metric(span) + span.get_metric(Datadog::OpenTracer::TextMapPropagator::SAMPLING_PRIORITY_KEY) + end + + describe 'via OpenTracing::FORMAT_TEXT_MAP' do + def baggage_to_carrier_format(baggage) + baggage.map { |k, v| [Datadog::OpenTracer::TextMapPropagator::BAGGAGE_PREFIX + k, v] }.to_h + end + + context 'when sending' do + let(:span_name) { 'operation.sender' } + let(:baggage) { { 'account_name' => 'acme' } } + let(:carrier) { {} } + + before(:each) do + tracer.start_active_span(span_name) do |scope| + scope.span.context.datadog_context.sampling_priority = 1 + baggage.each { |k, v| scope.span.set_baggage_item(k, v) } + tracer.inject( + scope.span.context, + OpenTracing::FORMAT_TEXT_MAP, + carrier + ) + end + end + + it do + expect(carrier).to include( + Datadog::OpenTracer::TextMapPropagator::HTTP_HEADER_TRACE_ID => a_kind_of(Integer), + Datadog::OpenTracer::TextMapPropagator::HTTP_HEADER_PARENT_ID => a_kind_of(Integer), + Datadog::OpenTracer::TextMapPropagator::HTTP_HEADER_SAMPLING_PRIORITY => a_kind_of(Integer) + ) + + expect(carrier[Datadog::OpenTracer::TextMapPropagator::HTTP_HEADER_PARENT_ID]).to be > 0 + + baggage.each do |k, v| + expect(carrier).to include(Datadog::OpenTracer::TextMapPropagator::BAGGAGE_PREFIX + k => v) + end + end + end + + context 'when receiving' do + let(:span_name) { 'operation.receiver' } + let(:baggage) { { 'account_name' => 'acme' } } + let(:baggage_with_prefix) { baggage_to_carrier_format(baggage) } + let(:carrier) { baggage_with_prefix } + + before(:each) do + span_context = tracer.extract(OpenTracing::FORMAT_TEXT_MAP, carrier) + tracer.start_active_span(span_name, child_of: span_context) do |scope| + @scope = scope + # Do some work. + end + end + + context 'a carrier with valid headers' do + let(:carrier) do + super().merge( + Datadog::OpenTracer::TextMapPropagator::HTTP_HEADER_TRACE_ID => trace_id.to_s, + Datadog::OpenTracer::TextMapPropagator::HTTP_HEADER_PARENT_ID => parent_id.to_s, + Datadog::OpenTracer::TextMapPropagator::HTTP_HEADER_SAMPLING_PRIORITY => sampling_priority.to_s + ) + end + + let(:trace_id) { Datadog::Span::MAX_ID - 1 } + let(:parent_id) { Datadog::Span::MAX_ID - 2 } + let(:sampling_priority) { 2 } + + let(:datadog_span) { datadog_spans.first } + + it { expect(datadog_spans).to have(1).items } + it { expect(datadog_span.name).to eq(span_name) } + it { expect(datadog_span.finished?).to be(true) } + it { expect(datadog_span.trace_id).to eq(trace_id) } + it { expect(datadog_span.parent_id).to eq(parent_id) } + it { expect(sampling_priority_metric(datadog_span)).to eq(sampling_priority) } + it { expect(@scope.span.context.baggage).to include(baggage) } + end + + context 'a carrier with no headers' do + let(:carrier) { {} } + + let(:datadog_span) { datadog_spans.first } + + it { expect(datadog_spans).to have(1).items } + it { expect(datadog_span.name).to eq(span_name) } + it { expect(datadog_span.finished?).to be(true) } + it { expect(datadog_span.parent_id).to eq(0) } + end + end + + context 'in a round-trip' do + let(:sender_span_name) { 'operation.sender' } + let(:receiver_span_name) { 'operation.receiver' } + let(:baggage) { { 'account_name' => 'acme' } } + let(:carrier) { {} } + + before(:each) do + tracer.start_active_span(sender_span_name) do |sender_scope| + sender_scope.span.context.datadog_context.sampling_priority = 1 + baggage.each { |k, v| sender_scope.span.set_baggage_item(k, v) } + tracer.inject( + sender_scope.span.context, + OpenTracing::FORMAT_TEXT_MAP, + carrier + ) + + span_context = tracer.extract(OpenTracing::FORMAT_TEXT_MAP, carrier) + tracer.start_active_span(receiver_span_name, child_of: span_context) do |receiver_scope| + @receiver_scope = receiver_scope + # Do some work. + end + end + end + + let(:sender_datadog_span) { datadog_spans.last } + let(:receiver_datadog_span) { datadog_spans.first } + + it { expect(datadog_spans).to have(2).items } + it { expect(sender_datadog_span.name).to eq(sender_span_name) } + it { expect(sender_datadog_span.finished?).to be(true) } + it { expect(sender_datadog_span.parent_id).to eq(0) } + it { expect(sampling_priority_metric(sender_datadog_span)).to eq(1) } + it { expect(receiver_datadog_span.name).to eq(receiver_span_name) } + it { expect(receiver_datadog_span.finished?).to be(true) } + it { expect(receiver_datadog_span.trace_id).to eq(sender_datadog_span.trace_id) } + it { expect(receiver_datadog_span.parent_id).to eq(sender_datadog_span.span_id) } + it { expect(sampling_priority_metric(receiver_datadog_span)).to eq(1) } + it { expect(@receiver_scope.span.context.baggage).to include(baggage) } + end + end + + describe 'via OpenTracing::FORMAT_RACK' do + def carrier_to_rack_format(carrier) + carrier.map { |k, v| ["http-#{k}".upcase!.tr('-', '_'), v] }.to_h + end + + def baggage_to_carrier_format(baggage) + baggage.map { |k, v| [Datadog::OpenTracer::RackPropagator::BAGGAGE_PREFIX + k, v] }.to_h + end + + context 'when sending' do + let(:span_name) { 'operation.sender' } + let(:baggage) { { 'account_name' => 'acme' } } + let(:carrier) { {} } + + before(:each) do + tracer.start_active_span(span_name) do |scope| + scope.span.context.datadog_context.sampling_priority = 1 + baggage.each { |k, v| scope.span.set_baggage_item(k, v) } + tracer.inject( + scope.span.context, + OpenTracing::FORMAT_RACK, + carrier + ) + end + end + + it do + expect(carrier).to include( + Datadog::OpenTracer::RackPropagator::HTTP_HEADER_TRACE_ID => a_kind_of(String), + Datadog::OpenTracer::RackPropagator::HTTP_HEADER_PARENT_ID => a_kind_of(String), + Datadog::OpenTracer::RackPropagator::HTTP_HEADER_SAMPLING_PRIORITY => a_kind_of(String) + ) + + expect(carrier[Datadog::OpenTracer::RackPropagator::HTTP_HEADER_PARENT_ID].to_i).to be > 0 + + baggage.each do |k, v| + expect(carrier).to include(Datadog::OpenTracer::RackPropagator::BAGGAGE_PREFIX + k => v) + end + end + end + + context 'when receiving' do + let(:span_name) { 'operation.receiver' } + let(:baggage) { { 'account_name' => 'acme' } } + let(:baggage_with_prefix) { baggage_to_carrier_format(baggage) } + let(:carrier) { carrier_to_rack_format(baggage_with_prefix) } + + before(:each) do + span_context = tracer.extract(OpenTracing::FORMAT_RACK, carrier) + tracer.start_active_span(span_name, child_of: span_context) do |scope| + @scope = scope + # Do some work. + end + end + + context 'a carrier with valid headers' do + let(:carrier) do + super().merge( + carrier_to_rack_format( + Datadog::OpenTracer::RackPropagator::HTTP_HEADER_TRACE_ID => trace_id.to_s, + Datadog::OpenTracer::RackPropagator::HTTP_HEADER_PARENT_ID => parent_id.to_s, + Datadog::OpenTracer::RackPropagator::HTTP_HEADER_SAMPLING_PRIORITY => sampling_priority.to_s + ) + ) + end + + let(:trace_id) { Datadog::Span::MAX_ID - 1 } + let(:parent_id) { Datadog::Span::MAX_ID - 2 } + let(:sampling_priority) { 2 } + + let(:datadog_span) { datadog_spans.first } + + it { expect(datadog_spans).to have(1).items } + it { expect(datadog_span.name).to eq(span_name) } + it { expect(datadog_span.finished?).to be(true) } + it { expect(datadog_span.trace_id).to eq(trace_id) } + it { expect(datadog_span.parent_id).to eq(parent_id) } + it { expect(sampling_priority_metric(datadog_span)).to eq(sampling_priority) } + it { expect(@scope.span.context.baggage).to include(baggage) } + end + + context 'a carrier with no headers' do + let(:carrier) { {} } + + let(:datadog_span) { datadog_spans.first } + + it { expect(datadog_spans).to have(1).items } + it { expect(datadog_span.name).to eq(span_name) } + it { expect(datadog_span.finished?).to be(true) } + it { expect(datadog_span.parent_id).to eq(0) } + end + end + + context 'in a round-trip' do + let(:sender_span_name) { 'operation.sender' } + let(:receiver_span_name) { 'operation.receiver' } + # NOTE: If these baggage names include either dashes or uppercase characters + # they will not make a round-trip with the same key format. They will + # be converted to underscores and lowercase characters, because Rack + # forces everything to uppercase/dashes in transport causing resolution + # on key format to be lost. + let(:baggage) { { 'account_name' => 'acme' } } + + before(:each) do + tracer.start_active_span(sender_span_name) do |sender_scope| + sender_scope.span.context.datadog_context.sampling_priority = 1 + baggage.each { |k, v| sender_scope.span.set_baggage_item(k, v) } + + carrier = {} + tracer.inject( + sender_scope.span.context, + OpenTracing::FORMAT_RACK, + carrier + ) + + carrier = carrier_to_rack_format(carrier) + + span_context = tracer.extract(OpenTracing::FORMAT_RACK, carrier) + tracer.start_active_span(receiver_span_name, child_of: span_context) do |receiver_scope| + @receiver_scope = receiver_scope + # Do some work. + end + end + end + + let(:sender_datadog_span) { datadog_spans.last } + let(:receiver_datadog_span) { datadog_spans.first } + + it { expect(datadog_spans).to have(2).items } + it { expect(sender_datadog_span.name).to eq(sender_span_name) } + it { expect(sender_datadog_span.finished?).to be(true) } + it { expect(sender_datadog_span.parent_id).to eq(0) } + it { expect(sampling_priority_metric(sender_datadog_span)).to eq(1) } + it { expect(receiver_datadog_span.name).to eq(receiver_span_name) } + it { expect(receiver_datadog_span.finished?).to be(true) } + it { expect(receiver_datadog_span.trace_id).to eq(sender_datadog_span.trace_id) } + it { expect(receiver_datadog_span.parent_id).to eq(sender_datadog_span.span_id) } + it { expect(sampling_priority_metric(receiver_datadog_span)).to eq(1) } + it { expect(@receiver_scope.span.context.baggage).to include(baggage) } + end + end + end +end diff --git a/spec/ddtrace/opentracer/propagator_spec.rb b/spec/ddtrace/opentracer/propagator_spec.rb new file mode 100644 index 00000000000..ecff15d2230 --- /dev/null +++ b/spec/ddtrace/opentracer/propagator_spec.rb @@ -0,0 +1,29 @@ +require 'spec_helper' + +require 'ddtrace/opentracer' +require 'ddtrace/opentracer/helper' + +if Datadog::OpenTracer.supported? + RSpec.describe Datadog::OpenTracer::Propagator do + include_context 'OpenTracing helpers' + + describe 'implemented class behavior' do + subject(:propagator_class) do + stub_const('TestPropagator', Class.new.tap do |klass| + klass.extend(described_class) + end) + end + + describe '#inject' do + let(:span_context) { instance_double(Datadog::OpenTracer::SpanContext) } + let(:carrier) { instance_double(Datadog::OpenTracer::Carrier) } + it { expect { propagator_class.inject(span_context, carrier) }.to raise_error(NotImplementedError) } + end + + describe '#extract' do + let(:carrier) { instance_double(Datadog::OpenTracer::Carrier) } + it { expect { propagator_class.extract(carrier) }.to raise_error(NotImplementedError) } + end + end + end +end diff --git a/spec/ddtrace/opentracer/rack_propagator_spec.rb b/spec/ddtrace/opentracer/rack_propagator_spec.rb new file mode 100644 index 00000000000..2d3957101bf --- /dev/null +++ b/spec/ddtrace/opentracer/rack_propagator_spec.rb @@ -0,0 +1,115 @@ +require 'spec_helper' + +require 'ddtrace/opentracer' +require 'ddtrace/opentracer/helper' + +if Datadog::OpenTracer.supported? + RSpec.describe Datadog::OpenTracer::RackPropagator do + include_context 'OpenTracing helpers' + + describe '#inject' do + subject { described_class.inject(span_context, carrier) } + + let(:span_context) do + instance_double( + Datadog::OpenTracer::SpanContext, + datadog_context: datadog_context, + baggage: baggage + ) + end + + let(:datadog_context) do + instance_double( + Datadog::Context, + trace_id: trace_id, + span_id: span_id, + sampling_priority: sampling_priority + ) + end + + let(:trace_id) { double('trace ID') } + let(:span_id) { double('span ID') } + let(:sampling_priority) { double('sampling priority') } + + let(:baggage) { { 'account_name' => 'acme' } } + + let(:carrier) { instance_double(Datadog::OpenTracer::Carrier) } + + # Expect carrier to be set with Datadog trace properties + before(:each) do + expect(carrier).to receive(:[]=) + .with(Datadog::HTTPPropagator::HTTP_HEADER_TRACE_ID, trace_id.to_s) + expect(carrier).to receive(:[]=) + .with(Datadog::HTTPPropagator::HTTP_HEADER_PARENT_ID, span_id.to_s) + expect(carrier).to receive(:[]=) + .with(Datadog::HTTPPropagator::HTTP_HEADER_SAMPLING_PRIORITY, sampling_priority.to_s) + end + + # Expect carrier to be set with OpenTracing baggage + before(:each) do + baggage.each do |key, value| + expect(carrier).to receive(:[]=) + .with(described_class::BAGGAGE_PREFIX + key, value) + end + end + + it { is_expected.to be nil } + end + + describe '#extract' do + subject(:span_context) { described_class.extract(carrier) } + let(:carrier) { instance_double(Datadog::OpenTracer::Carrier) } + let(:items) { {} } + let(:datadog_context) { instance_double(Datadog::Context) } + + before(:each) do + expect(Datadog::HTTPPropagator).to receive(:extract) + .with(carrier) + .and_return(datadog_context) + + allow(carrier).to receive(:each) { |&block| items.each(&block) } + end + + it { is_expected.to be_a_kind_of(Datadog::OpenTracer::SpanContext) } + + context 'when the carrier contains' do + context 'baggage' do + let(:value) { 'acme' } + let(:items) { { key => value } } + + before(:each) do + items.each do |key, value| + allow(carrier).to receive(:[]).with(key).and_return(value) + end + end + + context 'with a symbol' do + context 'that does not have a proper prefix' do + let(:key) { :my_baggage_item } + it { expect(span_context.baggage).to be_empty } + end + + context 'that has a proper prefix' do + let(:key) { :"#{described_class::BAGGAGE_PREFIX_FORMATTED}ACCOUNT_NAME" } + it { expect(span_context.baggage).to have(1).items } + it { expect(span_context.baggage).to include('account_name' => value) } + end + end + + context 'with a string' do + context 'that does not have a proper prefix' do + let(:key) { 'HTTP_ACCOUNT_NAME' } + it { expect(span_context.baggage).to be_empty } + end + + context 'that has a proper prefix' do + let(:key) { "#{described_class::BAGGAGE_PREFIX_FORMATTED}ACCOUNT_NAME" } + it { expect(span_context.baggage).to have(1).items } + it { expect(span_context.baggage).to include('account_name' => value) } + end + end + end + end + end + end +end diff --git a/spec/ddtrace/opentracer/scope_manager_spec.rb b/spec/ddtrace/opentracer/scope_manager_spec.rb new file mode 100644 index 00000000000..e22ae5d04e8 --- /dev/null +++ b/spec/ddtrace/opentracer/scope_manager_spec.rb @@ -0,0 +1,24 @@ +require 'spec_helper' + +require 'ddtrace/opentracer' +require 'ddtrace/opentracer/helper' + +if Datadog::OpenTracer.supported? + RSpec.describe Datadog::OpenTracer::ScopeManager do + include_context 'OpenTracing helpers' + + subject(:scope_manager) { described_class.new } + + describe '#activate' do + subject(:activate) { scope_manager.activate(span, finish_on_close: finish_on_close) } + let(:span) { instance_double(Datadog::OpenTracer::Span) } + let(:finish_on_close) { true } + it { is_expected.to be(OpenTracing::Scope::NOOP_INSTANCE) } + end + + describe '#activate' do + subject(:active) { scope_manager.active } + it { is_expected.to be(OpenTracing::Scope::NOOP_INSTANCE) } + end + end +end diff --git a/spec/ddtrace/opentracer/scope_spec.rb b/spec/ddtrace/opentracer/scope_spec.rb new file mode 100644 index 00000000000..6a503d63a94 --- /dev/null +++ b/spec/ddtrace/opentracer/scope_spec.rb @@ -0,0 +1,26 @@ +require 'spec_helper' + +require 'ddtrace/opentracer' +require 'ddtrace/opentracer/helper' + +if Datadog::OpenTracer.supported? + RSpec.describe Datadog::OpenTracer::Scope do + include_context 'OpenTracing helpers' + + subject(:scope) { described_class.new(manager: manager, span: span) } + let(:manager) { instance_double(Datadog::OpenTracer::ScopeManager) } + let(:span) { instance_double(Datadog::OpenTracer::Span) } + + it do + is_expected.to have_attributes( + manager: manager, + span: span + ) + end + + describe '#close' do + subject(:result) { scope.close } + it { is_expected.to be nil } + end + end +end diff --git a/spec/ddtrace/opentracer/span_context_factory_spec.rb b/spec/ddtrace/opentracer/span_context_factory_spec.rb new file mode 100644 index 00000000000..87cb75d9c32 --- /dev/null +++ b/spec/ddtrace/opentracer/span_context_factory_spec.rb @@ -0,0 +1,137 @@ +require 'spec_helper' + +require 'ddtrace/opentracer' +require 'ddtrace/opentracer/helper' + +if Datadog::OpenTracer.supported? + RSpec.describe Datadog::OpenTracer::SpanContextFactory do + include_context 'OpenTracing helpers' + + describe 'class methods' do + describe '#build' do + context 'given Datadog::Context' do + subject(:span_context) do + described_class.build( + datadog_context: datadog_context + ) + end + let(:datadog_context) { instance_double(Datadog::Context) } + + it { is_expected.to be_a_kind_of(Datadog::OpenTracer::SpanContext) } + + describe 'builds a SpanContext where' do + it { expect(span_context.datadog_context).to be(datadog_context) } + + describe '#baggage' do + subject(:baggage) { span_context.baggage } + it { is_expected.to be_a_kind_of(Hash) } + it { is_expected.to be_empty } + end + end + + context 'and baggage' do + subject(:span_context) do + described_class.build( + datadog_context: datadog_context, + baggage: original_baggage + ) + end + let(:original_baggage) { { 'account_id' => '1234' } } + + it { is_expected.to be_a_kind_of(Datadog::OpenTracer::SpanContext) } + + describe 'builds a SpanContext where' do + it { expect(span_context.datadog_context).to be(datadog_context) } + + describe '#baggage' do + subject(:baggage) { span_context.baggage } + it { is_expected.to be_a_kind_of(Hash) } + + context 'when the original baggage contains data' do + it { is_expected.to include('account_id' => '1234') } + it { is_expected.to_not be(original_baggage) } + end + end + end + end + end + end + + describe '#clone' do + context 'given a SpanContext' do + subject(:span_context) { described_class.clone(span_context: original_span_context) } + let(:original_span_context) do + instance_double( + Datadog::OpenTracer::SpanContext, + datadog_context: original_datadog_context, + baggage: original_baggage + ) + end + let(:original_datadog_context) { instance_double(Datadog::Context) } + let(:original_baggage) { {} } + + it { is_expected.to be_a_kind_of(Datadog::OpenTracer::SpanContext) } + + describe 'builds a SpanContext where' do + it { expect(span_context.datadog_context).to be(original_datadog_context) } + + describe '#baggage' do + subject(:baggage) { span_context.baggage } + it { is_expected.to be_a_kind_of(Hash) } + + context 'when the original SpanContext contains baggage' do + let(:original_baggage) { { 'org_id' => '4321' } } + it { is_expected.to include('org_id' => '4321') } + it { is_expected.to_not be(original_baggage) } + end + end + end + + context 'and baggage' do + subject(:span_context) { described_class.clone(span_context: original_span_context, baggage: param_baggage) } + let(:param_baggage) { {} } + + it { is_expected.to be_a_kind_of(Datadog::OpenTracer::SpanContext) } + + describe 'builds a SpanContext where' do + describe '#baggage' do + subject(:baggage) { span_context.baggage } + it { is_expected.to be_a_kind_of(Hash) } + + context 'when the original SpanContext contains baggage' do + let(:original_baggage) { { 'org_id' => '4321' } } + it { is_expected.to include('org_id' => '4321') } + it { is_expected.to_not be(original_baggage) } + end + + context 'when the original baggage contains data' do + let(:param_baggage) { { 'account_id' => '1234' } } + it { is_expected.to include('account_id' => '1234') } + it { is_expected.to_not be(param_baggage) } + end + + context 'when the original SpanContext baggage and param baggage contains data' do + context 'that doesn\'t overlap' do + let(:original_baggage) { { 'org_id' => '4321' } } + let(:param_baggage) { { 'account_id' => '1234' } } + it { is_expected.to include('org_id' => '4321', 'account_id' => '1234') } + it { is_expected.to_not be(original_baggage) } + it { is_expected.to_not be(param_baggage) } + end + + context 'that overlaps' do + let(:original_baggage) { { 'org_id' => '4321' } } + let(:param_baggage) { { 'org_id' => '1234' } } + it { is_expected.to include('org_id' => '1234') } + it { is_expected.to_not be(original_baggage) } + it { is_expected.to_not be(param_baggage) } + end + end + end + end + end + end + end + end + end +end diff --git a/spec/ddtrace/opentracer/span_context_spec.rb b/spec/ddtrace/opentracer/span_context_spec.rb new file mode 100644 index 00000000000..a220530e6da --- /dev/null +++ b/spec/ddtrace/opentracer/span_context_spec.rb @@ -0,0 +1,46 @@ +require 'spec_helper' + +require 'ddtrace/opentracer' +require 'ddtrace/opentracer/helper' + +if Datadog::OpenTracer.supported? + RSpec.describe Datadog::OpenTracer::SpanContext do + include_context 'OpenTracing helpers' + + describe '#initialize' do + context 'given a Datadog::Context' do + subject(:span_context) { described_class.new(datadog_context: datadog_context) } + let(:datadog_context) { instance_double(Datadog::Context) } + + it do + is_expected.to have_attributes( + datadog_context: datadog_context, + baggage: {} + ) + end + + context 'and baggage' do + subject(:span_context) do + described_class.new( + datadog_context: datadog_context, + baggage: original_baggage + ) + end + let(:original_baggage) { { account_id: '1234' } } + + it { is_expected.to be_a_kind_of(described_class) } + + describe 'builds a SpanContext where' do + describe '#baggage' do + subject(:baggage) { span_context.baggage } + it { is_expected.to be(original_baggage) } + it 'is immutable' do + expect { baggage[1] = 2 }.to raise_error(RuntimeError) + end + end + end + end + end + end + end +end diff --git a/spec/ddtrace/opentracer/span_spec.rb b/spec/ddtrace/opentracer/span_spec.rb new file mode 100644 index 00000000000..953564da801 --- /dev/null +++ b/spec/ddtrace/opentracer/span_spec.rb @@ -0,0 +1,92 @@ +require 'spec_helper' + +require 'ddtrace/opentracer' +require 'ddtrace/opentracer/helper' + +if Datadog::OpenTracer.supported? + RSpec.describe Datadog::OpenTracer::Span do + include_context 'OpenTracing helpers' + + subject(:span) { described_class.new(datadog_span: datadog_span, span_context: span_context) } + let(:datadog_span) { instance_double(Datadog::Span) } + let(:span_context) { instance_double(Datadog::OpenTracer::SpanContext) } + + describe '#operation_name=' do + subject(:result) { span.operation_name = name } + let(:name) { 'execute_job' } + + before(:each) { expect(datadog_span).to receive(:name=).with(name).and_return(name) } + it { expect(result).to eq(name) } + end + + describe '#context' do + subject(:context) { span.context } + it { is_expected.to be(span_context) } + end + + describe '#set_tag' do + subject(:result) { span.set_tag(key, value) } + let(:key) { 'account_id' } + let(:value) { '1234' } + before(:each) { expect(datadog_span).to receive(:set_tag).with(key, value) } + it { is_expected.to be(span) } + end + + describe '#set_baggage_item' do + subject(:result) { span.set_baggage_item(key, value) } + let(:key) { 'account_id' } + let(:value) { '1234' } + let(:new_span_context) { instance_double(Datadog::OpenTracer::SpanContext) } + + it 'creates a new SpanContext with the baggage item' do + expect(Datadog::OpenTracer::SpanContextFactory).to receive(:clone) + .with(span_context: span_context, baggage: hash_including(key => value)) + .and_return(new_span_context) + + is_expected.to be(span) + expect(span.context).to be(new_span_context) + end + end + + describe '#get_baggage_item' do + subject(:result) { span.get_baggage_item(key) } + let(:key) { 'account_id' } + let(:value) { '1234' } + let(:baggage) { { key => value } } + before(:each) { allow(span_context).to receive(:baggage).and_return(baggage) } + it { is_expected.to be(value) } + end + + describe '#log' do + subject(:log) { span.log(event: event, timestamp: timestamp, **fields) } + let(:event) { 'job_finished' } + let(:timestamp) { Time.now } + let(:fields) { { time_started: Time.now, account_id: '1234' } } + + # Expect a deprecation warning to be output. + it do + expect { log }.to output("Span#log is deprecated. Please use Span#log_kv instead.\n").to_stderr + end + end + + describe '#log_kv' do + subject(:log_kv) { span.log_kv(timestamp: timestamp, **fields) } + let(:timestamp) { Time.now } + + context 'when given arbitrary key/value pairs' do + let(:fields) { { time_started: Time.now, account_id: '1234' } } + # We don't expect this to do anything right now. + it { is_expected.to be nil } + end + + context 'when given an \'error.object\'' do + let(:fields) { { :'error.object' => error_object } } + let(:error_object) { instance_double(StandardError) } + + before(:each) { expect(datadog_span).to receive(:set_error).with(error_object) } + + it { is_expected.to be nil } + end + end + end +end diff --git a/spec/ddtrace/opentracer/text_map_propagator_spec.rb b/spec/ddtrace/opentracer/text_map_propagator_spec.rb new file mode 100644 index 00000000000..782c1d7ac34 --- /dev/null +++ b/spec/ddtrace/opentracer/text_map_propagator_spec.rb @@ -0,0 +1,148 @@ +require 'spec_helper' + +require 'ddtrace/opentracer' +require 'ddtrace/opentracer/helper' + +if Datadog::OpenTracer.supported? + RSpec.describe Datadog::OpenTracer::TextMapPropagator do + include_context 'OpenTracing helpers' + + describe '#inject' do + subject { described_class.inject(span_context, carrier) } + + let(:span_context) do + instance_double( + Datadog::OpenTracer::SpanContext, + datadog_context: datadog_context, + baggage: baggage + ) + end + + let(:datadog_context) do + instance_double( + Datadog::Context, + trace_id: trace_id, + span_id: span_id, + sampling_priority: sampling_priority + ) + end + + let(:trace_id) { double('trace ID') } + let(:span_id) { double('span ID') } + let(:sampling_priority) { double('sampling priority') } + + let(:baggage) { { 'account_name' => 'acme' } } + + let(:carrier) { instance_double(Datadog::OpenTracer::Carrier) } + + # Expect carrier to be set with Datadog trace properties + before(:each) do + expect(carrier).to receive(:[]=) + .with(Datadog::OpenTracer::DistributedHeaders::HTTP_HEADER_TRACE_ID, trace_id) + expect(carrier).to receive(:[]=) + .with(Datadog::OpenTracer::DistributedHeaders::HTTP_HEADER_PARENT_ID, span_id) + expect(carrier).to receive(:[]=) + .with(Datadog::OpenTracer::DistributedHeaders::HTTP_HEADER_SAMPLING_PRIORITY, sampling_priority) + end + + # Expect carrier to be set with OpenTracing baggage + before(:each) do + baggage.each do |key, value| + expect(carrier).to receive(:[]=) + .with(described_class::BAGGAGE_PREFIX + key, value) + end + end + + it { is_expected.to be nil } + end + + describe '#extract' do + subject(:span_context) { described_class.extract(carrier) } + let(:carrier) { instance_double(Datadog::OpenTracer::Carrier) } + let(:items) { {} } + let(:datadog_context) { span_context.datadog_context } + + before(:each) do + allow(carrier).to receive(:each) { |&block| items.each(&block) } + end + + context 'when the carrier contains' do + before(:each) do + allow(Datadog::OpenTracer::DistributedHeaders).to receive(:new) + .with(carrier) + .and_return(headers) + end + + shared_examples_for 'baggage' do + let(:value) { 'acme' } + let(:items) { { key => value } } + + context 'with a symbol' do + context 'that does not have a proper prefix' do + let(:key) { :my_baggage_item } + it { expect(span_context.baggage).to be_empty } + end + + context 'that has a proper prefix' do + let(:key) { :"#{described_class::BAGGAGE_PREFIX}account_name" } + it { expect(span_context.baggage).to have(1).items } + it { expect(span_context.baggage).to include('account_name' => value) } + end + end + + context 'with a string' do + context 'that does not have a proper prefix' do + let(:key) { 'HTTP_ACCOUNT_NAME' } + it { expect(span_context.baggage).to be_empty } + end + + context 'that has a proper prefix' do + let(:key) { "#{described_class::BAGGAGE_PREFIX}account_name" } + it { expect(span_context.baggage).to have(1).items } + it { expect(span_context.baggage).to include('account_name' => value) } + end + end + end + + context 'invalid Datadog headers' do + let(:headers) do + instance_double( + Datadog::OpenTracer::DistributedHeaders, + valid?: false + ) + end + + it { is_expected.to be_a_kind_of(Datadog::OpenTracer::SpanContext) } + it { expect(datadog_context.trace_id).to be nil } + it { expect(datadog_context.span_id).to be nil } + it { expect(datadog_context.sampling_priority).to be nil } + + it_behaves_like 'baggage' + end + + context 'valid Datadog headers' do + let(:headers) do + instance_double( + Datadog::OpenTracer::DistributedHeaders, + valid?: true, + trace_id: trace_id, + parent_id: parent_id, + sampling_priority: sampling_priority + ) + end + + let(:trace_id) { double('trace ID') } + let(:parent_id) { double('parent span ID') } + let(:sampling_priority) { double('sampling priority') } + + it { is_expected.to be_a_kind_of(Datadog::OpenTracer::SpanContext) } + it { expect(datadog_context.trace_id).to be trace_id } + it { expect(datadog_context.span_id).to be parent_id } + it { expect(datadog_context.sampling_priority).to be sampling_priority } + + it_behaves_like 'baggage' + end + end + end + end +end diff --git a/spec/ddtrace/opentracer/thread_local_scope_manager_spec.rb b/spec/ddtrace/opentracer/thread_local_scope_manager_spec.rb new file mode 100644 index 00000000000..503cd35c308 --- /dev/null +++ b/spec/ddtrace/opentracer/thread_local_scope_manager_spec.rb @@ -0,0 +1,40 @@ +require 'spec_helper' + +require 'ddtrace/opentracer' +require 'ddtrace/opentracer/helper' + +if Datadog::OpenTracer.supported? + RSpec.describe Datadog::OpenTracer::ThreadLocalScopeManager do + include_context 'OpenTracing helpers' + + subject(:scope_manager) { described_class.new } + + describe '#activate' do + subject(:activate) { scope_manager.activate(span, finish_on_close: finish_on_close) } + let(:scope) { activate } + let(:span) { instance_double(Datadog::OpenTracer::Span) } + let(:finish_on_close) { true } + it { is_expected.to be_a_kind_of(Datadog::OpenTracer::ThreadLocalScope) } + it { expect(scope.manager).to be(scope_manager) } + it { expect(scope.span).to be(span) } + end + + describe '#activate' do + subject(:active) { scope_manager.active } + + context 'when no scope has been activated' do + it { is_expected.to be nil } + end + + context 'when a scope has been activated' do + let(:scope) { scope_manager.activate(span, finish_on_close: finish_on_close) } + let(:span) { instance_double(Datadog::OpenTracer::Span) } + let(:finish_on_close) { true } + + before(:each) { scope } # Activate a scope + + it { is_expected.to be(scope) } + end + end + end +end diff --git a/spec/ddtrace/opentracer/thread_local_scope_spec.rb b/spec/ddtrace/opentracer/thread_local_scope_spec.rb new file mode 100644 index 00000000000..6fb2be5a8a6 --- /dev/null +++ b/spec/ddtrace/opentracer/thread_local_scope_spec.rb @@ -0,0 +1,80 @@ +require 'spec_helper' + +require 'ddtrace/opentracer' +require 'ddtrace/opentracer/helper' + +if Datadog::OpenTracer.supported? + RSpec.describe Datadog::OpenTracer::ThreadLocalScope do + include_context 'OpenTracing helpers' + + subject(:scope) do + described_class.new( + manager: manager, + span: span, + finish_on_close: finish_on_close + ) + end + let(:manager) { Datadog::OpenTracer::ThreadLocalScopeManager.new } + let(:span) { instance_double(Datadog::OpenTracer::Span) } + let(:finish_on_close) { true } + let(:previous_scope) { nil } + + before(:each) do + allow(manager).to receive(:active) do + # Unstub after first call + allow(manager).to receive(:active).and_call_original + previous_scope + end + end + + it { is_expected.to be_a_kind_of(Datadog::OpenTracer::Scope) } + it { is_expected.to have_attributes(finish_on_close: finish_on_close) } + + describe '#close' do + subject(:close) { scope.close } + + context 'when the scope is' do + before(:each) do + scope # Initialize the scope, to prevent overstubbing the previous stub + allow(manager).to receive(:active).and_return(active_scope) + end + + context 'active' do + let(:active_scope) { scope } + + context 'and #finish_on_close' do + context 'is true' do + let(:finish_on_close) { true } + + it 'finishes the span and restores the previous scope' do + expect(span).to receive(:finish) + expect(manager).to receive(:set_scope).with(previous_scope) + scope.close + end + end + + context 'is false' do + let(:finish_on_close) { false } + + it 'does not finish the span but restores the previous scope' do + expect(span).to_not receive(:finish) + expect(manager).to receive(:set_scope).with(previous_scope) + scope.close + end + end + end + end + + context 'not active' do + let(:active_scope) { instance_double(described_class) } + + it 'should do nothing' do + expect(span).to_not receive(:finish) + expect(manager).to_not receive(:set_scope) + scope.close + end + end + end + end + end +end diff --git a/spec/ddtrace/opentracer/tracer_integration_spec.rb b/spec/ddtrace/opentracer/tracer_integration_spec.rb new file mode 100644 index 00000000000..4a7849e8a2b --- /dev/null +++ b/spec/ddtrace/opentracer/tracer_integration_spec.rb @@ -0,0 +1,387 @@ +require 'spec_helper' + +require 'ddtrace/opentracer' +require 'ddtrace/opentracer/helper' + +if Datadog::OpenTracer.supported? + RSpec.describe Datadog::OpenTracer::Tracer do + include_context 'OpenTracing helpers' + + subject(:tracer) { described_class.new(writer: FauxWriter.new) } + let(:datadog_tracer) { tracer.datadog_tracer } + let(:datadog_spans) { datadog_tracer.writer.spans(:keep) } + + def current_trace_for(object) + case object + when Datadog::OpenTracer::Span + object.context.datadog_context.instance_variable_get(:@trace) + when Datadog::OpenTracer::SpanContext + object.datadog_context.instance_variable_get(:@trace) + when Datadog::OpenTracer::Scope + object.span.context.datadog_context.instance_variable_get(:@trace) + end + end + + describe '#start_span' do + context 'for a single span' do + context 'without a block' do + let(:span) { tracer.start_span(span_name, **options) } + let(:span_name) { 'operation.foo' } + let(:options) { {} } + before(:each) { span.finish } + + let(:datadog_span) { datadog_spans.first } + + it { expect(datadog_spans).to have(1).items } + it { expect(datadog_span.name).to eq(span_name) } + it { expect(datadog_span.finished?).to be(true) } + + context 'when given start_time' do + let(:options) { { start_time: start_time } } + let(:start_time) { Time.new(2000, 1, 1) } + it { expect(datadog_span.start_time).to be(start_time) } + end + + context 'when given tags' do + let(:options) { { tags: tags } } + let(:tags) { { 'operation.type' => 'validate', 'account_id' => 1 } } + it { tags.each { |k, v| expect(datadog_span.get_tag(k)).to eq(v.to_s) } } + end + end + end + + context 'for a nested span' do + context 'when there is no active scope' do + before(:each) do + tracer.start_span('operation.outer').tap do |_outer_span| + tracer.start_span('operation.inner').tap do |inner_span| + # Assert Datadog context integrity + # 1 item because they each should get their own context. + expect(current_trace_for(inner_span)).to have(1).items + expect(current_trace_for(inner_span)).to include(inner_span.datadog_span) + end.finish + end.finish + end + + let(:outer_datadog_span) { datadog_spans.last } + let(:inner_datadog_span) { datadog_spans.first } + + it { expect(datadog_spans).to have(2).items } + it { expect(outer_datadog_span.name).to eq('operation.outer') } + it { expect(outer_datadog_span.parent_id).to eq(0) } + it { expect(outer_datadog_span.finished?).to be true } + it { expect(inner_datadog_span.name).to eq('operation.inner') } + it { expect(inner_datadog_span.parent_id).to eq(0) } + it { expect(inner_datadog_span.finished?).to be true } + end + + context 'when there is an active scope' do + context 'which is used' do + before(:each) do + tracer.start_active_span('operation.parent') do |parent_scope| + tracer.start_span('operation.child').tap do |span| + # Assert Datadog context integrity + expect(current_trace_for(span)).to have(2).items + expect(current_trace_for(span)).to include(parent_scope.span.datadog_span, span.datadog_span) + end.finish + end + end + + let(:parent_datadog_span) { datadog_spans.last } + let(:child_datadog_span) { datadog_spans.first } + + it { expect(datadog_spans).to have(2).items } + it { expect(parent_datadog_span.name).to eq('operation.parent') } + it { expect(parent_datadog_span.parent_id).to eq(0) } + it { expect(parent_datadog_span.finished?).to be true } + it { expect(child_datadog_span.name).to eq('operation.child') } + it { expect(child_datadog_span.parent_id).to eq(parent_datadog_span.span_id) } + it { expect(child_datadog_span.finished?).to be true } + end + + context 'which is ignored' do + before(:each) do + tracer.start_active_span('operation.parent') do |_scope| + tracer.start_span('operation.child', ignore_active_scope: true).tap do |span| + # Assert Datadog context integrity + expect(current_trace_for(span)).to have(1).items + expect(current_trace_for(span)).to include(span.datadog_span) + end.finish + end + end + + let(:parent_datadog_span) { datadog_spans.last } + let(:child_datadog_span) { datadog_spans.first } + + it { expect(datadog_spans).to have(2).items } + it { expect(parent_datadog_span.name).to eq('operation.parent') } + it { expect(parent_datadog_span.parent_id).to eq(0) } + it { expect(parent_datadog_span.finished?).to be true } + it { expect(child_datadog_span.name).to eq('operation.child') } + it { expect(child_datadog_span.parent_id).to eq(0) } + it { expect(child_datadog_span.finished?).to be true } + end + end + + context 'manually associated with child_of' do + before(:each) do + tracer.start_span('operation.parent').tap do |parent_span| + tracer.start_active_span('operation.fake_parent') do + tracer.start_span('operation.child', child_of: parent_span).tap do |span| + # Assert Datadog context integrity + expect(current_trace_for(span)).to have(2).items + expect(current_trace_for(span)).to include(parent_span.datadog_span, span.datadog_span) + end.finish + end + end.finish + end + + let(:parent_datadog_span) { datadog_spans[2] } + let(:fake_parent_datadog_span) { datadog_spans[1] } + let(:child_datadog_span) { datadog_spans[0] } + + it { expect(datadog_spans).to have(3).items } + it { expect(parent_datadog_span.name).to eq('operation.parent') } + it { expect(parent_datadog_span.parent_id).to eq(0) } + it { expect(parent_datadog_span.finished?).to be true } + it { expect(child_datadog_span.name).to eq('operation.child') } + it { expect(child_datadog_span.parent_id).to eq(parent_datadog_span.span_id) } + it { expect(child_datadog_span.finished?).to be true } + end + end + + context 'for sibling span' do + before(:each) do + tracer.start_span('operation.older_sibling').finish + tracer.start_span('operation.younger_sibling').tap do |span| + # Assert Datadog context integrity + expect(current_trace_for(span)).to have(1).items + expect(current_trace_for(span)).to include(span.datadog_span) + end.finish + end + + let(:older_datadog_span) { datadog_spans.first } + let(:younger_datadog_span) { datadog_spans.last } + + it { expect(datadog_spans).to have(2).items } + it { expect(older_datadog_span.name).to eq('operation.older_sibling') } + it { expect(older_datadog_span.parent_id).to eq(0) } + it { expect(older_datadog_span.finished?).to be true } + it { expect(younger_datadog_span.name).to eq('operation.younger_sibling') } + it { expect(younger_datadog_span.parent_id).to eq(0) } + it { expect(younger_datadog_span.finished?).to be true } + end + end + + describe '#start_active_span' do + let(:span_name) { 'operation.foo' } + let(:options) { {} } + + context 'for a single span' do + context 'without a block' do + before(:each) { tracer.start_active_span(span_name, **options).close } + + let(:datadog_span) { datadog_spans.first } + + it { expect(datadog_spans).to have(1).items } + it { expect(datadog_span.name).to eq(span_name) } + it { expect(datadog_span.finished?).to be(true) } + + context 'when given start_time' do + let(:options) { { start_time: start_time } } + let(:start_time) { Time.new(2000, 1, 1) } + it { expect(datadog_span.start_time).to be(start_time) } + end + + context 'when given tags' do + let(:options) { { tags: tags } } + let(:tags) { { 'operation.type' => 'validate', 'account_id' => 1 } } + it { tags.each { |k, v| expect(datadog_span.get_tag(k)).to eq(v.to_s) } } + end + end + + context 'with a block' do + before(:each) { tracer.start_active_span(span_name, **options) { |scope| @scope = scope } } + + it do + expect { |b| tracer.start_active_span(span_name, &b) }.to yield_with_args( + a_kind_of(Datadog::OpenTracer::Scope) + ) + end + + let(:datadog_span) { datadog_spans.first } + + it { expect(datadog_spans).to have(1).items } + it { expect(datadog_span.name).to eq(span_name) } + it { expect(datadog_span.finished?).to be(true) } + + context 'when given finish_on_close' do + context 'as true' do + let(:options) { { finish_on_close: true } } + it { expect(datadog_span.finished?).to be(true) } + end + + context 'as false' do + let(:options) { { finish_on_close: false } } + let(:datadog_span) { @scope.span.datadog_span } + it { expect(datadog_span.finished?).to be(false) } + end + end + end + + context 'preceded by a Datadog span' do + let(:parent_span_name) { 'operation.bar' } + let(:options) { { finish_on_close: true } } + + before(:each) do + datadog_tracer.trace(parent_span_name) do |span| + @parent_span = span + tracer.start_active_span(span_name, **options) do |scope| + @scope = scope + end + end + end + + let(:parent_datadog_span) { datadog_spans.first } + let(:child_datadog_span) { datadog_spans.last } + + it { expect(datadog_spans).to have(2).items } + it { expect(parent_datadog_span.name).to eq(parent_span_name) } + it { expect(parent_datadog_span.parent_id).to eq(0) } + it { expect(parent_datadog_span.finished?).to be(true) } + it { expect(child_datadog_span.name).to eq(span_name) } + it { expect(child_datadog_span.parent_id).to eq(parent_datadog_span.span_id) } + it { expect(child_datadog_span.finished?).to be(true) } + it { expect(child_datadog_span.trace_id).to eq(parent_datadog_span.trace_id) } + end + + context 'followed by a Datadog span' do + let(:child_span_name) { 'operation.bar' } + let(:options) { { finish_on_close: true } } + + before(:each) do + tracer.start_active_span(span_name, **options) do |scope| + @scope = scope + datadog_tracer.trace(child_span_name) do |span| + @child_span = span + end + end + end + + let(:parent_datadog_span) { datadog_spans.last } + let(:child_datadog_span) { datadog_spans.first } + + it { expect(datadog_spans).to have(2).items } + it { expect(parent_datadog_span.name).to eq(span_name) } + it { expect(parent_datadog_span.parent_id).to eq(0) } + it { expect(parent_datadog_span.finished?).to be(true) } + it { expect(child_datadog_span.name).to eq(child_span_name) } + it { expect(child_datadog_span.parent_id).to eq(parent_datadog_span.span_id) } + it { expect(child_datadog_span.finished?).to be(true) } + it { expect(child_datadog_span.trace_id).to eq(parent_datadog_span.trace_id) } + end + end + + context 'for a nested span' do + context 'when there is an active scope' do + context 'which is used' do + before(:each) do + tracer.start_active_span('operation.parent') do |parent_scope| + tracer.start_active_span('operation.child') do |scope| + # Assert Datadog context integrity + expect(current_trace_for(scope)).to have(2).items + expect(current_trace_for(scope)).to include(parent_scope.span.datadog_span, scope.span.datadog_span) + end + end + end + + let(:parent_datadog_span) { datadog_spans.last } + let(:child_datadog_span) { datadog_spans.first } + + it { expect(datadog_spans).to have(2).items } + it { expect(parent_datadog_span.name).to eq('operation.parent') } + it { expect(parent_datadog_span.parent_id).to eq(0) } + it { expect(parent_datadog_span.finished?).to be true } + it { expect(child_datadog_span.name).to eq('operation.child') } + it { expect(child_datadog_span.parent_id).to eq(parent_datadog_span.span_id) } + it { expect(child_datadog_span.finished?).to be true } + end + + context 'which is ignored' do + before(:each) do + tracer.start_active_span('operation.parent') do |_parent_scope| + tracer.start_active_span('operation.child', ignore_active_scope: true) do |scope| + # Assert Datadog context integrity + expect(current_trace_for(scope)).to have(1).items + expect(current_trace_for(scope)).to include(scope.span.datadog_span) + end + end + end + + let(:parent_datadog_span) { datadog_spans.last } + let(:child_datadog_span) { datadog_spans.first } + + it { expect(datadog_spans).to have(2).items } + it { expect(parent_datadog_span.name).to eq('operation.parent') } + it { expect(parent_datadog_span.parent_id).to eq(0) } + it { expect(parent_datadog_span.finished?).to be true } + it { expect(child_datadog_span.name).to eq('operation.child') } + it { expect(child_datadog_span.parent_id).to eq(0) } + it { expect(child_datadog_span.finished?).to be true } + end + end + + context 'manually associated with child_of' do + before(:each) do + tracer.start_span('operation.parent').tap do |parent_span| + tracer.start_active_span('operation.fake_parent') do |_fake_parent_span| + tracer.start_active_span('operation.child', child_of: parent_span) do |scope| + # Assert Datadog context integrity + expect(current_trace_for(scope)).to have(2).items + expect(current_trace_for(scope)).to include(parent_span.datadog_span, scope.span.datadog_span) + end + end + end.finish + end + + let(:parent_datadog_span) { datadog_spans[2] } + let(:fake_parent_datadog_span) { datadog_spans[1] } + let(:child_datadog_span) { datadog_spans[0] } + + it { expect(datadog_spans).to have(3).items } + it { expect(parent_datadog_span.name).to eq('operation.parent') } + it { expect(parent_datadog_span.parent_id).to eq(0) } + it { expect(parent_datadog_span.finished?).to be true } + it { expect(child_datadog_span.name).to eq('operation.child') } + it { expect(child_datadog_span.parent_id).to eq(parent_datadog_span.span_id) } + it { expect(child_datadog_span.finished?).to be true } + it { expect(fake_parent_datadog_span.name).to eq('operation.fake_parent') } + it { expect(fake_parent_datadog_span.parent_id).to eq(0) } + it { expect(fake_parent_datadog_span.finished?).to be true } + end + end + + context 'for sibling span' do + before(:each) do + tracer.start_active_span('operation.older_sibling') { |scope| } + tracer.start_active_span('operation.younger_sibling') do |scope| + # Assert Datadog context integrity + expect(current_trace_for(scope)).to have(1).items + expect(current_trace_for(scope)).to include(scope.span.datadog_span) + end + end + + let(:older_datadog_span) { datadog_spans.first } + let(:younger_datadog_span) { datadog_spans.last } + + it { expect(datadog_spans).to have(2).items } + it { expect(older_datadog_span.name).to eq('operation.older_sibling') } + it { expect(older_datadog_span.parent_id).to eq(0) } + it { expect(older_datadog_span.finished?).to be true } + it { expect(younger_datadog_span.name).to eq('operation.younger_sibling') } + it { expect(younger_datadog_span.parent_id).to eq(0) } + it { expect(younger_datadog_span.finished?).to be true } + end + end + end +end diff --git a/spec/ddtrace/opentracer/tracer_spec.rb b/spec/ddtrace/opentracer/tracer_spec.rb new file mode 100644 index 00000000000..3bfaab5df94 --- /dev/null +++ b/spec/ddtrace/opentracer/tracer_spec.rb @@ -0,0 +1,166 @@ +require 'spec_helper' + +require 'ddtrace/opentracer' +require 'ddtrace/opentracer/helper' + +if Datadog::OpenTracer.supported? + RSpec.describe Datadog::OpenTracer::Tracer do + include_context 'OpenTracing helpers' + + subject(:tracer) { described_class.new(writer: FauxWriter.new) } + + ### Datadog::OpenTracing::Tracer behavior ### + + describe '#initialize' do + context 'when given options' do + subject(:tracer) { described_class.new(options) } + let(:options) { double('options') } + let(:datadog_tracer) { double('datadog_tracer') } + + before(:each) do + expect(Datadog::Tracer).to receive(:new) + .with(options) + .and_return(datadog_tracer) + end + + it { expect(tracer.datadog_tracer).to be(datadog_tracer) } + end + end + + describe '#datadog_tracer' do + subject(:datadog_tracer) { tracer.datadog_tracer } + it { is_expected.to be_a_kind_of(Datadog::Tracer) } + end + + describe '#configure' do + subject(:configure) { tracer.configure(options) } + let(:options) { double('options') } + + before(:each) do + expect(tracer.datadog_tracer).to receive(:configure) + .with(options) + end + + it { expect { configure }.to_not raise_error } + end + + ### Implemented OpenTracing::Tracer behavior ### + + describe '#scope_manager' do + subject(:scope_manager) { tracer.scope_manager } + it { is_expected.to be_a_kind_of(Datadog::OpenTracer::ThreadLocalScopeManager) } + end + + describe '#start_active_span' do + subject(:span) { tracer.start_active_span(name) } + let(:name) { 'opentracing_span' } + + it { is_expected.to be_a_kind_of(Datadog::OpenTracer::ThreadLocalScope) } + + context 'when a block is given' do + it do + expect { |b| tracer.start_active_span(name, &b) }.to yield_with_args( + a_kind_of(Datadog::OpenTracer::ThreadLocalScope) + ) + end + end + end + + describe '#start_span' do + subject(:span) { tracer.start_span(name) } + let(:name) { 'opentracing_span' } + + it { is_expected.to be_a_kind_of(Datadog::OpenTracer::Span) } + end + + describe '#inject' do + subject(:inject) { tracer.inject(span_context, format, carrier) } + let(:span_context) { instance_double(OpenTracing::SpanContext) } + let(:carrier) { instance_double(OpenTracing::Carrier) } + + shared_context 'by propagator' do + before(:each) do + expect(propagator).to receive(:inject) + .with(span_context, carrier) + end + end + + context 'when the format is' do + context 'OpenTracing::FORMAT_TEXT_MAP' do + include_context 'by propagator' + let(:format) { OpenTracing::FORMAT_TEXT_MAP } + let(:propagator) { Datadog::OpenTracer::TextMapPropagator } + it { expect { inject }.to_not output.to_stdout } + it { is_expected.to be nil } + end + + context 'OpenTracing::FORMAT_BINARY' do + include_context 'by propagator' + let(:format) { OpenTracing::FORMAT_BINARY } + let(:propagator) { Datadog::OpenTracer::BinaryPropagator } + it { expect { inject }.to_not output.to_stdout } + it { is_expected.to be nil } + end + + context 'OpenTracing::FORMAT_RACK' do + include_context 'by propagator' + let(:format) { OpenTracing::FORMAT_RACK } + let(:propagator) { Datadog::OpenTracer::RackPropagator } + it { expect { inject }.to_not output.to_stdout } + it { is_expected.to be nil } + end + + context 'unknown' do + let(:format) { double('unknown format') } + it { expect { inject }.to output("Unknown inject format\n").to_stderr } + end + end + end + + describe '#extract' do + subject(:extract) { tracer.extract(format, carrier) } + let(:carrier) { instance_double(OpenTracing::Carrier) } + let(:span_context) { instance_double(Datadog::OpenTracer::SpanContext) } + + shared_context 'by propagator' do + before(:each) do + expect(propagator).to receive(:extract) + .with(carrier) + .and_return(span_context) + end + end + + context 'when the format is' do + context 'OpenTracing::FORMAT_TEXT_MAP' do + include_context 'by propagator' + let(:format) { OpenTracing::FORMAT_TEXT_MAP } + let(:propagator) { Datadog::OpenTracer::TextMapPropagator } + it { expect { extract }.to_not output.to_stdout } + it { is_expected.to be span_context } + end + + context 'OpenTracing::FORMAT_BINARY' do + include_context 'by propagator' + let(:format) { OpenTracing::FORMAT_BINARY } + let(:propagator) { Datadog::OpenTracer::BinaryPropagator } + it { expect { extract }.to_not output.to_stdout } + it { is_expected.to be span_context } + end + + context 'OpenTracing::FORMAT_RACK' do + include_context 'by propagator' + let(:format) { OpenTracing::FORMAT_RACK } + let(:propagator) { Datadog::OpenTracer::RackPropagator } + it { expect { extract }.to_not output.to_stdout } + it { is_expected.to be span_context } + end + + context 'unknown' do + let(:format) { double('unknown format') } + before(:each) { expect { extract }.to output("Unknown extract format\n").to_stderr } + it { is_expected.to be nil } + end + end + end + end +end