Skip to content

Commit f5d4398

Browse files
khanayan123claudeandrewlock
authored
feat: Stable Session ID headers for telemetry (#8352)
## Summary Adds stable session ID headers to telemetry requests per the [Stable Service Instance Identifier RFC](https://docs.google.com/document/d/1ECKj9_NnwaKYtFqm3p3Rlpicx5d-OQcdj9kI2jvRqVU/edit?usp=sharing). - **DD-Session-ID** (`runtime_id`) added to every telemetry request via default headers - **DD-Root-Session-ID** added only when inherited from a parent process (child process detection) - Root session ID propagated to child processes via `_DD_ROOT_DOTNET_SESSION_ID` env var (registered in config registry) - Root session ID initialized eagerly in `Instrumentation.InitializeNoNativeParts()` so child processes spawned before the first telemetry flush inherit it ### Files changed - `Instrumentation.cs` — Eagerly initializes root session ID at startup - `RuntimeId.cs` — `GetRootSessionId()` with env var inheritance and auto-propagation - `TelemetryConstants.cs` — Header name constants - `TelemetryHttpHeaderNames.cs` — Session headers added to default agent and intake headers - `supported-configurations.yaml` — Registers `_DD_ROOT_DOTNET_SESSION_ID` in config registry - `RuntimeIdTests.cs` — Tests for root session ID (default + inherited paths) ### Related PRs - System tests: DataDog/system-tests#6510 - Go SDK: DataDog/dd-trace-go#4574 - Java SDK: DataDog/dd-trace-java#10914 ## Test plan - [x] `SetsRequiredHeaders` validates DD-Session-ID present on all telemetry - [x] `RootSessionId_UsesRuntimeIdWhenNotInherited_AndInheritsWhenSet` covers both paths - [x] System tests validate cross-process session ID inheritance --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: Andrew Lock <andrew.lock@datadoghq.com>
1 parent 0b65644 commit f5d4398

13 files changed

Lines changed: 192 additions & 19 deletions

File tree

tracer/src/Datadog.Trace/ClrProfiler/Instrumentation.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,10 @@ internal static void InitializeNoNativeParts(ref RefStopwatch sw)
307307
}
308308
#endif
309309

