Skip to content

Commit 50c187c

Browse files
feat(baggage): adding baggage to span tags (#7020)
## Summary of changes This PR introduces automatic propagation of baggage key-value pairs as span tags, prefixed with baggage. The new configuration option `DD_TRACE_BAGGAGE_TAG_KEYS` allows customization of this feature. ## Reason for change See the related [RFC](https://docs.google.com/document/d/1ZsgAcQHKjaBxDmhHup0zqFz07WYiOrbBksHPoXJe6u4/edit?tab=t.0#heading=h.q69qzmye8dfb) for more details. ## Implementation details ## Test coverage ## Other details <!-- Fixes #{issue} --> <!-- ⚠️ Note: where possible, please obtain 2 approvals prior to merging. Unless CODEOWNERS specifies otherwise, for external teams it is typically best to have one review from a team member, and one review from apm-dotnet. Trivial changes do not require 2 reviews. --> --------- Co-authored-by: Zach Montoya <zach.montoya@datadoghq.com>
1 parent 2a16ebb commit 50c187c

File tree

92 files changed

+910
-20
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

92 files changed

+910
-20
lines changed

tracer/src/Datadog.Trace/AspNet/TracingHttpModule.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,21 @@ internal static void AddHeaderTagsFromHttpResponse(HttpContext httpContext, Scop
125125
}
126126
}
127127

