From f4960a8aa5bf9055094361b1650624f15b639b35 Mon Sep 17 00:00:00 2001 From: dsedleckas Date: Wed, 22 May 2019 15:19:04 +0300 Subject: [PATCH] Implement baggage support for OpenTracing Spans --- .../HttpHeadersCodec.cs | 36 +++++++++++- .../OpenTracingHttpHeaderNames.cs | 13 +++++ .../OpenTracingSpan.cs | 12 ++-- .../OpenTracingSpanBuilder.cs | 19 ++++++- .../OpenTracingSpanContext.cs | 19 ++++++- .../HttpHeaderCodecTests.cs | 55 ++++++++++++++++++- .../OpenTracingTracerTests.cs | 47 ++++++++++++++++ 7 files changed, 191 insertions(+), 10 deletions(-) create mode 100644 src/Datadog.Trace.OpenTracing/OpenTracingHttpHeaderNames.cs diff --git a/src/Datadog.Trace.OpenTracing/HttpHeadersCodec.cs b/src/Datadog.Trace.OpenTracing/HttpHeadersCodec.cs index 2a5528c79327..5f0c4336fcd0 100644 --- a/src/Datadog.Trace.OpenTracing/HttpHeadersCodec.cs +++ b/src/Datadog.Trace.OpenTracing/HttpHeadersCodec.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Globalization; using Datadog.Trace.Headers; using OpenTracing.Propagation; @@ -20,7 +21,8 @@ internal class HttpHeadersCodec : ICodec IHeadersCollection headers = new TextMapHeadersCollection(map); var propagationContext = SpanContextPropagator.Instance.Extract(headers); - return new OpenTracingSpanContext(propagationContext); + var baggage = ExtractBaggage(map); + return new OpenTracingSpanContext(propagationContext, baggage); } public void Inject(global::OpenTracing.ISpanContext context, object carrier) @@ -33,6 +35,7 @@ public void Inject(global::OpenTracing.ISpanContext context, object carrier) } IHeadersCollection headers = new TextMapHeadersCollection(map); + InjectBaggage(context.GetBaggageItems(), headers); if (context is OpenTracingSpanContext otSpanContext && otSpanContext.Context is SpanContext ddSpanContext) { @@ -46,5 +49,36 @@ public void Inject(global::OpenTracing.ISpanContext context, object carrier) headers.Set(HttpHeaderNames.ParentId, context.SpanId.ToString(InvariantCulture)); } } + + private void InjectBaggage(IEnumerable> baggage, IHeadersCollection headers) + { + if (baggage == null) + { + return; + } + + foreach (var kv in baggage) + { + headers.Set($"{OpenTracingHttpHeaderNames.BaggagePrefix}{kv.Key}", kv.Value); + } + } + + private IEnumerable> ExtractBaggage(ITextMap headers) + { + if (headers == null) + { + yield break; + } + + foreach (var kv in headers) + { + if (kv.Key.StartsWith(OpenTracingHttpHeaderNames.BaggagePrefix, StringComparison.OrdinalIgnoreCase)) + { + yield return new KeyValuePair( + kv.Key.Substring(OpenTracingHttpHeaderNames.BaggagePrefix.Length), + kv.Value); + } + } + } } } diff --git a/src/Datadog.Trace.OpenTracing/OpenTracingHttpHeaderNames.cs b/src/Datadog.Trace.OpenTracing/OpenTracingHttpHeaderNames.cs new file mode 100644 index 000000000000..927aec8c5633 --- /dev/null +++ b/src/Datadog.Trace.OpenTracing/OpenTracingHttpHeaderNames.cs @@ -0,0 +1,13 @@ +namespace Datadog.Trace.OpenTracing +{ + /// + /// Open Tracing related http header names + /// + public static class OpenTracingHttpHeaderNames + { + /// + /// Http header suffix for propagated baggage items; + /// + public const string BaggagePrefix = "x-datadog-baggage-"; + } +} diff --git a/src/Datadog.Trace.OpenTracing/OpenTracingSpan.cs b/src/Datadog.Trace.OpenTracing/OpenTracingSpan.cs index 1278f983d6de..419d0326e322 100644 --- a/src/Datadog.Trace.OpenTracing/OpenTracingSpan.cs +++ b/src/Datadog.Trace.OpenTracing/OpenTracingSpan.cs @@ -8,10 +8,10 @@ namespace Datadog.Trace.OpenTracing { internal class OpenTracingSpan : ISpan { - internal OpenTracingSpan(Span span) + internal OpenTracingSpan(Span span, IEnumerable> baggage = null) { Span = span; - Context = new OpenTracingSpanContext(span.Context); + Context = new OpenTracingSpanContext(span.Context, baggage); } public OpenTracingSpanContext Context { get; } @@ -28,7 +28,7 @@ internal OpenTracingSpan(Span span) internal TimeSpan Duration => Span.Duration; - public string GetBaggageItem(string key) => null; + public string GetBaggageItem(string key) => Context?.GetBaggageItem(key); public ISpan Log(DateTimeOffset timestamp, IEnumerable> fields) => this; @@ -38,7 +38,11 @@ internal OpenTracingSpan(Span span) public ISpan Log(IEnumerable> fields) => this; - public ISpan SetBaggageItem(string key, string value) => this; + public ISpan SetBaggageItem(string key, string value) + { + Context?.SetBaggageItem(key, value); + return this; + } public ISpan SetOperationName(string operationName) { diff --git a/src/Datadog.Trace.OpenTracing/OpenTracingSpanBuilder.cs b/src/Datadog.Trace.OpenTracing/OpenTracingSpanBuilder.cs index eecb4961b118..796d219c167b 100644 --- a/src/Datadog.Trace.OpenTracing/OpenTracingSpanBuilder.cs +++ b/src/Datadog.Trace.OpenTracing/OpenTracingSpanBuilder.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Globalization; +using System.Linq; using Datadog.Trace.Logging; using OpenTracing; using OpenTracing.Tag; @@ -71,7 +72,9 @@ public ISpan Start() { ISpanContext parentContext = GetParentContext(); Span ddSpan = _tracer.DatadogTracer.StartSpan(_operationName, parentContext, _serviceName, _start, _ignoreActiveSpan); - var otSpan = new OpenTracingSpan(ddSpan); + + IEnumerable> parentBaggage = GetParentBaggage(); + var otSpan = new OpenTracingSpan(ddSpan, parentBaggage); if (_tags != null) { @@ -173,5 +176,19 @@ private ISpanContext GetParentContext() return parentContext; } + + private IEnumerable> GetParentBaggage() + { + var openTracingSpanContext = _parent as OpenTracingSpanContext; + var parentContext = openTracingSpanContext; + + if (parentContext == null && !_ignoreActiveSpan) + { + // if parent was not set explicitly, default to active span as parent (unless disabled) + return _tracer.ActiveSpan?.Context?.GetBaggageItems(); + } + + return parentContext?.GetBaggageItems(); + } } } diff --git a/src/Datadog.Trace.OpenTracing/OpenTracingSpanContext.cs b/src/Datadog.Trace.OpenTracing/OpenTracingSpanContext.cs index d023c29980df..66fabc7a5945 100644 --- a/src/Datadog.Trace.OpenTracing/OpenTracingSpanContext.cs +++ b/src/Datadog.Trace.OpenTracing/OpenTracingSpanContext.cs @@ -1,6 +1,6 @@ +using System.Collections.Concurrent; using System.Collections.Generic; using System.Globalization; -using System.Linq; using Datadog.Trace.Logging; namespace Datadog.Trace.OpenTracing @@ -9,9 +9,10 @@ internal class OpenTracingSpanContext : global::OpenTracing.ISpanContext { private static ILog _log = LogProvider.For(); - public OpenTracingSpanContext(ISpanContext context) + public OpenTracingSpanContext(ISpanContext context, IEnumerable> baggage = null) { Context = context; + Baggage = baggage != null ? new ConcurrentDictionary(baggage) : new ConcurrentDictionary(); } public string TraceId => Context.TraceId.ToString(CultureInfo.InvariantCulture); @@ -20,9 +21,21 @@ public OpenTracingSpanContext(ISpanContext context) internal ISpanContext Context { get; } + private ConcurrentDictionary Baggage { get; } + public IEnumerable> GetBaggageItems() { - return Enumerable.Empty>(); + return Baggage.ToArray(); + } + + public string GetBaggageItem(string key) + { + return Baggage.TryGetValue(key, out var value) ? value : null; + } + + public void SetBaggageItem(string key, string value) + { + Baggage.AddOrUpdate(key, value, (k, cmp) => value); } } } diff --git a/test/Datadog.Trace.OpenTracing.Tests/HttpHeaderCodecTests.cs b/test/Datadog.Trace.OpenTracing.Tests/HttpHeaderCodecTests.cs index 3a60cdec2668..36de786b3c1d 100644 --- a/test/Datadog.Trace.OpenTracing.Tests/HttpHeaderCodecTests.cs +++ b/test/Datadog.Trace.OpenTracing.Tests/HttpHeaderCodecTests.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using Xunit; namespace Datadog.Trace.OpenTracing.Tests @@ -9,6 +10,7 @@ public class HttpHeaderCodecTests private const string HttpHeaderTraceId = "x-datadog-trace-id"; private const string HttpHeaderParentId = "x-datadog-parent-id"; private const string HttpHeaderSamplingPriority = "x-datadog-sampling-priority"; + private const string HttpHeaderBaggagePrefix = "x-datadog-baggage-"; private readonly HttpHeadersCodec _codec = new HttpHeadersCodec(); @@ -29,6 +31,30 @@ public void Extract_ValidParentAndTraceId_ProperSpanContext() Assert.Equal(parentId, spanContext.Context.SpanId); } + [Fact] + public void Extract_ValidBaggageItems_ProperSpanContext() + { + var baggage = new KeyValuePair[] + { + new KeyValuePair("Potato", "Cannon"), + new KeyValuePair("Foo", "Bar") + }; + + var headers = new MockTextMap(); + foreach (var kv in baggage) + { + headers.Set($"{HttpHeaderBaggagePrefix}{kv.Key}", kv.Value); + } + + var spanContext = _codec.Extract(headers) as OpenTracingSpanContext; + + Assert.NotNull(spanContext); + foreach (var kv in baggage) + { + Assert.Equal(kv.Value, spanContext.GetBaggageItem(kv.Key)); + } + } + [Fact] public void Extract_WrongHeaderCase_ExtractionStillWorks() { @@ -36,16 +62,32 @@ public void Extract_WrongHeaderCase_ExtractionStillWorks() const ulong parentId = 120; const SamplingPriority samplingPriority = SamplingPriority.UserKeep; + var baggage = new KeyValuePair[] + { + new KeyValuePair("Potato", "Cannon"), + new KeyValuePair("Foo", "Bar") + }; + var headers = new MockTextMap(); headers.Set(HttpHeaderTraceId.ToUpper(), traceId.ToString()); headers.Set(HttpHeaderParentId.ToUpper(), parentId.ToString()); headers.Set(HttpHeaderSamplingPriority.ToUpper(), ((int)samplingPriority).ToString()); + foreach (var kv in baggage) + { + headers.Set($"{HttpHeaderBaggagePrefix.ToUpper()}{kv.Key}", kv.Value); + } + var spanContext = _codec.Extract(headers) as OpenTracingSpanContext; Assert.NotNull(spanContext); Assert.Equal(traceId, spanContext.Context.TraceId); Assert.Equal(parentId, spanContext.Context.SpanId); + + foreach (var kv in baggage) + { + Assert.Equal(kv.Value, spanContext.GetBaggageItem(kv.Key)); + } } [Fact] @@ -55,8 +97,14 @@ public void Inject_SpanContext_HeadersWithCorrectInfo() const ulong traceId = 7; const SamplingPriority samplingPriority = SamplingPriority.UserKeep; + var baggage = new KeyValuePair[] + { + new KeyValuePair("Potato", "Cannon"), + new KeyValuePair("Foo", "Bar") + }; + var ddSpanContext = new SpanContext(traceId, spanId, samplingPriority); - var spanContext = new OpenTracingSpanContext(ddSpanContext); + var spanContext = new OpenTracingSpanContext(ddSpanContext, baggage); var headers = new MockTextMap(); _codec.Inject(spanContext, headers); @@ -64,6 +112,11 @@ public void Inject_SpanContext_HeadersWithCorrectInfo() Assert.Equal(spanId.ToString(), headers.Get(HttpHeaderParentId)); Assert.Equal(traceId.ToString(), headers.Get(HttpHeaderTraceId)); Assert.Equal(((int)samplingPriority).ToString(), headers.Get(HttpHeaderSamplingPriority)); + + foreach (var kv in baggage) + { + Assert.Equal(kv.Value, headers.Get($"{HttpHeaderBaggagePrefix}{kv.Key}")); + } } } } diff --git a/test/Datadog.Trace.OpenTracing.Tests/OpenTracingTracerTests.cs b/test/Datadog.Trace.OpenTracing.Tests/OpenTracingTracerTests.cs index 25c855fd8c54..59c4dd17f8cb 100644 --- a/test/Datadog.Trace.OpenTracing.Tests/OpenTracingTracerTests.cs +++ b/test/Datadog.Trace.OpenTracing.Tests/OpenTracingTracerTests.cs @@ -40,9 +40,14 @@ public void BuildSpan_NoParameter_DefaultParameters() [Fact] public void BuildSpan_OneChild_ChildParentProperlySet() { + const string baggageKey = "BaggageKey"; + const string baggageValue = "BaggageValue"; IScope root = _tracer .BuildSpan("Root") .StartActive(finishSpanOnDispose: true); + + root.Span.SetBaggageItem(baggageKey, baggageValue); + IScope child = _tracer .BuildSpan("Child") .StartActive(finishSpanOnDispose: true); @@ -52,15 +57,21 @@ public void BuildSpan_OneChild_ChildParentProperlySet() Assert.Equal(rootDatadogSpan.Context.TraceContext, (ITraceContext)childDatadogSpan.Context.TraceContext); Assert.Equal(rootDatadogSpan.Context.SpanId, childDatadogSpan.Context.ParentId); + Assert.Equal(baggageValue, child.Span.GetBaggageItem(baggageKey)); } [Fact] public void BuildSpan_2ChildrenOfRoot_ChildrenParentProperlySet() { + const string baggageKey = "BaggageKey"; + const string baggageValue = "BaggageValue"; + IScope root = _tracer .BuildSpan("Root") .StartActive(finishSpanOnDispose: true); + root.Span.SetBaggageItem(baggageKey, baggageValue); + IScope child1 = _tracer .BuildSpan("Child1") .StartActive(finishSpanOnDispose: true); @@ -77,19 +88,34 @@ public void BuildSpan_2ChildrenOfRoot_ChildrenParentProperlySet() Assert.Same(rootDatadogSpan.Context.TraceContext, child1DatadogSpan.Context.TraceContext); Assert.Equal(rootDatadogSpan.Context.SpanId, child1DatadogSpan.Context.ParentId); + Assert.Equal(baggageValue, child1.Span.GetBaggageItem(baggageKey)); + Assert.Same(rootDatadogSpan.Context.TraceContext, child2DatadogSpan.Context.TraceContext); Assert.Equal(rootDatadogSpan.Context.SpanId, child2DatadogSpan.Context.ParentId); + Assert.Equal(baggageValue, child2.Span.GetBaggageItem(baggageKey)); } [Fact] public void BuildSpan_2LevelChildren_ChildrenParentProperlySet() { + const string baggageKeyRoot = "BaggageKeyRoot"; + const string baggageValueRoot = "BaggageValueRoot"; + + const string baggageKeyChild1 = "BaggageKeyChild1"; + const string baggageValueChild1 = "BaggageValueChild1"; + IScope root = _tracer .BuildSpan("Root") .StartActive(finishSpanOnDispose: true); + + root.Span.SetBaggageItem(baggageKeyRoot, baggageValueRoot); + IScope child1 = _tracer .BuildSpan("Child1") .StartActive(finishSpanOnDispose: true); + + child1.Span.SetBaggageItem(baggageKeyChild1, baggageValueChild1); + IScope child2 = _tracer .BuildSpan("Child2") .StartActive(finishSpanOnDispose: true); @@ -100,8 +126,13 @@ public void BuildSpan_2LevelChildren_ChildrenParentProperlySet() Assert.Same(rootDatadogSpan.Context.TraceContext, child1DatadogSpan.Context.TraceContext); Assert.Equal(rootDatadogSpan.Context.SpanId, child1DatadogSpan.Context.ParentId); + Assert.Equal(baggageValueRoot, child1.Span.GetBaggageItem(baggageKeyRoot)); + Assert.Equal(baggageValueChild1, child1.Span.GetBaggageItem(baggageKeyChild1)); + Assert.Same(rootDatadogSpan.Context.TraceContext, child2DatadogSpan.Context.TraceContext); Assert.Equal(child1DatadogSpan.Context.SpanId, child2DatadogSpan.Context.ParentId); + Assert.Equal(baggageValueRoot, child2.Span.GetBaggageItem(baggageKeyRoot)); + Assert.Equal(baggageValueChild1, child2.Span.GetBaggageItem(baggageKeyChild1)); } [Fact] @@ -109,10 +140,15 @@ public async Task BuildSpan_AsyncChildrenCreation_ChildrenParentProperlySet() { var tcs = new TaskCompletionSource(); + const string baggageKey = "BaggageKey"; + const string baggageValue = "BaggageValue"; + IScope root = _tracer .BuildSpan("Root") .StartActive(finishSpanOnDispose: true); + root.Span.SetBaggageItem(baggageKey, baggageValue); + Func> createSpanAsync = async (t) => { await tcs.Task; @@ -133,19 +169,25 @@ public async Task BuildSpan_AsyncChildrenCreation_ChildrenParentProperlySet() var span = await task; Assert.Equal(rootDatadogSpan.Context.TraceContext, (ITraceContext)span.DDSpan.Context.TraceContext); Assert.Equal(rootDatadogSpan.Context.SpanId, span.DDSpan.Context.ParentId); + Assert.Equal(baggageValue, span.GetBaggageItem(baggageKey)); } } [Fact] public void Inject_HttpHeadersFormat_CorrectHeaders() { + const string baggageKey = "BaggageKey"; + const string baggageValue = "BaggageValue"; + var span = (OpenTracingSpan)_tracer.BuildSpan("Span").Start(); + span.SetBaggageItem(baggageKey, baggageValue); var headers = new MockTextMap(); _tracer.Inject(span.Context, BuiltinFormats.HttpHeaders, headers); Assert.Equal(span.DDSpan.Context.TraceId.ToString(), headers.Get(HttpHeaderNames.TraceId)); Assert.Equal(span.DDSpan.Context.SpanId.ToString(), headers.Get(HttpHeaderNames.ParentId)); + Assert.Equal(span.GetBaggageItem(baggageKey), headers.Get($"{OpenTracingHttpHeaderNames.BaggagePrefix}{baggageKey}")); } [Fact] @@ -153,14 +195,19 @@ public void Extract_HeadersProperlySet_SpanContext() { const ulong parentId = 10; const ulong traceId = 42; + const string baggageKey = "BaggageKey"; + const string baggageValue = "BaggageValue"; + var headers = new MockTextMap(); headers.Set(HttpHeaderNames.ParentId, parentId.ToString()); headers.Set(HttpHeaderNames.TraceId, traceId.ToString()); + headers.Set($"{OpenTracingHttpHeaderNames.BaggagePrefix}{baggageKey}", baggageValue); var otSpanContext = (OpenTracingSpanContext)_tracer.Extract(BuiltinFormats.HttpHeaders, headers); Assert.Equal(parentId, otSpanContext.Context.SpanId); Assert.Equal(traceId, otSpanContext.Context.TraceId); + Assert.Equal(baggageValue, otSpanContext.GetBaggageItem(baggageKey)); } [Fact]