310+
// Eagerly initialize the root session ID so child processes
311+
// inherit it even if spawned before the first telemetry flush.
312+
_ = RuntimeId.GetRootSessionId();
313+
310314
try
311315
{
312316
// ensure global instance is created if it's not already

tracer/src/Datadog.Trace/Configuration/supported-configurations.yaml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1580,6 +1580,15 @@ supportedConfigurations:
15801580
documentation: |-
15811581
Configuration key for whether telemetry metrics should be sent.
15821582
<see cref="Datadog.Trace.Telemetry.TelemetrySettings.MetricsEnabled"/>
1583+
_DD_ROOT_DOTNET_SESSION_ID:
1584+
- implementation: A
1585+
type: string
1586+
default: null
1587+
product: Telemetry
1588+
const_name: RootSessionId
1589+
documentation: |-
1590+
Internal env var for propagating the root session ID to child processes.
1591+
Set automatically by the tracer at init time; not user-configurable.
15831592
DD_TEST_MANAGEMENT_ATTEMPT_TO_FIX_RETRIES:
15841593
- implementation: A
15851594
type: int

tracer/src/Datadog.Trace/Generated/net461/Datadog.Trace.SourceGenerators/ConfigurationKeysGenerator/ConfigurationKeys.Telemetry.g.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,12 @@ internal static partial class ConfigurationKeys
1717
{
1818
internal static class Telemetry
1919
{
20+
/// <summary>
21+
/// Internal env var for propagating the root session ID to child processes.
22+
/// Set automatically by the tracer at init time; not user-configurable.
23+
/// </summary>
24+
public const string RootSessionId = "_DD_ROOT_DOTNET_SESSION_ID";
25+
2026
/// <summary>
2127
/// SSI variable that provides a unique identifier for the instrumentation installation.
2228
/// Used for tracking and correlation purposes in telemetry.

tracer/src/Datadog.Trace/Generated/net6.0/Datadog.Trace.SourceGenerators/ConfigurationKeysGenerator/ConfigurationKeys.Telemetry.g.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,12 @@ internal static partial class ConfigurationKeys
1717
{
1818
internal static class Telemetry
1919
{
20+
/// <summary>
21+
/// Internal env var for propagating the root session ID to child processes.
22+
/// Set automatically by the tracer at init time; not user-configurable.
23+
/// </summary>
24+
public const string RootSessionId = "_DD_ROOT_DOTNET_SESSION_ID";
25+
2026
/// <summary>
2127
/// SSI variable that provides a unique identifier for the instrumentation installation.
2228
/// Used for tracking and correlation purposes in telemetry.

tracer/src/Datadog.Trace/Generated/netcoreapp3.1/Datadog.Trace.SourceGenerators/ConfigurationKeysGenerator/ConfigurationKeys.Telemetry.g.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,12 @@ internal static partial class ConfigurationKeys
1717
{
1818
internal static class Telemetry
1919
{
20+
/// <summary>
21+
/// Internal env var for propagating the root session ID to child processes.
22+
/// Set automatically by the tracer at init time; not user-configurable.
23+
/// </summary>
24+
public const string RootSessionId = "_DD_ROOT_DOTNET_SESSION_ID";
25+
2026
/// <summary>
2127
/// SSI variable that provides a unique identifier for the instrumentation installation.
2228
/// Used for tracking and correlation purposes in telemetry.

tracer/src/Datadog.Trace/Generated/netstandard2.0/Datadog.Trace.SourceGenerators/ConfigurationKeysGenerator/ConfigurationKeys.Telemetry.g.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,12 @@ internal static partial class ConfigurationKeys
1717
{
1818
internal static class Telemetry
1919
{
20+
/// <summary>
21+
/// Internal env var for propagating the root session ID to child processes.
22+
/// Set automatically by the tracer at init time; not user-configurable.
23+
/// </summary>
24+
public const string RootSessionId = "_DD_ROOT_DOTNET_SESSION_ID";
25+
2026
/// <summary>
2127
/// SSI variable that provides a unique identifier for the instrumentation installation.
2228
/// Used for tracking and correlation purposes in telemetry.

tracer/src/Datadog.Trace/Telemetry/TelemetryConstants.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ internal static class TelemetryConstants
2121
public const string ClientLibraryLanguageHeader = "DD-Client-Library-Language";
2222
public const string ClientLibraryVersionHeader = "DD-Client-Library-Version";
2323

24+
public const string SessionIdHeader = "DD-Session-ID";
25+
public const string RootSessionIdHeader = "DD-Root-Session-ID";
26+
2427
public const string CloudProviderHeader = "DD-Cloud-Provider";
2528
public const string CloudResourceTypeHeader = "DD-Cloud-Resource-Type";
2629
public const string CloudResourceIdentifierHeader = "DD-Cloud-Resource-Identifier";
Lines changed: 65 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,56 +1,103 @@
1-
// <copyright file="TelemetryHttpHeaderNames.cs" company="Datadog">
1+
// <copyright file="TelemetryHttpHeaderNames.cs" company="Datadog">
22
// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License.
33
// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc.
44
// </copyright>
55

6+
using System;
67
using System.Collections.Generic;
8+
using System.Threading;
79
using Datadog.Trace.HttpOverStreams;
10+
using Datadog.Trace.Util;
811

912
namespace Datadog.Trace.Telemetry
1013
{
1114
internal static class TelemetryHttpHeaderNames
1215
{
16+
private static string _httpSerializedDefaultAgentHeaders;
17+
1318
/// <summary>
14-
/// Returns <see cref="GetDefaultAgentHeaders"/>, in the format <c>Key: Value\r\n</c>. For use in HTTP headers
19+
/// Gets the default agent headers in the format <c>Key: Value\r\n</c>. For use in HTTP headers.
20+
/// Lazily initialized because session headers depend on runtime values.
1521
/// </summary>
16-
internal const string HttpSerializedDefaultAgentHeaders =
17-
$"{TelemetryConstants.ClientLibraryLanguageHeader}: {TracerConstants.Language}" + DatadogHttpValues.CrLf +
18-
$"{TelemetryConstants.ClientLibraryVersionHeader}: {TracerConstants.AssemblyVersion}" + DatadogHttpValues.CrLf +
19-
$"{HttpHeaderNames.TracingEnabled}: false" + DatadogHttpValues.CrLf;
22+
internal static string HttpSerializedDefaultAgentHeaders =>
23+
LazyInitializer.EnsureInitialized(ref _httpSerializedDefaultAgentHeaders, BuildSerializedAgentHeaders);
2024

2125
/// <summary>
2226
/// Gets the default constant headers that should be added to any request to the agent
2327
/// </summary>
2428
internal static KeyValuePair<string, string>[] GetDefaultAgentHeaders()
25-
=>
26-
[
27-
new(TelemetryConstants.ClientLibraryLanguageHeader, TracerConstants.Language),
28-
new(TelemetryConstants.ClientLibraryVersionHeader, TracerConstants.AssemblyVersion),
29-
new(HttpHeaderNames.TracingEnabled, "false") // don't add automatic instrumentation to requests directed to the agent
30-
];
29+
{
30+
var sessionId = RuntimeId.Get();
31+
var rootSessionId = RuntimeId.GetRootSessionId();
32+
var includeRoot = rootSessionId != sessionId;
33+
var headerCount = includeRoot ? 5 : 4;
34+
35+
var headers = new KeyValuePair<string, string>[headerCount];
36+
headers[0] = new(TelemetryConstants.ClientLibraryLanguageHeader, TracerConstants.Language);
37+
headers[1] = new(TelemetryConstants.ClientLibraryVersionHeader, TracerConstants.AssemblyVersion);
38+
headers[2] = new(HttpHeaderNames.TracingEnabled, "false"); // don't add automatic instrumentation to requests directed to the agent
39+
headers[3] = new(TelemetryConstants.SessionIdHeader, sessionId);
40+
41+
if (includeRoot)
42+
{
43+
headers[4] = new(TelemetryConstants.RootSessionIdHeader, rootSessionId);
44+
}
45+
46+
return headers;
47+
}
3148

3249
/// <summary>
3350
/// Gets the default constant headers that should be added to any request to the direct telemetry intake
3451
/// </summary>
3552
internal static KeyValuePair<string, string>[] GetDefaultIntakeHeaders(TelemetrySettings.AgentlessSettings settings)
3653
{
37-
var headerCount = settings.Cloud is null ? 4 : 7;
54+
var sessionId = RuntimeId.Get();
55+
var rootSessionId = RuntimeId.GetRootSessionId();
56+
var includeRoot = rootSessionId != sessionId;
57+
var baseCount = settings.Cloud is null ? 5 : 8;
58+
var headerCount = includeRoot ? baseCount + 1 : baseCount;
3859

3960
var headers = new KeyValuePair<string, string>[headerCount];
40-
4161
headers[0] = new(TelemetryConstants.ClientLibraryLanguageHeader, TracerConstants.Language);
4262
headers[1] = new(TelemetryConstants.ClientLibraryVersionHeader, TracerConstants.AssemblyVersion);
43-
headers[2] = new(HttpHeaderNames.TracingEnabled, "false"); // don't add automatic instrumentation to requests directed to the agent
63+
headers[2] = new(HttpHeaderNames.TracingEnabled, "false");
4464
headers[3] = new(TelemetryConstants.ApiKeyHeader, settings.ApiKey);
65+
headers[4] = new(TelemetryConstants.SessionIdHeader, sessionId);
4566

67+
var index = 5;
4668
if (settings.Cloud is { } cloud)
4769
{
48-
headers[4] = new(TelemetryConstants.CloudProviderHeader, cloud.Provider);
49-
headers[5] = new(TelemetryConstants.CloudResourceTypeHeader, cloud.ResourceType);
50-
headers[6] = new(TelemetryConstants.CloudResourceIdentifierHeader, cloud.ResourceIdentifier);
70+
headers[index++] = new(TelemetryConstants.CloudProviderHeader, cloud.Provider);
71+
headers[index++] = new(TelemetryConstants.CloudResourceTypeHeader, cloud.ResourceType);
72+
headers[index++] = new(TelemetryConstants.CloudResourceIdentifierHeader, cloud.ResourceIdentifier);
73+
}
74+
75+
if (includeRoot)
76+
{
77+
headers[index] = new(TelemetryConstants.RootSessionIdHeader, rootSessionId);
5178
}
5279

5380
return headers;
5481
}
82+
83+
private static string BuildSerializedAgentHeaders()
84+
{
85+
var sessionId = RuntimeId.Get();
86+
var rootSessionId = RuntimeId.GetRootSessionId();
87+
88+
if (rootSessionId != sessionId)
89+
{
90+
return $"{TelemetryConstants.ClientLibraryLanguageHeader}: {TracerConstants.Language}" + DatadogHttpValues.CrLf +
91+
$"{TelemetryConstants.ClientLibraryVersionHeader}: {TracerConstants.AssemblyVersion}" + DatadogHttpValues.CrLf +
92+
$"{HttpHeaderNames.TracingEnabled}: false" + DatadogHttpValues.CrLf +
93+
$"{TelemetryConstants.SessionIdHeader}: {sessionId}" + DatadogHttpValues.CrLf +
94+
$"{TelemetryConstants.RootSessionIdHeader}: {rootSessionId}" + DatadogHttpValues.CrLf;
95+
}
96+
97+
return $"{TelemetryConstants.ClientLibraryLanguageHeader}: {TracerConstants.Language}" + DatadogHttpValues.CrLf +
98+
$"{TelemetryConstants.ClientLibraryVersionHeader}: {TracerConstants.AssemblyVersion}" + DatadogHttpValues.CrLf +
99+
$"{HttpHeaderNames.TracingEnabled}: false" + DatadogHttpValues.CrLf +
100+
$"{TelemetryConstants.SessionIdHeader}: {sessionId}" + DatadogHttpValues.CrLf;
101+
}
55102
}
56103
}

tracer/src/Datadog.Trace/Util/RuntimeId.cs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,27 @@
55

66
using System;
77
using System.Threading;
8+
using Datadog.Trace.Configuration;
9+
using Datadog.Trace.Configuration.Telemetry;
810
using Datadog.Trace.Logging;
11+
using Datadog.Trace.SourceGenerators;
12+
using Datadog.Trace.Telemetry;
913

1014
namespace Datadog.Trace.Util
1115
{
1216
internal static class RuntimeId
1317
{
1418
private static readonly IDatadogLogger Log = DatadogLogging.GetLoggerFor(typeof(RuntimeId));
1519
private static string _runtimeId;
20+
private static string _rootSessionId;
1621

1722
public static string Get() => LazyInitializer.EnsureInitialized(ref _runtimeId, () => GetImpl());
1823

24+
public static string GetRootSessionId() => LazyInitializer.EnsureInitialized(ref _rootSessionId, () => GetRootSessionIdImpl());
25+
26+
[TestingAndPrivateOnly]
27+
internal static void ResetForTests() => _rootSessionId = null;
28+
1929
private static string GetImpl()
2030
{
2131
if (NativeLoader.TryGetRuntimeIdFromNative(out var runtimeId))
@@ -29,5 +39,20 @@ private static string GetImpl()
2939

3040
return guid;
3141
}
42+
43+
private static string GetRootSessionIdImpl()
44+
{
45+
var config = new ConfigurationBuilder(new EnvironmentConfigurationSource(), TelemetryFactory.Config);
46+
var inherited = config.WithKeys(ConfigurationKeys.Telemetry.RootSessionId).AsString();
47+
if (!string.IsNullOrEmpty(inherited))
48+
{
49+
Log.Debug("Inherited root session ID from parent: {RootSessionId}", inherited);
50+
return inherited;
51+
}
52+
53+
var rootId = Get();
54+
EnvironmentHelpers.SetEnvironmentVariable(ConfigurationKeys.Telemetry.RootSessionId, rootId);
55+
return rootId;
56+
}
3257
}
3358
}

tracer/test/Datadog.Trace.IntegrationTests/TelemetryTransportTests.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
using Datadog.Trace.Telemetry;
1515
using Datadog.Trace.Telemetry.Transports;
1616
using Datadog.Trace.TestHelpers;
17+
using Datadog.Trace.Util;
1718
using FluentAssertions;
1819
using FluentAssertions.Execution;
1920
using Xunit;
@@ -116,6 +117,12 @@ public async Task SetsRequiredHeaders(bool agentless, bool useCloudAgentless)
116117
{ "DD-Client-Library-Version", TracerConstants.AssemblyVersion },
117118
};
118119

120+
// DD-Session-ID is always present and equals the runtime ID
121+
allExpected.Add(TelemetryConstants.SessionIdHeader, RuntimeId.Get());
122+
123+
// DD-Root-Session-ID is absent when rootSessionId == runtimeId (normal process)
124+
// We can't assert absence in the loop below, so we check it separately after
125+
119126
if (ContainerMetadata.Instance.ContainerId is { } containerId)
120127
{
121128
allExpected.Add(AgentHttpHeaderNames.ContainerId, containerId);
@@ -149,6 +156,12 @@ public async Task SetsRequiredHeaders(bool agentless, bool useCloudAgentless)
149156
}
150157
}
151158

159+
// DD-Root-Session-ID should be absent in a normal (non-child) process
160+
if (RuntimeId.GetRootSessionId() == RuntimeId.Get())
161+
{
162+
headers.AllKeys.Should().NotContain(TelemetryConstants.RootSessionIdHeader);
163+
}
164+
152165
// should have either content-length or chunked encoding
153166
headers.AllKeys.Should()
154167
.Contain(s => s.Equals("Content-Type", StringComparison.OrdinalIgnoreCase)

0 commit comments

Comments
 (0)