128+
private static void AddBaggageTagsToSpan(ISpan span, Baggage baggage, Tracer tracer)
129+
{
130+
if (baggage != null)
131+
{
132+
try
133+
{
134+
tracer.TracerManager.SpanContextPropagator.AddBaggageToSpanAsTags(span, baggage, tracer.Settings.BaggageTagKeys);
135+
}
136+
catch (Exception ex)
137+
{
138+
Log.Error(ex, "Error adding baggage tags to span.");
139+
}
140+
}
141+
}
142+
128143
private void OnBeginRequest(object sender, EventArgs eventArgs)
129144
{
130145
Scope scope = null;
@@ -198,6 +213,8 @@ private void OnBeginRequest(object sender, EventArgs eventArgs)
198213
scope.Span.DecorateWebServerSpan(resourceName: resourceName, httpMethod, host, url, userAgent, tags);
199214
tracer.TracerManager.SpanContextPropagator.AddHeadersToSpanAsTags(scope.Span, headers, tracer.Settings.HeaderTags, defaultTagPrefix: SpanContextPropagator.HttpRequestHeadersTagPrefix);
200215

216+
AddBaggageTagsToSpan(scope.Span, extractedContext.Baggage, tracer);
217+
201218
if (tracer.Settings.IpHeaderEnabled || Security.Instance.AppsecEnabled)
202219
{
203220
Headers.Ip.RequestIpExtractor.AddIpToTags(httpRequest.UserHostAddress, httpRequest.IsSecureConnection, key => requestHeaders[key], tracer.Settings.IpHeader, tags);

tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/AspNet/AspNetMvcIntegration.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,21 @@ internal static class AspNetMvcIntegration
4040
private const IntegrationId IntegrationId = Configuration.IntegrationId.AspNetMvc;
4141
private static readonly IDatadogLogger Log = DatadogLogging.GetLoggerFor(typeof(AspNetMvcIntegration));
4242

43+
private static void AddBaggageTagsToSpan(ISpan span, Baggage baggage, Tracer tracer)
44+
{
45+
if (baggage != null)
46+
{
47+
try
48+
{
49+
tracer.TracerManager.SpanContextPropagator.AddBaggageToSpanAsTags(span, baggage, tracer.Settings.BaggageTagKeys);
50+
}
51+
catch (Exception ex)
52+
{
53+
Log.Error(ex, "Error adding baggage tags to span.");
54+
}
55+
}
56+
}
57+
4358
/// <summary>
4459
/// Creates a scope used to instrument an MVC action and populates some common details.
4560
/// </summary>
@@ -183,6 +198,8 @@ internal static Scope CreateScope(ControllerContextStruct controllerContext)
183198
tracer.TracerManager.SpanContextPropagator.AddHeadersToSpanAsTags(span, headers.Value, tracer.Settings.HeaderTags, SpanContextPropagator.HttpRequestHeadersTagPrefix);
184199
}
185200

201+
AddBaggageTagsToSpan(span, extractedContext.Baggage, tracer);
202+
186203
if (tracer.Settings.IpHeaderEnabled || Security.Instance.AppsecEnabled)
187204
{
188205
Headers.Ip.RequestIpExtractor.AddIpToTags(httpContext.Request.UserHostAddress, httpContext.Request.IsSecureConnection, key => httpContext.Request.Headers[key], tracer.Settings.IpHeader, tags);

tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/AspNet/AspNetWebApi2Integration.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,21 @@ internal static class AspNetWebApi2Integration
3333
private const IntegrationId IntegrationId = Configuration.IntegrationId.AspNetWebApi2;
3434
private static readonly IDatadogLogger Log = DatadogLogging.GetLoggerFor(typeof(AspNetWebApi2Integration));
3535

36+
private static void AddBaggageTagsToSpan(ISpan span, Baggage baggage, Tracer tracer)
37+
{
38+
if (baggage != null)
39+
{
40+
try
41+
{
42+
tracer.TracerManager.SpanContextPropagator.AddBaggageToSpanAsTags(span, baggage, tracer.Settings.BaggageTagKeys);
43+
}
44+
catch (Exception ex)
45+
{
46+
Log.Error(ex, "Error adding baggage tags to span.");
47+
}
48+
}
49+
}
50+
3651
internal static Scope CreateScope(IHttpControllerContext controllerContext, out AspNetTags tags)
3752
{
3853
Scope scope = null;
@@ -104,6 +119,8 @@ internal static Scope CreateScope(IHttpControllerContext controllerContext, out
104119
tracer.TracerManager.SpanContextPropagator.AddHeadersToSpanAsTags(scope.Span, headersCollection.Value, tracer.Settings.HeaderTags, SpanContextPropagator.HttpRequestHeadersTagPrefix, request.Headers.UserAgent.ToString());
105120
}
106121

122+
AddBaggageTagsToSpan(scope.Span, extractedContext.Baggage, tracer);
123+
107124
tags.SetAnalyticsSampleRate(IntegrationId, tracer.Settings, enabledWithGlobalSetting: true);
108125
tracer.TracerManager.Telemetry.IntegrationGeneratedSpan(IntegrationId);
109126
}

tracer/src/Datadog.Trace/Configuration/ConfigurationKeys.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -467,6 +467,18 @@ internal static partial class ConfigurationKeys
467467
/// <seealso cref="TracerSettings.BaggageMaximumBytes"/>
468468
public const string BaggageMaximumBytes = "DD_TRACE_BAGGAGE_MAX_BYTES";
469469

470+
/// <summary>
471+
/// Configuration key for controlling which baggage keys are converted into span tags.
472+
/// Default value is "user.id,session.id,account.id".
473+
///
474+
/// Behavior options:
475+
/// - Empty string: No baggage keys are converted into span tags (feature disabled)
476+
/// - Comma-separated list: Only baggage keys matching exact, case-sensitive names in the list are added as span tags
477+
/// - Wildcard (*): All baggage keys are converted into span tags
478+
/// </summary>
479+
/// <seealso cref="TracerSettings.BaggageTagKeys"/>
480+
public const string BaggageTagKeys = "DD_TRACE_BAGGAGE_TAG_KEYS";
481+
470482
/// <summary>
471483
/// Configuration key for enabling automatic instrumentation on specified methods.
472484
/// Default value is "" (disabled).

tracer/src/Datadog.Trace/Configuration/TracerSettings.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -637,6 +637,11 @@ _ when x.ToBoolean() is { } boolean => boolean,
637637
.WithKeys(ConfigurationKeys.BaggageMaximumBytes)
638638
.AsInt32(defaultValue: W3CBaggagePropagator.DefaultMaximumBaggageBytes);
639639

640+
BaggageTagKeys = config
641+
.WithKeys(ConfigurationKeys.BaggageTagKeys)
642+
.AsString(defaultValue: "user.id,session.id,account.id")
643+
?.Split([','], StringSplitOptions.RemoveEmptyEntries) ?? [];
644+
640645
LogSubmissionSettings = new DirectLogSubmissionSettings(source, _telemetry);
641646

642647
TraceMethods = config
@@ -1146,6 +1151,13 @@ public bool DiagnosticSourceEnabled
11461151
/// <seealso cref="ConfigurationKeys.BaggageMaximumBytes"/>
11471152
internal int BaggageMaximumBytes { get; }
11481153

1154+
/// <summary>
1155+
/// Gets the configuration for which baggage keys are converted into span tags.
1156+
/// Default value is "user.id,session.id,account.id".
1157+
/// </summary>
1158+
/// <seealso cref="ConfigurationKeys.BaggageTagKeys"/>
1159+
internal string[] BaggageTagKeys { get; }
1160+
11491161
/// <summary>
11501162
/// Gets a value indicating whether runtime metrics
11511163
/// are enabled and sent to DogStatsd.

tracer/src/Datadog.Trace/PlatformHelpers/AspNetCoreHttpRequestHandler.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,21 @@ private void AddHeaderTagsToSpan(ISpan span, HttpRequest request, Tracer tracer)
9898
}
9999
}
100100

