diff --git a/tracer/src/Datadog.Trace/ClrProfiler/Instrumentation.cs b/tracer/src/Datadog.Trace/ClrProfiler/Instrumentation.cs
index ccaded8513d7..3f32a82f6567 100644
--- a/tracer/src/Datadog.Trace/ClrProfiler/Instrumentation.cs
+++ b/tracer/src/Datadog.Trace/ClrProfiler/Instrumentation.cs
@@ -307,6 +307,10 @@ internal static void InitializeNoNativeParts(ref RefStopwatch sw)
}
#endif
+ // Eagerly initialize the root session ID so child processes
+ // inherit it even if spawned before the first telemetry flush.
+ _ = RuntimeId.GetRootSessionId();
+
try
{
// ensure global instance is created if it's not already
diff --git a/tracer/src/Datadog.Trace/Configuration/supported-configurations.yaml b/tracer/src/Datadog.Trace/Configuration/supported-configurations.yaml
index aa637d4bacd7..480080c9f001 100644
--- a/tracer/src/Datadog.Trace/Configuration/supported-configurations.yaml
+++ b/tracer/src/Datadog.Trace/Configuration/supported-configurations.yaml
@@ -1580,6 +1580,15 @@ supportedConfigurations:
documentation: |-
Configuration key for whether telemetry metrics should be sent.
+ _DD_ROOT_DOTNET_SESSION_ID:
+ - implementation: A
+ type: string
+ default: null
+ product: Telemetry
+ const_name: RootSessionId
+ documentation: |-
+ Internal env var for propagating the root session ID to child processes.
+ Set automatically by the tracer at init time; not user-configurable.
DD_TEST_MANAGEMENT_ATTEMPT_TO_FIX_RETRIES:
- implementation: A
type: int
diff --git a/tracer/src/Datadog.Trace/Generated/net461/Datadog.Trace.SourceGenerators/ConfigurationKeysGenerator/ConfigurationKeys.Telemetry.g.cs b/tracer/src/Datadog.Trace/Generated/net461/Datadog.Trace.SourceGenerators/ConfigurationKeysGenerator/ConfigurationKeys.Telemetry.g.cs
index b62c847ae1be..2a3de1cdbd1b 100644
--- a/tracer/src/Datadog.Trace/Generated/net461/Datadog.Trace.SourceGenerators/ConfigurationKeysGenerator/ConfigurationKeys.Telemetry.g.cs
+++ b/tracer/src/Datadog.Trace/Generated/net461/Datadog.Trace.SourceGenerators/ConfigurationKeysGenerator/ConfigurationKeys.Telemetry.g.cs
@@ -17,6 +17,12 @@ internal static partial class ConfigurationKeys
{
internal static class Telemetry
{
+ ///
+ /// Internal env var for propagating the root session ID to child processes.
+ /// Set automatically by the tracer at init time; not user-configurable.
+ ///
+ public const string RootSessionId = "_DD_ROOT_DOTNET_SESSION_ID";
+
///
/// SSI variable that provides a unique identifier for the instrumentation installation.
/// Used for tracking and correlation purposes in telemetry.
diff --git a/tracer/src/Datadog.Trace/Generated/net6.0/Datadog.Trace.SourceGenerators/ConfigurationKeysGenerator/ConfigurationKeys.Telemetry.g.cs b/tracer/src/Datadog.Trace/Generated/net6.0/Datadog.Trace.SourceGenerators/ConfigurationKeysGenerator/ConfigurationKeys.Telemetry.g.cs
index b62c847ae1be..2a3de1cdbd1b 100644
--- a/tracer/src/Datadog.Trace/Generated/net6.0/Datadog.Trace.SourceGenerators/ConfigurationKeysGenerator/ConfigurationKeys.Telemetry.g.cs
+++ b/tracer/src/Datadog.Trace/Generated/net6.0/Datadog.Trace.SourceGenerators/ConfigurationKeysGenerator/ConfigurationKeys.Telemetry.g.cs
@@ -17,6 +17,12 @@ internal static partial class ConfigurationKeys
{
internal static class Telemetry
{
+ ///
+ /// Internal env var for propagating the root session ID to child processes.
+ /// Set automatically by the tracer at init time; not user-configurable.
+ ///
+ public const string RootSessionId = "_DD_ROOT_DOTNET_SESSION_ID";
+
///
/// SSI variable that provides a unique identifier for the instrumentation installation.
/// Used for tracking and correlation purposes in telemetry.
diff --git a/tracer/src/Datadog.Trace/Generated/netcoreapp3.1/Datadog.Trace.SourceGenerators/ConfigurationKeysGenerator/ConfigurationKeys.Telemetry.g.cs b/tracer/src/Datadog.Trace/Generated/netcoreapp3.1/Datadog.Trace.SourceGenerators/ConfigurationKeysGenerator/ConfigurationKeys.Telemetry.g.cs
index b62c847ae1be..2a3de1cdbd1b 100644
--- a/tracer/src/Datadog.Trace/Generated/netcoreapp3.1/Datadog.Trace.SourceGenerators/ConfigurationKeysGenerator/ConfigurationKeys.Telemetry.g.cs
+++ b/tracer/src/Datadog.Trace/Generated/netcoreapp3.1/Datadog.Trace.SourceGenerators/ConfigurationKeysGenerator/ConfigurationKeys.Telemetry.g.cs
@@ -17,6 +17,12 @@ internal static partial class ConfigurationKeys
{
internal static class Telemetry
{
+ ///
+ /// Internal env var for propagating the root session ID to child processes.
+ /// Set automatically by the tracer at init time; not user-configurable.
+ ///
+ public const string RootSessionId = "_DD_ROOT_DOTNET_SESSION_ID";
+
///
/// SSI variable that provides a unique identifier for the instrumentation installation.
/// Used for tracking and correlation purposes in telemetry.
diff --git a/tracer/src/Datadog.Trace/Generated/netstandard2.0/Datadog.Trace.SourceGenerators/ConfigurationKeysGenerator/ConfigurationKeys.Telemetry.g.cs b/tracer/src/Datadog.Trace/Generated/netstandard2.0/Datadog.Trace.SourceGenerators/ConfigurationKeysGenerator/ConfigurationKeys.Telemetry.g.cs
index b62c847ae1be..2a3de1cdbd1b 100644
--- a/tracer/src/Datadog.Trace/Generated/netstandard2.0/Datadog.Trace.SourceGenerators/ConfigurationKeysGenerator/ConfigurationKeys.Telemetry.g.cs
+++ b/tracer/src/Datadog.Trace/Generated/netstandard2.0/Datadog.Trace.SourceGenerators/ConfigurationKeysGenerator/ConfigurationKeys.Telemetry.g.cs
@@ -17,6 +17,12 @@ internal static partial class ConfigurationKeys
{
internal static class Telemetry
{
+ ///
+ /// Internal env var for propagating the root session ID to child processes.
+ /// Set automatically by the tracer at init time; not user-configurable.
+ ///
+ public const string RootSessionId = "_DD_ROOT_DOTNET_SESSION_ID";
+
///
/// SSI variable that provides a unique identifier for the instrumentation installation.
/// Used for tracking and correlation purposes in telemetry.
diff --git a/tracer/src/Datadog.Trace/Telemetry/TelemetryConstants.cs b/tracer/src/Datadog.Trace/Telemetry/TelemetryConstants.cs
index 1dceed6c98b9..4ac0c7d26120 100644
--- a/tracer/src/Datadog.Trace/Telemetry/TelemetryConstants.cs
+++ b/tracer/src/Datadog.Trace/Telemetry/TelemetryConstants.cs
@@ -21,6 +21,9 @@ internal static class TelemetryConstants
public const string ClientLibraryLanguageHeader = "DD-Client-Library-Language";
public const string ClientLibraryVersionHeader = "DD-Client-Library-Version";
+ public const string SessionIdHeader = "DD-Session-ID";
+ public const string RootSessionIdHeader = "DD-Root-Session-ID";
+
public const string CloudProviderHeader = "DD-Cloud-Provider";
public const string CloudResourceTypeHeader = "DD-Cloud-Resource-Type";
public const string CloudResourceIdentifierHeader = "DD-Cloud-Resource-Identifier";
diff --git a/tracer/src/Datadog.Trace/Telemetry/TelemetryHttpHeaderNames.cs b/tracer/src/Datadog.Trace/Telemetry/TelemetryHttpHeaderNames.cs
index ed54b9e6143a..bab5fd8c073c 100644
--- a/tracer/src/Datadog.Trace/Telemetry/TelemetryHttpHeaderNames.cs
+++ b/tracer/src/Datadog.Trace/Telemetry/TelemetryHttpHeaderNames.cs
@@ -1,56 +1,103 @@
-//
+//
// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License.
// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc.
//
+using System;
using System.Collections.Generic;
+using System.Threading;
using Datadog.Trace.HttpOverStreams;
+using Datadog.Trace.Util;
namespace Datadog.Trace.Telemetry
{
internal static class TelemetryHttpHeaderNames
{
+ private static string _httpSerializedDefaultAgentHeaders;
+
///
- /// Returns , in the format Key: Value\r\n. For use in HTTP headers
+ /// Gets the default agent headers in the format Key: Value\r\n. For use in HTTP headers.
+ /// Lazily initialized because session headers depend on runtime values.
///
- internal const string HttpSerializedDefaultAgentHeaders =
- $"{TelemetryConstants.ClientLibraryLanguageHeader}: {TracerConstants.Language}" + DatadogHttpValues.CrLf +
- $"{TelemetryConstants.ClientLibraryVersionHeader}: {TracerConstants.AssemblyVersion}" + DatadogHttpValues.CrLf +
- $"{HttpHeaderNames.TracingEnabled}: false" + DatadogHttpValues.CrLf;
+ internal static string HttpSerializedDefaultAgentHeaders =>
+ LazyInitializer.EnsureInitialized(ref _httpSerializedDefaultAgentHeaders, BuildSerializedAgentHeaders);
///
/// Gets the default constant headers that should be added to any request to the agent
///
internal static KeyValuePair[] GetDefaultAgentHeaders()
- =>
- [
- new(TelemetryConstants.ClientLibraryLanguageHeader, TracerConstants.Language),
- new(TelemetryConstants.ClientLibraryVersionHeader, TracerConstants.AssemblyVersion),
- new(HttpHeaderNames.TracingEnabled, "false") // don't add automatic instrumentation to requests directed to the agent
- ];
+ {
+ var sessionId = RuntimeId.Get();
+ var rootSessionId = RuntimeId.GetRootSessionId();
+ var includeRoot = rootSessionId != sessionId;
+ var headerCount = includeRoot ? 5 : 4;
+
+ var headers = new KeyValuePair[headerCount];
+ headers[0] = new(TelemetryConstants.ClientLibraryLanguageHeader, TracerConstants.Language);
+ headers[1] = new(TelemetryConstants.ClientLibraryVersionHeader, TracerConstants.AssemblyVersion);
+ headers[2] = new(HttpHeaderNames.TracingEnabled, "false"); // don't add automatic instrumentation to requests directed to the agent
+ headers[3] = new(TelemetryConstants.SessionIdHeader, sessionId);
+
+ if (includeRoot)
+ {
+ headers[4] = new(TelemetryConstants.RootSessionIdHeader, rootSessionId);
+ }
+
+ return headers;
+ }
///
/// Gets the default constant headers that should be added to any request to the direct telemetry intake
///
internal static KeyValuePair[] GetDefaultIntakeHeaders(TelemetrySettings.AgentlessSettings settings)
{
- var headerCount = settings.Cloud is null ? 4 : 7;
+ var sessionId = RuntimeId.Get();
+ var rootSessionId = RuntimeId.GetRootSessionId();
+ var includeRoot = rootSessionId != sessionId;
+ var baseCount = settings.Cloud is null ? 5 : 8;
+ var headerCount = includeRoot ? baseCount + 1 : baseCount;
var headers = new KeyValuePair[headerCount];
-
headers[0] = new(TelemetryConstants.ClientLibraryLanguageHeader, TracerConstants.Language);
headers[1] = new(TelemetryConstants.ClientLibraryVersionHeader, TracerConstants.AssemblyVersion);
- headers[2] = new(HttpHeaderNames.TracingEnabled, "false"); // don't add automatic instrumentation to requests directed to the agent
+ headers[2] = new(HttpHeaderNames.TracingEnabled, "false");
headers[3] = new(TelemetryConstants.ApiKeyHeader, settings.ApiKey);
+ headers[4] = new(TelemetryConstants.SessionIdHeader, sessionId);
+ var index = 5;
if (settings.Cloud is { } cloud)
{
- headers[4] = new(TelemetryConstants.CloudProviderHeader, cloud.Provider);
- headers[5] = new(TelemetryConstants.CloudResourceTypeHeader, cloud.ResourceType);
- headers[6] = new(TelemetryConstants.CloudResourceIdentifierHeader, cloud.ResourceIdentifier);
+ headers[index++] = new(TelemetryConstants.CloudProviderHeader, cloud.Provider);
+ headers[index++] = new(TelemetryConstants.CloudResourceTypeHeader, cloud.ResourceType);
+ headers[index++] = new(TelemetryConstants.CloudResourceIdentifierHeader, cloud.ResourceIdentifier);
+ }
+
+ if (includeRoot)
+ {
+ headers[index] = new(TelemetryConstants.RootSessionIdHeader, rootSessionId);
}
return headers;
}
+
+ private static string BuildSerializedAgentHeaders()
+ {
+ var sessionId = RuntimeId.Get();
+ var rootSessionId = RuntimeId.GetRootSessionId();
+
+ if (rootSessionId != sessionId)
+ {
+ return $"{TelemetryConstants.ClientLibraryLanguageHeader}: {TracerConstants.Language}" + DatadogHttpValues.CrLf +
+ $"{TelemetryConstants.ClientLibraryVersionHeader}: {TracerConstants.AssemblyVersion}" + DatadogHttpValues.CrLf +
+ $"{HttpHeaderNames.TracingEnabled}: false" + DatadogHttpValues.CrLf +
+ $"{TelemetryConstants.SessionIdHeader}: {sessionId}" + DatadogHttpValues.CrLf +
+ $"{TelemetryConstants.RootSessionIdHeader}: {rootSessionId}" + DatadogHttpValues.CrLf;
+ }
+
+ return $"{TelemetryConstants.ClientLibraryLanguageHeader}: {TracerConstants.Language}" + DatadogHttpValues.CrLf +
+ $"{TelemetryConstants.ClientLibraryVersionHeader}: {TracerConstants.AssemblyVersion}" + DatadogHttpValues.CrLf +
+ $"{HttpHeaderNames.TracingEnabled}: false" + DatadogHttpValues.CrLf +
+ $"{TelemetryConstants.SessionIdHeader}: {sessionId}" + DatadogHttpValues.CrLf;
+ }
}
}
diff --git a/tracer/src/Datadog.Trace/Util/RuntimeId.cs b/tracer/src/Datadog.Trace/Util/RuntimeId.cs
index bd941132adc6..29855cda0c05 100644
--- a/tracer/src/Datadog.Trace/Util/RuntimeId.cs
+++ b/tracer/src/Datadog.Trace/Util/RuntimeId.cs
@@ -5,7 +5,11 @@
using System;
using System.Threading;
+using Datadog.Trace.Configuration;
+using Datadog.Trace.Configuration.Telemetry;
using Datadog.Trace.Logging;
+using Datadog.Trace.SourceGenerators;
+using Datadog.Trace.Telemetry;
namespace Datadog.Trace.Util
{
@@ -13,9 +17,15 @@ internal static class RuntimeId
{
private static readonly IDatadogLogger Log = DatadogLogging.GetLoggerFor(typeof(RuntimeId));
private static string _runtimeId;
+ private static string _rootSessionId;
public static string Get() => LazyInitializer.EnsureInitialized(ref _runtimeId, () => GetImpl());
+ public static string GetRootSessionId() => LazyInitializer.EnsureInitialized(ref _rootSessionId, () => GetRootSessionIdImpl());
+
+ [TestingAndPrivateOnly]
+ internal static void ResetForTests() => _rootSessionId = null;
+
private static string GetImpl()
{
if (NativeLoader.TryGetRuntimeIdFromNative(out var runtimeId))
@@ -29,5 +39,20 @@ private static string GetImpl()
return guid;
}
+
+ private static string GetRootSessionIdImpl()
+ {
+ var config = new ConfigurationBuilder(new EnvironmentConfigurationSource(), TelemetryFactory.Config);
+ var inherited = config.WithKeys(ConfigurationKeys.Telemetry.RootSessionId).AsString();
+ if (!string.IsNullOrEmpty(inherited))
+ {
+ Log.Debug("Inherited root session ID from parent: {RootSessionId}", inherited);
+ return inherited;
+ }
+
+ var rootId = Get();
+ EnvironmentHelpers.SetEnvironmentVariable(ConfigurationKeys.Telemetry.RootSessionId, rootId);
+ return rootId;
+ }
}
}
diff --git a/tracer/test/Datadog.Trace.IntegrationTests/TelemetryTransportTests.cs b/tracer/test/Datadog.Trace.IntegrationTests/TelemetryTransportTests.cs
index f0c449eaf693..5287eac5fb45 100644
--- a/tracer/test/Datadog.Trace.IntegrationTests/TelemetryTransportTests.cs
+++ b/tracer/test/Datadog.Trace.IntegrationTests/TelemetryTransportTests.cs
@@ -14,6 +14,7 @@
using Datadog.Trace.Telemetry;
using Datadog.Trace.Telemetry.Transports;
using Datadog.Trace.TestHelpers;
+using Datadog.Trace.Util;
using FluentAssertions;
using FluentAssertions.Execution;
using Xunit;
@@ -116,6 +117,12 @@ public async Task SetsRequiredHeaders(bool agentless, bool useCloudAgentless)
{ "DD-Client-Library-Version", TracerConstants.AssemblyVersion },
};
+ // DD-Session-ID is always present and equals the runtime ID
+ allExpected.Add(TelemetryConstants.SessionIdHeader, RuntimeId.Get());
+
+ // DD-Root-Session-ID is absent when rootSessionId == runtimeId (normal process)
+ // We can't assert absence in the loop below, so we check it separately after
+
if (ContainerMetadata.Instance.ContainerId is { } containerId)
{
allExpected.Add(AgentHttpHeaderNames.ContainerId, containerId);
@@ -149,6 +156,12 @@ public async Task SetsRequiredHeaders(bool agentless, bool useCloudAgentless)
}
}
+ // DD-Root-Session-ID should be absent in a normal (non-child) process
+ if (RuntimeId.GetRootSessionId() == RuntimeId.Get())
+ {
+ headers.AllKeys.Should().NotContain(TelemetryConstants.RootSessionIdHeader);
+ }
+
// should have either content-length or chunked encoding
headers.AllKeys.Should()
.Contain(s => s.Equals("Content-Type", StringComparison.OrdinalIgnoreCase)
diff --git a/tracer/test/Datadog.Trace.Tests/Telemetry/ConfigurationTests.cs b/tracer/test/Datadog.Trace.Tests/Telemetry/ConfigurationTests.cs
index f9d1c994b6e2..77df9cee720f 100644
--- a/tracer/test/Datadog.Trace.Tests/Telemetry/ConfigurationTests.cs
+++ b/tracer/test/Datadog.Trace.Tests/Telemetry/ConfigurationTests.cs
@@ -52,6 +52,7 @@ public class ConfigurationTests
"DD_CIVISIBILITY_CODE_COVERAGE_MODE",
"DD_CIVISIBILITY_AUTO_INSTRUMENTATION_PROVIDER",
// Internal env vars that we only ever read from environment
+ "_DD_ROOT_DOTNET_SESSION_ID",
"DD_INTERNAL_TRACE_NATIVE_ENGINE_PATH",
"DD_INTERNAL_PROFILING_NATIVE_ENGINE_PATH",
"DD_DOTNET_TRACER_HOME",
diff --git a/tracer/test/Datadog.Trace.Tests/Telemetry/Transports/JsonTelemetryTransportTests.cs b/tracer/test/Datadog.Trace.Tests/Telemetry/Transports/JsonTelemetryTransportTests.cs
index 4b7ce405aa63..5813c1d32aa3 100644
--- a/tracer/test/Datadog.Trace.Tests/Telemetry/Transports/JsonTelemetryTransportTests.cs
+++ b/tracer/test/Datadog.Trace.Tests/Telemetry/Transports/JsonTelemetryTransportTests.cs
@@ -74,7 +74,7 @@ public async Task ShouldContainRequiredHeaders(bool debugEnabled, [Combinatorial
{ "DD-Telemetry-API-Version", TelemetryConstants.ApiVersionV2 },
{ "DD-Telemetry-Request-Type", "my-request-type" },
{ "Datadog-Container-ID", "my-container-id" },
- { "Datadog-Entity-ID", "my-entity-id" }
+ { "Datadog-Entity-ID", "my-entity-id" },
};
if (debugEnabled)
{
diff --git a/tracer/test/Datadog.Trace.Tests/Util/RuntimeIdTests.cs b/tracer/test/Datadog.Trace.Tests/Util/RuntimeIdTests.cs
new file mode 100644
index 000000000000..f104f6f4bb18
--- /dev/null
+++ b/tracer/test/Datadog.Trace.Tests/Util/RuntimeIdTests.cs
@@ -0,0 +1,47 @@
+//
+// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License.
+// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc.
+//
+
+using System;
+using Datadog.Trace.Configuration;
+using Datadog.Trace.TestHelpers;
+using Datadog.Trace.Util;
+using FluentAssertions;
+using Xunit;
+
+namespace Datadog.Trace.Tests.Util
+{
+ [EnvironmentRestorer(ConfigurationKeys.Telemetry.RootSessionId)]
+ public class RuntimeIdTests
+ {
+ [Fact]
+ public void RootSessionId_UsesRuntimeIdWhenNotInherited_AndInheritsWhenSet()
+ {
+ try
+ {
+ // When no env var is set, root session ID should default to runtime ID
+ RuntimeId.ResetForTests();
+ Environment.SetEnvironmentVariable(ConfigurationKeys.Telemetry.RootSessionId, null);
+
+ var rootSessionId = RuntimeId.GetRootSessionId();
+ rootSessionId.Should().Be(RuntimeId.Get());
+
+ // When env var is pre-set (simulating a child process), root session ID
+ // should return the inherited value instead of the current runtime ID
+ var inherited = "inherited-root-session-id";
+ RuntimeId.ResetForTests();
+ Environment.SetEnvironmentVariable(ConfigurationKeys.Telemetry.RootSessionId, inherited);
+
+ RuntimeId.GetRootSessionId().Should().Be(inherited);
+ RuntimeId.GetRootSessionId().Should().NotBe(RuntimeId.Get());
+ }
+ finally
+ {
+ RuntimeId.ResetForTests();
+ Environment.SetEnvironmentVariable(ConfigurationKeys.Telemetry.RootSessionId, null);
+ RuntimeId.GetRootSessionId();
+ }
+ }
+ }
+}