diff --git a/Appraisals b/Appraisals index 94c3bb228c..02373b7653 100644 --- a/Appraisals +++ b/Appraisals @@ -391,6 +391,7 @@ elsif Gem::Version.new('2.2.0') <= Gem::Version.new(RUBY_VERSION) \ gem 'http' gem 'mongo', '>= 2.8.0' gem 'mysql2', '< 0.5', platform: :ruby + gem 'qless' gem 'racecar', '>= 0.3.5' gem 'rack', '< 2.1.0' # Locked due to grape incompatibility: https://github.com/ruby-grape/grape/issues/1980 gem 'rack-test' @@ -584,6 +585,7 @@ elsif Gem::Version.new('2.3.0') <= Gem::Version.new(RUBY_VERSION) \ gem 'mysql2', '< 0.5', platform: :ruby gem 'pg', platform: :ruby gem 'presto-client', '>= 0.5.14' + gem 'qless' gem 'racecar', '>= 0.3.5' gem 'rack', '< 2.1.0' # Locked due to grape incompatibility: https://github.com/ruby-grape/grape/issues/1980 gem 'rack-test' @@ -687,6 +689,7 @@ elsif Gem::Version.new('2.4.0') <= Gem::Version.new(RUBY_VERSION) \ gem 'mysql2', '< 0.5', platform: :ruby gem 'pg', platform: :ruby gem 'presto-client', '>= 0.5.14' + gem 'qless' gem 'racecar', '>= 0.3.5' gem 'rack' gem 'rack-test' @@ -839,6 +842,7 @@ elsif Gem::Version.new('2.5.0') <= Gem::Version.new(RUBY_VERSION) \ gem 'pg', platform: :ruby gem 'activerecord-jdbcpostgresql-adapter', platform: :jruby gem 'presto-client', '>= 0.5.14' + gem 'qless' gem 'racecar', '>= 0.3.5' gem 'rack' gem 'rack-test' @@ -979,6 +983,7 @@ elsif Gem::Version.new('2.6.0') <= Gem::Version.new(RUBY_VERSION) \ gem 'mysql2', '< 0.5', platform: :ruby gem 'pg', platform: :ruby gem 'presto-client', '>= 0.5.14' + gem 'qless' gem 'racecar', '>= 0.3.5' gem 'rack' gem 'rack-test' @@ -1122,6 +1127,7 @@ elsif Gem::Version.new('2.7.0') <= Gem::Version.new(RUBY_VERSION) gem 'mysql2', '< 0.5', platform: :ruby gem 'pg', platform: :ruby gem 'presto-client', '>= 0.5.14' + gem 'qless' gem 'racecar', '>= 0.3.5' gem 'rack' gem 'rack-test' diff --git a/Rakefile b/Rakefile index a4709c3805..6e6ed08088 100644 --- a/Rakefile +++ b/Rakefile @@ -101,6 +101,7 @@ namespace :spec do :mongodb, :mysql2, :presto, + :qless, :que, :racecar, :rack, @@ -355,6 +356,7 @@ task :ci do declare 'bundle exec appraisal contrib rake spec:mongodb' declare 'bundle exec appraisal contrib rake spec:mysql2' declare 'bundle exec appraisal contrib rake spec:presto' + declare 'bundle exec appraisal contrib rake spec:qless' declare 'bundle exec appraisal contrib rake spec:que' declare 'bundle exec appraisal contrib rake spec:racecar' declare 'bundle exec appraisal contrib rake spec:rack' @@ -582,6 +584,7 @@ task :ci do declare 'bundle exec appraisal contrib rake spec:mongodb' declare 'bundle exec appraisal contrib rake spec:mysql2' if RUBY_PLATFORM != 'java' # built-in jdbc is used instead declare 'bundle exec appraisal contrib rake spec:presto' + declare 'bundle exec appraisal contrib rake spec:qless' declare 'bundle exec appraisal contrib rake spec:que' declare 'bundle exec appraisal contrib rake spec:racecar' declare 'bundle exec appraisal contrib rake spec:rack' @@ -664,6 +667,7 @@ task :ci do declare 'bundle exec appraisal contrib rake spec:mongodb' declare 'bundle exec appraisal contrib rake spec:mysql2' declare 'bundle exec appraisal contrib rake spec:presto' + declare 'bundle exec appraisal contrib rake spec:qless' declare 'bundle exec appraisal contrib rake spec:que' declare 'bundle exec appraisal contrib rake spec:racecar' declare 'bundle exec appraisal contrib rake spec:rack' @@ -679,6 +683,7 @@ task :ci do declare 'bundle exec appraisal contrib rake spec:sneakers' declare 'bundle exec appraisal contrib rake spec:sucker_punch' declare 'bundle exec appraisal contrib rake spec:suite' + # Contrib specs with old gem versions declare 'bundle exec appraisal contrib-old rake spec:faraday' # Rails minitests @@ -746,6 +751,7 @@ task :ci do declare 'bundle exec appraisal contrib rake spec:mongodb' declare 'bundle exec appraisal contrib rake spec:mysql2' declare 'bundle exec appraisal contrib rake spec:presto' + declare 'bundle exec appraisal contrib rake spec:qless' declare 'bundle exec appraisal contrib rake spec:que' declare 'bundle exec appraisal contrib rake spec:racecar' declare 'bundle exec appraisal contrib rake spec:rack' @@ -761,6 +767,7 @@ task :ci do declare 'bundle exec appraisal contrib rake spec:sneakers' declare 'bundle exec appraisal contrib rake spec:sucker_punch' declare 'bundle exec appraisal contrib rake spec:suite' + # Contrib specs with old gem versions declare 'bundle exec appraisal contrib-old rake spec:faraday' # Rails minitests diff --git a/docs/GettingStarted.md b/docs/GettingStarted.md index da0cf11ac1..5cff96aa83 100644 --- a/docs/GettingStarted.md +++ b/docs/GettingStarted.md @@ -49,6 +49,7 @@ To contribute, check out the [contribution guidelines][contribution docs] and [d - [MySQL2](#mysql2) - [Net/HTTP](#net-http) - [Presto](#presto) + - [Qless](#qless) - [Que](#que) - [Racecar](#racecar) - [Rack](#rack) @@ -356,6 +357,7 @@ For a list of available integrations, and their configuration options, please re | MySQL2 | `mysql2` | `>= 0.3.21` | *gem not available* | *[Link](#mysql2)* | *[Link](https://github.com/brianmario/mysql2)* | | Net/HTTP | `http` | *(Any supported Ruby)* | *(Any supported Ruby)* | *[Link](#nethttp)* | *[Link](https://ruby-doc.org/stdlib-2.4.0/libdoc/net/http/rdoc/Net/HTTP.html)* | | Presto | `presto` | `>= 0.5.14` | `>= 0.5.14` | *[Link](#presto)* | *[Link](https://github.com/treasure-data/presto-client-ruby)* | +| Qless | `qless` | `>= 0.10.0` | `>= 0.10.0` | *[Link](#qless)* | *[Link](https://github.com/seomoz/qless)* | | Que | `que` | `>= 1.0.0.beta2` | `>= 1.0.0.beta2` | *[Link](#que)* | *[Link](https://github.com/que-rb/que)* | | Racecar | `racecar` | `>= 0.3.5` | `>= 0.3.5` | *[Link](#racecar)* | *[Link](https://github.com/zendesk/racecar)* | | Rack | `rack` | `>= 1.1` | `>= 1.1` | *[Link](#rack)* | *[Link](https://github.com/rack/rack)* | @@ -1155,6 +1157,29 @@ Where `options` is an optional `Hash` that accepts the following parameters: | `analytics_enabled` | Enable analytics for spans produced by this integration. `true` for on, `nil` to defer to global setting, `false` for off. | `false` | | `service_name` | Service name used for `presto` instrumentation | `'presto'` | +### Qless + +The Qless integration uses lifecycle hooks to trace job executions. + +To add tracing to a Qless job: + +```ruby +require 'ddtrace' + +Datadog.configure do |c| + c.use :qless, options +end +``` + +Where `options` is an optional `Hash` that accepts the following parameters: + +| Key | Description | Default | +| --- | ----------- | ------- | +| `analytics_enabled` | Enable analytics for spans produced by this integration. `true` for on, `nil` to defer to the global setting, `false` for off. | `false` | +| `service_name` | Service name used for `qless` instrumentation | `'qless'` | +| `tag_job_data` | Enable tagging with job arguments. true for on, false for off. | `false` | +| `tag_job_tags` | Enable tagging with job tags. true for on, false for off. | `false` | + ### Que The Que integration is a middleware which will trace job executions. diff --git a/lib/ddtrace.rb b/lib/ddtrace.rb index 7ca051a6fe..3e0cef55e9 100644 --- a/lib/ddtrace.rb +++ b/lib/ddtrace.rb @@ -63,6 +63,7 @@ module Datadog require 'ddtrace/contrib/que/integration' require 'ddtrace/contrib/mysql2/integration' require 'ddtrace/contrib/mongodb/integration' +require 'ddtrace/contrib/qless/integration' require 'ddtrace/contrib/racecar/integration' require 'ddtrace/contrib/rack/integration' require 'ddtrace/contrib/rails/integration' diff --git a/lib/ddtrace/contrib/qless/configuration/settings.rb b/lib/ddtrace/contrib/qless/configuration/settings.rb new file mode 100644 index 0000000000..5ca9a7ecde --- /dev/null +++ b/lib/ddtrace/contrib/qless/configuration/settings.rb @@ -0,0 +1,35 @@ +require 'ddtrace/contrib/configuration/settings' +require 'ddtrace/contrib/qless/ext' + +module Datadog + module Contrib + module Qless + module Configuration + # Custom settings for the Qless integration + class Settings < Contrib::Configuration::Settings + option :analytics_enabled do |o| + o.default { env_to_bool(Ext::ENV_ANALYTICS_ENABLED, false) } + o.lazy + end + + option :analytics_sample_rate do |o| + o.default { env_to_float(Ext::ENV_ANALYTICS_SAMPLE_RATE, 1.0) } + o.lazy + end + + option :tag_job_data do |o| + o.default { env_to_bool(Ext::ENV_TAG_JOB_DATA, false) } + o.lazy + end + + option :tag_job_tags do |o| + o.default { env_to_bool(Ext::ENV_TAG_JOB_TAGS, false) } + o.lazy + end + + option :service_name, default: Ext::SERVICE_NAME + end + end + end + end +end diff --git a/lib/ddtrace/contrib/qless/ext.rb b/lib/ddtrace/contrib/qless/ext.rb new file mode 100644 index 0000000000..9704100859 --- /dev/null +++ b/lib/ddtrace/contrib/qless/ext.rb @@ -0,0 +1,20 @@ +module Datadog + module Contrib + module Qless + # Qless integration constants + module Ext + APP = 'qless'.freeze + ENV_ANALYTICS_ENABLED = 'DD_QLESS_ANALYTICS_ENABLED'.freeze + ENV_ANALYTICS_SAMPLE_RATE = 'DD_QLESS_ANALYTICS_SAMPLE_RATE'.freeze + ENV_TAG_JOB_DATA = 'DD_QLESS_TAG_JOB_DATA'.freeze + ENV_TAG_JOB_TAGS = 'DD_QLESS_TAG_JOB_TAGS'.freeze + SERVICE_NAME = 'qless'.freeze + SPAN_JOB = 'qless.job'.freeze + TAG_JOB_ID = 'qless.job.id'.freeze + TAG_JOB_DATA = 'qless.job.data'.freeze + TAG_JOB_QUEUE = 'qless.job.queue'.freeze + TAG_JOB_TAGS = 'qless.job.tags'.freeze + end + end + end +end diff --git a/lib/ddtrace/contrib/qless/integration.rb b/lib/ddtrace/contrib/qless/integration.rb new file mode 100644 index 0000000000..916fb56789 --- /dev/null +++ b/lib/ddtrace/contrib/qless/integration.rb @@ -0,0 +1,38 @@ +require 'ddtrace/contrib/integration' +require 'ddtrace/contrib/qless/configuration/settings' +require 'ddtrace/contrib/qless/patcher' + +module Datadog + module Contrib + module Qless + # Description of Qless integration + class Integration + include Contrib::Integration + + MINIMUM_VERSION = Gem::Version.new('0.10.0') + + register_as :qless, auto_patch: true + + def self.version + Gem.loaded_specs['qless'] && Gem.loaded_specs['qless'].version + end + + def self.loaded? + !defined?(::Qless).nil? + end + + def self.compatible? + super && version >= MINIMUM_VERSION + end + + def default_configuration + Configuration::Settings.new + end + + def patcher + Patcher + end + end + end + end +end diff --git a/lib/ddtrace/contrib/qless/patcher.rb b/lib/ddtrace/contrib/qless/patcher.rb new file mode 100644 index 0000000000..2593a54317 --- /dev/null +++ b/lib/ddtrace/contrib/qless/patcher.rb @@ -0,0 +1,35 @@ +require 'ddtrace/contrib/patcher' +require 'ddtrace/ext/app_types' + +module Datadog + module Contrib + module Qless + # Patcher enables patching of 'qless' module. + module Patcher + include Contrib::Patcher + + module_function + + def target_version + Integration.version + end + + def patch + require_relative 'qless_job' + require_relative 'tracer_cleaner' + + # Instrument all Qless Workers + ::Qless::Workers::BaseWorker.class_eval do + # These are executed in inverse order of listing here + include QlessJob + include TracerCleaner + end + end + + def get_option(option) + Datadog.configuration[:qless].get_option(option) + end + end + end + end +end diff --git a/lib/ddtrace/contrib/qless/qless_job.rb b/lib/ddtrace/contrib/qless/qless_job.rb new file mode 100644 index 0000000000..70f3d0f2f0 --- /dev/null +++ b/lib/ddtrace/contrib/qless/qless_job.rb @@ -0,0 +1,72 @@ +require 'ddtrace/ext/app_types' +require 'ddtrace/contrib/analytics' +require 'qless' + +module Datadog + module Contrib + module Qless + # Uses Qless job hooks to create traces + module QlessJob + def around_perform(job) + return super unless datadog_configuration && tracer + tracer.trace(Ext::SPAN_JOB, span_options) do |span| + span.resource = job.klass_name + span.span_type = Datadog::Ext::AppTypes::WORKER + span.set_tag(Ext::TAG_JOB_ID, job.jid) + span.set_tag(Ext::TAG_JOB_QUEUE, job.queue_name) + + tag_job_tags = datadog_configuration[:tag_job_tags] + span.set_tag(Ext::TAG_JOB_TAGS, job.tags) if tag_job_tags + + tag_job_data = datadog_configuration[:tag_job_data] + if tag_job_data && !job.data.empty? + job_data = job.data.with_indifferent_access + formatted_data = job_data.except(:tags).map do |key, value| + "#{key}:#{value}".underscore + end + + span.set_tag(Ext::TAG_JOB_DATA, formatted_data) + end + + # Set analytics sample rate + if Contrib::Analytics.enabled?(datadog_configuration[:analytics_enabled]) + Contrib::Analytics.set_sample_rate(span, datadog_configuration[:analytics_sample_rate]) + end + + # Measure service stats + Contrib::Analytics.set_measured(span) + + super + end + end + + def after_fork + configuration = Datadog.configuration[:qless] + return if configuration.nil? + + # Add a pin, marking the job as forked. + # Used to trigger shutdown in forks for performance reasons. + # Cleanup happens in the TracerCleaner class + Datadog::Pin.new( + configuration[:service_name], + config: { forked: true } + ).onto(::Qless) + end + + private + + def span_options + { service: datadog_configuration[:service_name] } + end + + def tracer + datadog_configuration.tracer + end + + def datadog_configuration + Datadog.configuration[:qless] + end + end + end + end +end diff --git a/lib/ddtrace/contrib/qless/tracer_cleaner.rb b/lib/ddtrace/contrib/qless/tracer_cleaner.rb new file mode 100644 index 0000000000..3e3503e505 --- /dev/null +++ b/lib/ddtrace/contrib/qless/tracer_cleaner.rb @@ -0,0 +1,32 @@ +module Datadog + module Contrib + module Qless + # Shutdown Tracer in forks for performance reasons + module TracerCleaner + def around_perform(job) + return super unless datadog_configuration && tracer + + super.tap do + tracer.shutdown! if forked? + end + end + + private + + def forked? + pin = Datadog::Pin.get_from(::Qless) + return false unless pin + pin.config[:forked] == true + end + + def tracer + datadog_configuration.tracer + end + + def datadog_configuration + Datadog.configuration[:qless] + end + end + end + end +end diff --git a/spec/ddtrace/contrib/qless/instrumentation_spec.rb b/spec/ddtrace/contrib/qless/instrumentation_spec.rb new file mode 100644 index 0000000000..9799755584 --- /dev/null +++ b/spec/ddtrace/contrib/qless/instrumentation_spec.rb @@ -0,0 +1,146 @@ +require 'ddtrace/contrib/support/spec_helper' +require 'ddtrace/contrib/analytics_examples' +require 'ddtrace/contrib/qless/integration' +require_relative 'support/job' + +require 'ddtrace' + +RSpec.describe 'Qless instrumentation' do + include_context 'Qless job' + + let(:configuration_options) { {} } + + before(:each) do + delete_all_redis_keys + + # Patch Qless + Datadog.configure do |c| + c.use :qless, configuration_options + end + end + + around do |example| + # Reset before and after each example; don't allow global state to linger. + Datadog.registry[:qless].reset_configuration! + example.run + Datadog.registry[:qless].reset_configuration! + end + + shared_examples 'job execution tracing' do + context 'that succeeds' do + before(:each) do + perform_job(job_class, job_args) + end + + it 'is traced' do + expect(spans).to have(1).items + expect(failed_jobs.count).to eq(0) + expect(span.name).to eq('qless.job') + expect(span.resource).to eq(job_class.name) + expect(span.span_type).to eq(Datadog::Ext::AppTypes::WORKER) + expect(span.service).to eq('qless') + expect(span).to_not have_error + end + + it_behaves_like 'analytics for integration' do + let(:analytics_enabled_var) { Datadog::Contrib::Qless::Ext::ENV_ANALYTICS_ENABLED } + let(:analytics_sample_rate_var) { Datadog::Contrib::Qless::Ext::ENV_ANALYTICS_SAMPLE_RATE } + end + + it_behaves_like 'measured span for integration', true + end + + context 'that fails' do + before(:each) do + # Rig the job to fail + expect(job_class).to receive(:perform) do + raise error_class, error_message + end + + # Perform it + perform_job(job_class) + end + + let(:error_class_name) { 'TestJobFailError' } + let(:error_class) { stub_const(error_class_name, Class.new(StandardError)) } + let(:error_message) { 'TestJob failed' } + + it 'is traced' do + expect(spans).to have(1).items + expect(failed_jobs.count).to eq(1) + expect(failed_jobs).to eq('TestJob:TestJobFailError' => 1) + expect(span.name).to eq('qless.job') + expect(span.resource).to eq(job_class.name) + expect(span.span_type).to eq(Datadog::Ext::AppTypes::WORKER) + expect(span.service).to eq('qless') + expect(span).to have_error_message(error_message) + expect(span).to have_error + expect(span).to have_error_type(error_class_name) + end + end + end + + context 'without forking' do + let(:worker) { Qless::Workers::SerialWorker.new(reserver) } + + it_should_behave_like 'job execution tracing' + + it 'ensures worker is not using forking' do + expect(worker.class).to eq(Qless::Workers::SerialWorker) + end + + describe 'patching for workers' do + it 'adds the instrumentation module' do + expect(worker.singleton_class.included_modules).to include(Datadog::Contrib::Qless::QlessJob) + end + end + end + + context 'with forking' do + before do + skip unless PlatformHelpers.supports_fork? + + # Ensures worker is using forking + expect(worker.class).to eq(Qless::Workers::ForkingWorker) + end + + context 'trace context' do + subject(:perform) do + tracer.trace('parent.process') do |span| + @parent_span = span + perform_job(job_class) + end + + expect(failed_jobs.count).to eq(0) + end + + context 'on main process' do + it 'only contains parent process spans' do + perform + + expect(span.name).to eq('parent.process') + end + end + + context 'on child process' do + it 'does not include parent process spans' do + expect(job_class).to receive(:perform) do + # Mock #shutdown! in fork only + expect(tracer).to receive(:shutdown!).and_wrap_original do |m, *args| + m.call(*args) + + expect(span.name).to eq('qless.job') + expect(span).to have_distributed_parent(@parent_span) + end + end + + perform + + # Remove hard-expectation from parent process, + # as `job_class#perform` won't be called in this process. + RSpec::Mocks.space.proxy_for(job_class).reset + end + end + end + end +end diff --git a/spec/ddtrace/contrib/qless/integration_spec.rb b/spec/ddtrace/contrib/qless/integration_spec.rb new file mode 100644 index 0000000000..74b6926125 --- /dev/null +++ b/spec/ddtrace/contrib/qless/integration_spec.rb @@ -0,0 +1,68 @@ +require 'spec_helper' + +require 'ddtrace/contrib/qless/integration' + +RSpec.describe Datadog::Contrib::Qless::Integration do + extend ConfigurationHelpers + + let(:integration) { described_class.new(:qless) } + + describe '.version' do + subject(:version) { described_class.version } + + context 'when the "qless" gem is loaded' do + include_context 'loaded gems', qless: described_class::MINIMUM_VERSION + it { is_expected.to be_a_kind_of(Gem::Version) } + end + + context 'when "qless" gem is not loaded' do + include_context 'loaded gems', qless: nil + it { is_expected.to be nil } + end + end + + describe '.loaded?' do + subject(:loaded?) { described_class.loaded? } + + context 'when Qless is defined' do + before { stub_const('Qless', Class.new) } + it { is_expected.to be true } + end + + context 'when Qless is not defined' do + before { hide_const('Qless') } + it { is_expected.to be false } + end + end + + describe '.compatible?' do + subject(:compatible?) { described_class.compatible? } + + context 'when "qless" gem is loaded with a version' do + context 'that is less than the minimum' do + include_context 'loaded gems', qless: decrement_gem_version(described_class::MINIMUM_VERSION) + it { is_expected.to be false } + end + + context 'that meets the minimum version' do + include_context 'loaded gems', qless: described_class::MINIMUM_VERSION + it { is_expected.to be true } + end + end + + context 'when gem is not loaded' do + include_context 'loaded gems', qless: nil + it { is_expected.to be false } + end + end + + describe '#default_configuration' do + subject(:default_configuration) { integration.default_configuration } + it { is_expected.to be_a_kind_of(Datadog::Contrib::Qless::Configuration::Settings) } + end + + describe '#patcher' do + subject(:patcher) { integration.patcher } + it { is_expected.to be Datadog::Contrib::Qless::Patcher } + end +end diff --git a/spec/ddtrace/contrib/qless/support/job.rb b/spec/ddtrace/contrib/qless/support/job.rb new file mode 100644 index 0000000000..b8322e5465 --- /dev/null +++ b/spec/ddtrace/contrib/qless/support/job.rb @@ -0,0 +1,66 @@ +LogHelpers.without_warnings do + require 'qless' +end + +require 'ddtrace/contrib/qless/qless_job' +require 'qless' +require 'qless/test_helpers/worker_helpers' +require 'qless/worker' +require 'qless/job_reservers/ordered' + +### For ForkingWorker +require 'qless/job_reservers/round_robin' +require 'tempfile' + +class TempfileWithString < Tempfile + # To mirror StringIO#string + def string + rewind + read.tap { close } + end +end + +RSpec.shared_context 'Qless job' do + include Qless::WorkerHelpers + + let(:host) { ENV.fetch('TEST_REDIS_HOST', '127.0.0.1') } + let(:port) { ENV.fetch('TEST_REDIS_PORT', 6379) } + let(:client) { Qless::Client.new(host: host, port: port) } + let(:queue) { client.queues['main'] } + let(:reserver) { Qless::JobReservers::Ordered.new([queue]) } + + let(:log_io) { TempfileWithString.new('qless.log') } + let(:worker) do + Qless::Workers::ForkingWorker.new( + Qless::JobReservers::RoundRobin.new([queue]), + interval: 1, + max_startup_interval: 0, + output: log_io, + log_level: Logger::DEBUG + ) + end + + after { log_io.unlink } + + def perform_job(klass, *args) + queue.put(klass, args) + drain_worker_queues(worker) + end + + def failed_jobs + client.jobs.failed + end + + def delete_all_redis_keys + client.redis.keys.each { |k| client.redis.del k } + end + + let(:job_class) do + stub_const('TestJob', Class.new).tap do |mod| + mod.send(:define_singleton_method, :perform) do |job| + # Do nothing by default. + end + end + end + let(:job_args) { {} } +end diff --git a/spec/support/span_helpers.rb b/spec/support/span_helpers.rb index 75608662bf..9ac79f6f92 100644 --- a/spec/support/span_helpers.rb +++ b/spec/support/span_helpers.rb @@ -36,4 +36,17 @@ def description_of(actual) # rubocop:disable Lint/NestedMethodDefinition define_have_error_tag(:message, Datadog::Ext::Errors::MSG) define_have_error_tag(:stack, Datadog::Ext::Errors::STACK) define_have_error_tag(:type, Datadog::Ext::Errors::TYPE) + + # Distributed traces have the same trace_id and parent_id as upstream parent + # span, but don't actually share the same Context with the parent. + RSpec::Matchers.define :have_distributed_parent do |parent| + match do |actual| + @matcher = have_attributes(parent_id: parent.span_id, trace_id: parent.trace_id) + @matcher.matches? actual + end + + failure_message do + @matcher.failure_message + end + end end