101+
private void AddBaggageTagsToSpan(ISpan span, Baggage baggage, Tracer tracer)
102+
{
103+
if (baggage != null)
104+
{
105+
try
106+
{
107+
tracer.TracerManager.SpanContextPropagator.AddBaggageToSpanAsTags(span, baggage, tracer.Settings.BaggageTagKeys);
108+
}
109+
catch (Exception ex)
110+
{
111+
_log.Error(ex, "Error adding baggage tags to span.");
112+
}
113+
}
114+
}
115+
101116
public Scope StartAspNetCorePipelineScope(Tracer tracer, Security security, HttpContext httpContext, string resourceName)
102117
{
103118
var request = httpContext.Request;
@@ -125,6 +140,7 @@ public Scope StartAspNetCorePipelineScope(Tracer tracer, Security security, Http
125140
var scope = tracer.StartActiveInternal(_requestInOperationName, extractedContext.SpanContext, tags: tags, links: extractedContext.Links);
126141
scope.Span.DecorateWebServerSpan(resourceName, httpMethod, host, url, userAgent, tags);
127142
AddHeaderTagsToSpan(scope.Span, request, tracer);
143+
AddBaggageTagsToSpan(scope.Span, extractedContext.Baggage, tracer);
128144

129145
var originalPath = request.PathBase.HasValue ? request.PathBase.Add(request.Path) : request.Path;
130146
var requestTrackingFeature = new RequestTrackingFeature(originalPath, scope);

tracer/src/Datadog.Trace/Propagators/SpanContextPropagator.cs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,40 @@ internal void ExtractHeaderTags<THeaders, TProcessor>(ref TProcessor processor,
310310
}
311311
}
312312

313+
internal void AddBaggageToSpanAsTags(ISpan span, Baggage? baggage, string[] baggageTagKeys)
314+
{
315+
if (baggage is null or { Count: 0 })
316+
{
317+
return;
318+
}
319+
320+
if (baggageTagKeys.Length == 0)
321+
{
322+
// feature disabled
323+
return;
324+
}
325+
326+
if (baggageTagKeys.Length == 1 && baggageTagKeys[0] == "*")
327+
{
328+
// add all baggage items as tags
329+
foreach (var item in baggage)
330+
{
331+
span.SetTag("baggage." + item.Key, item.Value);
332+
}
333+
334+
return;
335+
}
336+
337+
// add only specified baggage items as tags
338+
foreach (var key in baggageTagKeys)
339+
{
340+
if (baggage.TryGetValue(key, out var value))
341+
{
342+
span.SetTag("baggage." + key, value);
343+
}
344+
}
345+
}
346+
313347
#pragma warning disable SA1201
314348
public interface IHeaderTagProcessor
315349
#pragma warning restore SA1201

tracer/test/Datadog.Trace.ClrProfiler.IntegrationTests/AspNet/AspNetMvc5Tests.cs

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
#pragma warning disable SA1649 // File name must match first type name
99

1010
using System;
11+
using System.Collections.Generic;
1112
using System.Globalization;
1213
using System.Linq;
1314
using System.Net;
@@ -173,6 +174,113 @@ public AspNetMvc5TestsInferredProxySpansDisabled(IisFixture iisFixture, ITestOut
173174
}
174175
}
175176

