diff --git a/lib/ddtrace/opentracer/thread_local_scope_manager.rb b/lib/ddtrace/opentracer/thread_local_scope_manager.rb index 60a5acb0e4..04b42a170e 100644 --- a/lib/ddtrace/opentracer/thread_local_scope_manager.rb +++ b/lib/ddtrace/opentracer/thread_local_scope_manager.rb @@ -11,7 +11,11 @@ class ThreadLocalScopeManager < ScopeManager # 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).tap do |scope| + ThreadLocalScope.new( + manager: self, + span: span, + finish_on_close: finish_on_close + ).tap do |scope| set_scope(scope) end end diff --git a/lib/ddtrace/opentracer/tracer.rb b/lib/ddtrace/opentracer/tracer.rb index 9b7e07778d..fcd7547045 100644 --- a/lib/ddtrace/opentracer/tracer.rb +++ b/lib/ddtrace/opentracer/tracer.rb @@ -17,6 +17,154 @@ 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) + span = start_span( + operation_name, + child_of: child_of, + references: references, + start_time: start_time, + tags: tags, + ignore_active_scope: ignore_active_scope + ) + + 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) + # 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 + when Span + child_of.context + when SpanContext + child_of + else + ignore_active_scope ? nil : scope_manager.active && scope_manager.active.span.context + end + + # 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, OpenTracing::FORMAT_BINARY, OpenTracing::FORMAT_RACK + return nil + 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, OpenTracing::FORMAT_BINARY, OpenTracing::FORMAT_RACK + return SpanContext::NOOP_INSTANCE + else + warn 'Unknown extract format' + nil + 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 0000000000..38f9ed51c3 --- /dev/null +++ b/spec/ddtrace/opentracer/tracer_integration_spec.rb @@ -0,0 +1,308 @@ +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 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 + 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 + 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 } + 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 index 1a69e68afe..20b5e2dc1c 100644 --- a/spec/ddtrace/opentracer/tracer_spec.rb +++ b/spec/ddtrace/opentracer/tracer_spec.rb @@ -7,7 +7,7 @@ RSpec.describe Datadog::OpenTracer::Tracer do include_context 'OpenTracing helpers' - subject(:tracer) { described_class.new } + subject(:tracer) { described_class.new(writer: FauxWriter.new) } ### Datadog::OpenTracing::Tracer behavior ### @@ -48,17 +48,21 @@ describe '#scope_manager' do subject(:scope_manager) { tracer.scope_manager } - it { is_expected.to be(OpenTracing::ScopeManager::NOOP_INSTANCE) } + 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 OpenTracing::Scope::NOOP_INSTANCE } + it { is_expected.to be_a_kind_of(Datadog::OpenTracer::ThreadLocalScope) } context 'when a block is given' do - it { expect { |b| tracer.start_active_span(name, &b) }.to yield_with_args(OpenTracing::Scope::NOOP_INSTANCE) } + it do + expect { |b| tracer.start_active_span(name, &b) }.to yield_with_args( + a_kind_of(Datadog::OpenTracer::ThreadLocalScope) + ) + end end end @@ -66,7 +70,7 @@ subject(:span) { tracer.start_span(name) } let(:name) { 'opentracing_span' } - it { is_expected.to be OpenTracing::Span::NOOP_INSTANCE } + it { is_expected.to be_a_kind_of(Datadog::OpenTracer::Span) } end describe '#inject' do