diff --git a/lib/ddtrace/opentracer.rb b/lib/ddtrace/opentracer.rb index c5e59270608..75092b632bb 100644 --- a/lib/ddtrace/opentracer.rb +++ b/lib/ddtrace/opentracer.rb @@ -20,6 +20,11 @@ def load_opentracer 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 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/distributed_headers.rb b/lib/ddtrace/opentracer/distributed_headers.rb new file mode 100644 index 00000000000..d31e9c9a9f4 --- /dev/null +++ b/lib/ddtrace/opentracer/distributed_headers.rb @@ -0,0 +1,42 @@ +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 + value = @carrier[HTTP_HEADER_TRACE_ID].to_i + return if value <= 0 || value >= Datadog::Span::MAX_ID + value + end + + def parent_id + value = @carrier[HTTP_HEADER_PARENT_ID].to_i + return if value <= 0 || value >= Datadog::Span::MAX_ID + value + 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 + 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..754984ddbbe --- /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.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/text_map_propagator.rb b/lib/ddtrace/opentracer/text_map_propagator.rb new file mode 100644 index 00000000000..5b7974c0474 --- /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.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/tracer.rb b/lib/ddtrace/opentracer/tracer.rb index fcd75470453..75f5cd7efbf 100644 --- a/lib/ddtrace/opentracer/tracer.rb +++ b/lib/ddtrace/opentracer/tracer.rb @@ -100,21 +100,6 @@ def start_span(operation_name, start_time: Time.now, tags: nil, ignore_active_scope: false) - # Get the parent Datadog span - parent_datadog_span = case child_of - when Span - child_of.datadog_span - else - ignore_active_scope ? nil : scope_manager.active && scope_manager.active.span.datadog_span - end - - # Build the new Datadog span - datadog_span = datadog_tracer.start_span( - operation_name, - child_of: parent_datadog_span, - start_time: start_time, - tags: tags || {} - ) # Derive the OpenTracer::SpanContext to inherit from parent_span_context = case child_of @@ -126,6 +111,14 @@ def start_span(operation_name, ignore_active_scope ? nil : scope_manager.active && scope_manager.active.span.context end + # Build the new Datadog span + datadog_span = datadog_tracer.start_span( + operation_name, + child_of: parent_span_context && parent_span_context.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) @@ -144,8 +137,12 @@ def start_span(operation_name, # @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, OpenTracing::FORMAT_BINARY, OpenTracing::FORMAT_RACK - return nil + 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 @@ -158,8 +155,12 @@ def inject(span_context, format, carrier) # @return [SpanContext, nil] the extracted SpanContext or nil if none could be found def extract(format, carrier) case format - when OpenTracing::FORMAT_TEXT_MAP, OpenTracing::FORMAT_BINARY, OpenTracing::FORMAT_RACK - return SpanContext::NOOP_INSTANCE + 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 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/distributed_headers_spec.rb b/spec/ddtrace/opentracer/distributed_headers_spec.rb new file mode 100644 index 00000000000..5dd2f985c76 --- /dev/null +++ b/spec/ddtrace/opentracer/distributed_headers_spec.rb @@ -0,0 +1,123 @@ +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 } + 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 } + 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/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..c05ecf4e0d2 --- /dev/null +++ b/spec/ddtrace/opentracer/rack_propagator_spec.rb @@ -0,0 +1,100 @@ +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 'without a proper prefix' do + let(:key) { 'HTTP_ACCOUNT_NAME' } + it { expect(span_context.baggage).to be_empty } + end + + context 'with 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 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..7186dcc6928 --- /dev/null +++ b/spec/ddtrace/opentracer/text_map_propagator_spec.rb @@ -0,0 +1,133 @@ +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 'without a proper prefix' do + let(:key) { 'account_name' } + it { expect(span_context.baggage).to be_empty } + end + + context 'with 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 '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/tracer_spec.rb b/spec/ddtrace/opentracer/tracer_spec.rb index 20b5e2dc1c4..3bfaab5df94 100644 --- a/spec/ddtrace/opentracer/tracer_spec.rb +++ b/spec/ddtrace/opentracer/tracer_spec.rb @@ -78,21 +78,34 @@ 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 @@ -107,24 +120,39 @@ 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 OpenTracing::SpanContext::NOOP_INSTANCE } + 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 OpenTracing::SpanContext::NOOP_INSTANCE } + 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 OpenTracing::SpanContext::NOOP_INSTANCE } + it { is_expected.to be span_context } end context 'unknown' do