177+
[UsesVerify]
178+
public abstract class AspNetMvc5TestsWithBaggage : TracingIntegrationTest, IClassFixture<IisFixture>, IAsyncLifetime
179+
{
180+
private readonly IisFixture _iisFixture;
181+
private readonly string _testName;
182+
private readonly bool _classicMode;
183+
private readonly bool _enableInferredProxySpans;
184+
185+
protected AspNetMvc5TestsWithBaggage(IisFixture iisFixture, ITestOutputHelper output)
186+
: base("AspNetMvc5", @"test\test-applications\aspnet", output)
187+
{
188+
SetServiceVersion("1.0.0");
189+
SetEnvironmentVariable(ConfigurationKeys.FeatureFlags.RouteTemplateResourceNamesEnabled, true.ToString());
190+
SetEnvironmentVariable(ConfigurationKeys.ExpandRouteTemplatesEnabled, false.ToString());
191+
SetEnvironmentVariable(ConfigurationKeys.FeatureFlags.TraceId128BitGenerationEnabled, false.ToString());
192+
SetEnvironmentVariable(ConfigurationKeys.FeatureFlags.InferredProxySpansEnabled, false.ToString());
193+
194+
_classicMode = false;
195+
_enableInferredProxySpans = false;
196+
_iisFixture = iisFixture;
197+
_iisFixture.ShutdownPath = "/home/shutdown";
198+
199+
_testName = nameof(AspNetMvc5Tests)
200+
+ ".Integrated" // _classicMode = false
201+
+ ".WithFF" // enableRouteTemplateResourceNames = true, enableRouteTemplateExpansion = false
202+
+ ".WithBaggage";
203+
}
204+
205+
public static TheoryData<string, int> Data => new()
206+
{
207+
{ "/", 200 },
208+
{ "/Home/Index", 200 },
209+
{ "/badrequest", 500 },
210+
};
211+
212+
protected virtual string ExpectedServiceName => "sample";
213+
214+
public override Result ValidateIntegrationSpan(MockSpan span, string metadataSchemaVersion) =>
215+
span.Name switch
216+
{
217+
"aspnet.request" => span.IsAspNet(metadataSchemaVersion, excludeTags: new HashSet<string> { "baggage.user.id" }),
218+
"aspnet-mvc.request" => span.IsAspNetMvc(metadataSchemaVersion, excludeTags: new HashSet<string> { "baggage.user.id" }),
219+
_ => Result.DefaultSuccess,
220+
};
221+
222+
[SkippableTheory]
223+
[Trait("Category", "EndToEnd")]
224+
[Trait("RunOnWindows", "True")]
225+
[Trait("LoadFromGAC", "True")]
226+
[MemberData(nameof(Data))]
227+
public async Task BaggageInSpanTags(string path, HttpStatusCode statusCode)
228+
{
229+
// TransferRequest cannot be called in the classic mode, so we expect a 500 when this happens
230+
var toLowerPath = path.ToLower();
231+
if (_testName.Contains(".Classic") && toLowerPath.Contains("badrequest") && toLowerPath.Contains("transferrequest"))
232+
{
233+
statusCode = (HttpStatusCode)500;
234+
}
235+
236+
var expectedSpanCount = _enableInferredProxySpans ? 3 : 2;
237+
238+
var spans = await GetWebServerSpans(
239+
path: _iisFixture.VirtualApplicationPath + path, // Append virtual directory to the actual request
240+
agent: _iisFixture.Agent,
241+
httpPort: _iisFixture.HttpPort,
242+
expectedHttpStatusCode: statusCode,
243+
expectedSpanCount: expectedSpanCount,
244+
filterServerSpans: !_enableInferredProxySpans);
245+
246+
var serverSpans = spans.Where(s => s.Tags.GetValueOrDefault(Tags.SpanKind) == SpanKinds.Server);
247+
ValidateIntegrationSpans(serverSpans, metadataSchemaVersion: "v0", expectedServiceName: ExpectedServiceName, isExternalSpan: false);
248+
249+
var sanitisedPath = VerifyHelper.SanitisePathsForVerify(path);
250+
var settings = VerifyHelper.GetSpanVerifierSettings(sanitisedPath, (int)statusCode);
251+
252+
await Verifier.Verify(spans, settings)
253+
.UseMethodName("_withBaggage")
254+
.UseTypeName(_testName);
255+
}
256+
257+
public async Task InitializeAsync()
258+
{
259+
await _iisFixture.TryStartIis(this, _classicMode ? IisAppType.AspNetClassic : IisAppType.AspNetIntegrated);
260+
}
261+
262+
public Task DisposeAsync() => Task.CompletedTask;
263+
264+
/// <summary>
265+
/// Override <see cref="CreateHttpRequestMessage"/> to add baggage headers to the request.
266+
/// </summary>
267+
protected override HttpRequestMessage CreateHttpRequestMessage(HttpMethod method, string path, DateTimeOffset testStart)
268+
{
269+
var request = base.CreateHttpRequestMessage(method, path, testStart);
270+
request.Headers.Add("baggage", "user.id=doggo");
271+
return request;
272+
}
273+
}
274+
275+
[Collection("IisTests")]
276+
public class AspNetMvc5TestsWithBaggageEnabled : AspNetMvc5TestsWithBaggage
277+
{
278+
public AspNetMvc5TestsWithBaggageEnabled(IisFixture iisFixture, ITestOutputHelper output)
279+
: base(iisFixture, output)
280+
{
281+
}
282+
}
283+
176284
[UsesVerify]
177285
public abstract class AspNetMvc5Tests : TracingIntegrationTest, IClassFixture<IisFixture>, IAsyncLifetime
178286
{

tracer/test/Datadog.Trace.ClrProfiler.IntegrationTests/AspNet/OwinWebApi2Tests.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
#pragma warning disable SA1649 // File name must match first type name
99

1010
using System;
11+
using System.Collections.Generic;
1112
using System.Collections.Immutable;
1213
using System.Diagnostics;
1314
using System.Linq;
@@ -112,7 +113,7 @@ public OwinWebApi2Tests(OwinFixture fixture, ITestOutputHelper output, bool enab
112113
public override Result ValidateIntegrationSpan(MockSpan span, string metadataSchemaVersion) =>
113114
span.Name switch
114115
{
115-
"aspnet-webapi.request" => span.IsAspNetWebApi2(metadataSchemaVersion),
116+
"aspnet-webapi.request" => span.IsAspNetWebApi2(metadataSchemaVersion, excludeTags: new HashSet<string> { "baggage.user.id" }),
116117
_ => Result.DefaultSuccess,
117118
};
118119

@@ -147,6 +148,7 @@ public OwinFixture()
147148
_httpClient = new HttpClient();
148149
_httpClient.DefaultRequestHeaders.Add(HttpHeaderNames.TracingEnabled, "false");
149150
_httpClient.DefaultRequestHeaders.Add(HttpHeaderNames.UserAgent, "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.69 Safari/537.36");
151+
_httpClient.DefaultRequestHeaders.Add("baggage", "user.id=doggo");
150152
}
151153

152154
public MockTracerAgent.TcpUdpAgent Agent { get; private set; }

0 commit comments

Comments
 (0)