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(); + } + } + } +}