From efa9f9d937665d11fe1712c673590d1a54c15160 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Vandon?= Date: Tue, 25 Nov 2025 17:11:18 +0100 Subject: [PATCH 1/3] Add process tags to runtime metrics & dynamic instrumentation metrics probes # Conflicts: # tracer/src/Datadog.Trace/Debugger/DebuggerFactory.cs # tracer/src/Datadog.Trace/TracerManagerFactory.cs --- .../Datadog.Trace/Debugger/DebuggerFactory.cs | 1 + .../Datadog.Trace/DogStatsd/StatsdFactory.cs | 27 +++++----- .../Datadog.Trace/DogStatsd/StatsdManager.cs | 14 +++-- tracer/src/Datadog.Trace/ProcessTags.cs | 6 +-- .../Protocol/RcmClientTracer.cs | 8 ++- .../RemoteConfigurationManager.cs | 2 +- .../Datadog.Trace.Tests/DogStatsDTests.cs | 4 ++ .../DogStatsd/StatsdManagerTests.cs | 54 +++++++++---------- .../RuntimeEventListenerTests.cs | 2 +- 9 files changed, 64 insertions(+), 54 deletions(-) diff --git a/tracer/src/Datadog.Trace/Debugger/DebuggerFactory.cs b/tracer/src/Datadog.Trace/Debugger/DebuggerFactory.cs index 93c7b717a057..4dd8f3f5e1ee 100644 --- a/tracer/src/Datadog.Trace/Debugger/DebuggerFactory.cs +++ b/tracer/src/Datadog.Trace/Debugger/DebuggerFactory.cs @@ -72,6 +72,7 @@ private static IDogStatsd GetDogStatsd(TracerSettings tracerSettings, string ser tracerSettings.Manager.InitialMutableSettings, tracerSettings.Manager.InitialExporterSettings, includeDefaultTags: false, + tracerSettings.PropagateProcessTags, prefix: DebuggerSettings.DebuggerMetricPrefix); } diff --git a/tracer/src/Datadog.Trace/DogStatsd/StatsdFactory.cs b/tracer/src/Datadog.Trace/DogStatsd/StatsdFactory.cs index 37f25162c6b2..ee79369721e4 100644 --- a/tracer/src/Datadog.Trace/DogStatsd/StatsdFactory.cs +++ b/tracer/src/Datadog.Trace/DogStatsd/StatsdFactory.cs @@ -18,19 +18,19 @@ internal static class StatsdFactory { private static readonly IDatadogLogger Log = DatadogLogging.GetLoggerFor(typeof(StatsdFactory)); - internal static IDogStatsd CreateDogStatsdClient(MutableSettings settings, ExporterSettings exporter, bool includeDefaultTags, string? prefix = null) + internal static IDogStatsd CreateDogStatsdClient(MutableSettings settings, ExporterSettings exporter, bool includeDefaultTags, bool includeProcessTags, string? prefix = null) { + var customTagCount = settings.GlobalTags.Count; + var tagsCount = (includeDefaultTags ? 5 + customTagCount : 0) + (includeProcessTags ? ProcessTags.TagsList.Count : 0); + var constantTags = new List(tagsCount); if (includeDefaultTags) { - var customTagCount = settings.GlobalTags.Count; - var constantTags = new List(5 + customTagCount) - { - "lang:.NET", - $"lang_interpreter:{FrameworkDescription.Instance.Name}", - $"lang_version:{FrameworkDescription.Instance.ProductVersion}", - $"tracer_version:{TracerConstants.AssemblyVersion}", - $"{Tags.RuntimeId}:{Tracer.RuntimeId}" - }; + constantTags.Add("lang:.NET"); + constantTags.Add($"lang_interpreter:{FrameworkDescription.Instance.Name}"); + constantTags.Add($"lang_version:{FrameworkDescription.Instance.ProductVersion}"); + constantTags.Add($"tracer_version:{TracerConstants.AssemblyVersion}"); + constantTags.Add($"{Tags.RuntimeId}:{Tracer.RuntimeId}"); + // update count above if adding new tags if (customTagCount > 0) { @@ -43,11 +43,14 @@ internal static IDogStatsd CreateDogStatsdClient(MutableSettings settings, Expor constantTags.Add($"{key}:{value}"); } } + } - return CreateDogStatsdClient(settings, exporter, constantTags, prefix); + if (includeProcessTags) + { + constantTags.AddRange(ProcessTags.TagsList); } - return CreateDogStatsdClient(settings, exporter, constantTags: null, prefix); + return CreateDogStatsdClient(settings, exporter, constantTags, prefix); } private static IDogStatsd CreateDogStatsdClient(MutableSettings settings, ExporterSettings exporter, List? constantTags, string? prefix = null) diff --git a/tracer/src/Datadog.Trace/DogStatsd/StatsdManager.cs b/tracer/src/Datadog.Trace/DogStatsd/StatsdManager.cs index 3d455cb149ab..8df3bbea4e08 100644 --- a/tracer/src/Datadog.Trace/DogStatsd/StatsdManager.cs +++ b/tracer/src/Datadog.Trace/DogStatsd/StatsdManager.cs @@ -33,12 +33,13 @@ public StatsdManager(TracerSettings tracerSettings) } // Internal for testing - internal StatsdManager(TracerSettings tracerSettings, Func statsdFactory) + internal StatsdManager(TracerSettings tracerSettings, Func statsdFactory) { // The initial factory, assuming there's no updates _factory = () => statsdFactory( tracerSettings.Manager.InitialMutableSettings, - tracerSettings.Manager.InitialExporterSettings); + tracerSettings.Manager.InitialExporterSettings, + tracerSettings.PropagateProcessTags); // We don't create a new client unless we need one, and we rely on consumers of the manager to tell us when it's needed _current = null; @@ -60,7 +61,8 @@ internal StatsdManager(TracerSettings tracerSettings, Func statsdFactory( c.UpdatedMutable ?? c.PreviousMutable, - c.UpdatedExporter ?? c.PreviousExporter)); + c.UpdatedExporter ?? c.PreviousExporter, + tracerSettings.PropagateProcessTags)); // check if we actually need to do an update or if noone is using the client yet if (Volatile.Read(ref _isRequiredMask) != 0) @@ -173,8 +175,10 @@ internal static bool HasImpactingChanges(TracerSettings.SettingsManager.SettingC return hasChanges; } - private static StatsdClientHolder CreateClient(MutableSettings settings, ExporterSettings exporter) - => new(StatsdFactory.CreateDogStatsdClient(settings, exporter, includeDefaultTags: true)); + private static StatsdClientHolder CreateClient(MutableSettings settings, ExporterSettings exporter, bool withProcessTags) + { + return new StatsdClientHolder(StatsdFactory.CreateDogStatsdClient(settings, exporter, includeDefaultTags: true, withProcessTags)); + } private void EnsureClient(bool ensureCreated, bool forceRecreate) { diff --git a/tracer/src/Datadog.Trace/ProcessTags.cs b/tracer/src/Datadog.Trace/ProcessTags.cs index 9e65d4a37425..a48525f8f377 100644 --- a/tracer/src/Datadog.Trace/ProcessTags.cs +++ b/tracer/src/Datadog.Trace/ProcessTags.cs @@ -7,10 +7,10 @@ using System; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.IO; using Datadog.Trace.Configuration; using Datadog.Trace.Processors; -using Datadog.Trace.Util; namespace Datadog.Trace; @@ -21,7 +21,7 @@ internal static class ProcessTags public const string EntrypointWorkdir = "entrypoint.workdir"; // two views on the same data - public static readonly List TagsList = GetTagsList(); + public static readonly ReadOnlyCollection TagsList = GetTagsList().AsReadOnly(); public static readonly string SerializedTags = GetSerializedTagsFromList(TagsList); private static List GetTagsList() @@ -53,7 +53,7 @@ private static void AddNormalizedTag(this List tags, string key, string? tags.Add($"{key}:{normalizedValue}"); } - private static string GetSerializedTagsFromList(List tags) + private static string GetSerializedTagsFromList(IEnumerable tags) { return string.Join(",", tags); } diff --git a/tracer/src/Datadog.Trace/RemoteConfigurationManagement/Protocol/RcmClientTracer.cs b/tracer/src/Datadog.Trace/RemoteConfigurationManagement/Protocol/RcmClientTracer.cs index 8ce1f8ad4623..1c4d96bbb9dc 100644 --- a/tracer/src/Datadog.Trace/RemoteConfigurationManagement/Protocol/RcmClientTracer.cs +++ b/tracer/src/Datadog.Trace/RemoteConfigurationManagement/Protocol/RcmClientTracer.cs @@ -10,15 +10,13 @@ using System.Linq; using Datadog.Trace.Vendors.Newtonsoft.Json; -#nullable enable - namespace Datadog.Trace.RemoteConfigurationManagement.Protocol { internal class RcmClientTracer { // Don't change this constructor - it's used by Newtonsoft.JSON for deserialization // and that can mean the provided properties are not _really_ nullable, even though we "require" them to be - public RcmClientTracer(string runtimeId, string tracerVersion, string service, string env, string? appVersion, List tags, List? processTags) + public RcmClientTracer(string runtimeId, string tracerVersion, string service, string env, string? appVersion, List tags, IList? processTags) { RuntimeId = runtimeId; Language = TracerConstants.Language; @@ -46,7 +44,7 @@ public RcmClientTracer(string runtimeId, string tracerVersion, string service, s public string? Service { get; } [JsonProperty("process_tags")] - public List? ProcessTags { get; } + public IList? ProcessTags { get; } [JsonProperty("extra_services")] public string[]? ExtraServices { get; set; } @@ -60,7 +58,7 @@ public RcmClientTracer(string runtimeId, string tracerVersion, string service, s [JsonProperty("tags")] public List Tags { get; } - public static RcmClientTracer Create(string runtimeId, string tracerVersion, string service, string env, string? appVersion, ReadOnlyDictionary globalTags, List? processTags) + public static RcmClientTracer Create(string runtimeId, string tracerVersion, string service, string env, string? appVersion, ReadOnlyDictionary globalTags, IList? processTags) => new(runtimeId, tracerVersion, service, env, appVersion, GetTags(env, service, globalTags), processTags); private static List GetTags(string? environment, string? serviceVersion, ReadOnlyDictionary? globalTags) diff --git a/tracer/src/Datadog.Trace/RemoteConfigurationManagement/RemoteConfigurationManager.cs b/tracer/src/Datadog.Trace/RemoteConfigurationManagement/RemoteConfigurationManager.cs index 9b7fcb74059e..a6e5fb06c27a 100644 --- a/tracer/src/Datadog.Trace/RemoteConfigurationManagement/RemoteConfigurationManager.cs +++ b/tracer/src/Datadog.Trace/RemoteConfigurationManagement/RemoteConfigurationManager.cs @@ -42,7 +42,7 @@ private RemoteConfigurationManager( TimeSpan pollInterval, IGitMetadataTagsProvider gitMetadataTagsProvider, IRcmSubscriptionManager subscriptionManager, - List? processTags) + IList? processTags) { _discoveryService = discoveryService; _pollInterval = pollInterval; diff --git a/tracer/test/Datadog.Trace.Tests/DogStatsDTests.cs b/tracer/test/Datadog.Trace.Tests/DogStatsDTests.cs index 487b80ffcb86..4e69888dd417 100644 --- a/tracer/test/Datadog.Trace.Tests/DogStatsDTests.cs +++ b/tracer/test/Datadog.Trace.Tests/DogStatsDTests.cs @@ -139,6 +139,7 @@ public void CanCreateDogStatsD_UDP_FromTraceAgentSettings(string agentUri, strin settings.Manager.InitialMutableSettings, settings.Manager.InitialExporterSettings, includeDefaultTags: true, + includeProcessTags: true, prefix: null); // If there's an error during configuration, we get a no-op instance, so using this as a test @@ -168,6 +169,7 @@ public void CanCreateDogStatsD_NamedPipes_FromTraceAgentSettings() settings.Manager.InitialMutableSettings, settings.Manager.InitialExporterSettings, includeDefaultTags: true, + includeProcessTags: true, prefix: null); // If there's an error during configuration, we get a no-op instance, so using this as a test @@ -202,6 +204,7 @@ public void CanCreateDogStatsD_UDS_FromTraceAgentSettings() settings.Manager.InitialMutableSettings, settings.Manager.InitialExporterSettings, includeDefaultTags: true, + includeProcessTags: true, prefix: null); // If there's an error during configuration, we get a no-op instance, so using this as a test @@ -234,6 +237,7 @@ public void CanCreateDogStatsD_UDS_FallsBackToUdp_FromTraceAgentSettings() settings.Manager.InitialMutableSettings, settings.Manager.InitialExporterSettings, includeDefaultTags: true, + includeProcessTags: true, prefix: null); // If there's an error during configuration, we get a no-op instance, so using this as a test diff --git a/tracer/test/Datadog.Trace.Tests/DogStatsd/StatsdManagerTests.cs b/tracer/test/Datadog.Trace.Tests/DogStatsd/StatsdManagerTests.cs index 4dc61ae82271..c0b0d31389b9 100644 --- a/tracer/test/Datadog.Trace.Tests/DogStatsd/StatsdManagerTests.cs +++ b/tracer/test/Datadog.Trace.Tests/DogStatsd/StatsdManagerTests.cs @@ -111,7 +111,7 @@ public void HasImpactingChanges_WhenMutableChangesGlobalTags() public void InitialState_ClientNotCreated() { var clientCount = 0; - using var manager = new StatsdManager(new TracerSettings(), (_, _) => + using var manager = new StatsdManager(new TracerSettings(), (_, _, _) => { Interlocked.Increment(ref clientCount); return new(new MockStatsdClient()); @@ -127,7 +127,7 @@ public void InitialState_ClientNotCreated() public void SetRequired_CreatesClient() { var clientCount = 0; - using var manager = new StatsdManager(new TracerSettings(), (_, _) => + using var manager = new StatsdManager(new TracerSettings(), (_, _, _) => { Interlocked.Increment(ref clientCount); return new(new MockStatsdClient()); @@ -144,7 +144,7 @@ public void SetRequired_CreatesClient() public void SetRequired_False_DisposesClient() { var holder = new StatsdManager.StatsdClientHolder(new MockStatsdClient()); - using var manager = new StatsdManager(new TracerSettings(), (_, _) => holder); + using var manager = new StatsdManager(new TracerSettings(), (_, _, _) => holder); manager.SetRequired(StatsdConsumer.RuntimeMetricsWriter, true); using (manager.TryGetClientLease()) @@ -160,7 +160,7 @@ public void SetRequired_False_DisposesClient() public void MultipleConsumers_AllRequire_SingleClient() { var clientCount = 0; - using var manager = new StatsdManager(new TracerSettings(), (_, _) => + using var manager = new StatsdManager(new TracerSettings(), (_, _, _) => { Interlocked.Increment(ref clientCount); return new(new MockStatsdClient()); @@ -178,7 +178,7 @@ public void MultipleConsumers_PartialUnrequire_KeepsClient() { var clientCount = 0; var holder = new StatsdManager.StatsdClientHolder(new MockStatsdClient()); - using var manager = new StatsdManager(new TracerSettings(), (_, _) => + using var manager = new StatsdManager(new TracerSettings(), (_, _, _) => { Interlocked.Increment(ref clientCount); return holder; @@ -198,7 +198,7 @@ public void MultipleConsumers_AllUnrequire_DisposesClient() { var clientCount = 0; var holder = new StatsdManager.StatsdClientHolder(new MockStatsdClient()); - using var manager = new StatsdManager(new TracerSettings(), (_, _) => + using var manager = new StatsdManager(new TracerSettings(), (_, _, _) => { Interlocked.Increment(ref clientCount); return holder; @@ -223,7 +223,7 @@ public void MultipleConsumers_ReRequire_CreatesNewClient() { var clientCount = 0; StatsdManager.StatsdClientHolder holder = null; - using var manager = new StatsdManager(new TracerSettings(), (_, _) => + using var manager = new StatsdManager(new TracerSettings(), (_, _, _) => { Interlocked.Increment(ref clientCount); var newClient = new StatsdManager.StatsdClientHolder(new MockStatsdClient()); @@ -245,7 +245,7 @@ public void MultipleConsumers_ReRequire_CreatesNewClient() public void Lease_ProvidesAccessToClient() { var holder = new StatsdManager.StatsdClientHolder(new MockStatsdClient()); - using var manager = new StatsdManager(new TracerSettings(), (_, _) => holder); + using var manager = new StatsdManager(new TracerSettings(), (_, _, _) => holder); manager.SetRequired(StatsdConsumer.RuntimeMetricsWriter, true); using var lease = manager.TryGetClientLease(); @@ -257,7 +257,7 @@ public void Lease_ProvidesAccessToClient() public void MultipleLeasesSimultaneously_ShareSameClient() { var holder = new StatsdManager.StatsdClientHolder(new MockStatsdClient()); - using var manager = new StatsdManager(new TracerSettings(), (_, _) => holder); + using var manager = new StatsdManager(new TracerSettings(), (_, _, _) => holder); manager.SetRequired(StatsdConsumer.RuntimeMetricsWriter, true); using var lease1 = manager.TryGetClientLease(); @@ -273,7 +273,7 @@ public void MultipleLeasesSimultaneously_ShareSameClient() public void DisposingLease_DoesNotDisposeClient_WhileOtherLeasesActive() { var holder = new StatsdManager.StatsdClientHolder(new MockStatsdClient()); - using var manager = new StatsdManager(new TracerSettings(), (_, _) => holder); + using var manager = new StatsdManager(new TracerSettings(), (_, _, _) => holder); manager.SetRequired(StatsdConsumer.RuntimeMetricsWriter, true); var lease1 = manager.TryGetClientLease(); @@ -291,7 +291,7 @@ public void DisposingLease_DoesNotDisposeClient_WhileOtherLeasesActive() public void NeverReturnsDisposedClient() { StatsdManager.StatsdClientHolder holder = null; - using var manager = new StatsdManager(new TracerSettings(), (_, _) => + using var manager = new StatsdManager(new TracerSettings(), (_, _, _) => { var newClient = new StatsdManager.StatsdClientHolder(new MockStatsdClient()); Volatile.Write(ref holder, newClient); @@ -321,7 +321,7 @@ public void NeverReturnsDisposedClient() public void ReferenceCountingPreventsDisposalWhileLeasesActive() { var holder = new StatsdManager.StatsdClientHolder(new MockStatsdClient()); - using var manager = new StatsdManager(new TracerSettings(), (_, _) => holder); + using var manager = new StatsdManager(new TracerSettings(), (_, _, _) => holder); manager.SetRequired(StatsdConsumer.RuntimeMetricsWriter, true); var lease = manager.TryGetClientLease(); @@ -338,7 +338,7 @@ public void ReferenceCountingPreventsDisposalWhileLeasesActive() public void Dispose_WithActiveLease_DisposesAfterLeaseReleased() { var holder = new StatsdManager.StatsdClientHolder(new MockStatsdClient()); - var manager = new StatsdManager(new TracerSettings(), (_, _) => holder); + var manager = new StatsdManager(new TracerSettings(), (_, _, _) => holder); manager.SetRequired(StatsdConsumer.RuntimeMetricsWriter, true); var lease = manager.TryGetClientLease(); @@ -358,7 +358,7 @@ public void SettingsUpdate_RecreatesClient_WhenRequired() { var clientCount = 0; var tracerSettings = new TracerSettings(); - using var manager = new StatsdManager(tracerSettings, (_, _) => + using var manager = new StatsdManager(tracerSettings, (_, _, _) => { Interlocked.Increment(ref clientCount); return new(new MockStatsdClient()); @@ -389,7 +389,7 @@ public void SettingsUpdate_OldLeaseContinuesWorkingWithOldClient() var tracerSettings = new TracerSettings(); StatsdManager.StatsdClientHolder holder = null; - using var manager = new StatsdManager(tracerSettings, (_, _) => + using var manager = new StatsdManager(tracerSettings, (_, _, _) => { var newClient = new StatsdManager.StatsdClientHolder(new MockStatsdClient()); Volatile.Write(ref holder, newClient); @@ -426,7 +426,7 @@ public void SettingsUpdate_DoesNotRecreateClient_WhenNotRequired() { var tracerSettings = new TracerSettings(); var clientCount = 0; - using var manager = new StatsdManager(tracerSettings, (_, _) => + using var manager = new StatsdManager(tracerSettings, (_, _, _) => { Interlocked.Increment(ref clientCount); return new(new MockStatsdClient()); @@ -448,7 +448,7 @@ public void SettingsUpdate_DoesNotRecreateClient_WhenSettingsDontChange() { var tracerSettings = new TracerSettings(); var clientCount = 0; - using var manager = new StatsdManager(tracerSettings, (_, _) => + using var manager = new StatsdManager(tracerSettings, (_, _, _) => { Interlocked.Increment(ref clientCount); return new(new MockStatsdClient()); @@ -471,7 +471,7 @@ public void SettingsUpdate_DoesNotRecreateClient_WhenRelevantSettingsDontChange( { var tracerSettings = new TracerSettings(); var clientCount = 0; - using var manager = new StatsdManager(tracerSettings, (_, _) => + using var manager = new StatsdManager(tracerSettings, (_, _, _) => { Interlocked.Increment(ref clientCount); return new(new MockStatsdClient()); @@ -494,7 +494,7 @@ public void SettingsUpdate_DoesNotRecreateClient_WhenRelevantSettingsDontChange( public void ConcurrentLeaseAcquisition_AllSucceed() { var holder = new StatsdManager.StatsdClientHolder(new MockStatsdClient()); - using var manager = new StatsdManager(new TracerSettings(), (_, _) => holder); + using var manager = new StatsdManager(new TracerSettings(), (_, _, _) => holder); manager.SetRequired(StatsdConsumer.RuntimeMetricsWriter, true); var leases = new ConcurrentQueue(); @@ -519,7 +519,7 @@ public async Task ConcurrentLeaseAcquisitionAndDisposal_ThreadSafe() { var clientCount = 0; var holder = new StatsdManager.StatsdClientHolder(new MockStatsdClient()); - using var manager = new StatsdManager(new TracerSettings(), (_, _) => + using var manager = new StatsdManager(new TracerSettings(), (_, _, _) => { Interlocked.Increment(ref clientCount); return holder; @@ -567,7 +567,7 @@ public async Task ConcurrentLeaseAcquisitionAndDisposal_ThreadSafe() public void ConcurrentSetRequired_ThreadSafe() { var clientCount = 0; - using var manager = new StatsdManager(new TracerSettings(), (_, _) => + using var manager = new StatsdManager(new TracerSettings(), (_, _, _) => { Interlocked.Increment(ref clientCount); return new(new MockStatsdClient()); @@ -593,7 +593,7 @@ public async Task ConcurrentSettingsUpdateAndLeaseAcquisition_ThreadSafe() { var tracerSettings = new TracerSettings(); var clientCount = 0; - using var manager = new StatsdManager(tracerSettings, (_, _) => + using var manager = new StatsdManager(tracerSettings, (_, _, _) => { Interlocked.Increment(ref clientCount); return new(new MockStatsdClient()); @@ -647,7 +647,7 @@ public async Task ConcurrentSettingsUpdateAndLeaseAcquisition_ThreadSafe() public async Task ConcurrentLeaseDisposalDuringClientRecreation_ThreadSafe() { var holders = new ConcurrentQueue(); - using var manager = new StatsdManager(new TracerSettings(), (_, _) => + using var manager = new StatsdManager(new TracerSettings(), (_, _, _) => { var client = new StatsdManager.StatsdClientHolder(new MockStatsdClient()); holders.Enqueue(client); @@ -694,7 +694,7 @@ public async Task ConcurrentLeaseDisposalDuringClientRecreation_ThreadSafe() public void MultipleTransitionsBetweenRequiredAndNotRequired() { var holders = new List(); - using var manager = new StatsdManager(new TracerSettings(), (_, _) => + using var manager = new StatsdManager(new TracerSettings(), (_, _, _) => { var client = new StatsdManager.StatsdClientHolder(new MockStatsdClient()); holders.Add(client); @@ -716,7 +716,7 @@ public void MultipleTransitionsBetweenRequiredAndNotRequired() [Fact] public void Dispose_MultipleTimes_IsSafe() { - using var manager = new StatsdManager(new TracerSettings(), (_, _) => new(new MockStatsdClient())); + using var manager = new StatsdManager(new TracerSettings(), (_, _, _) => new(new MockStatsdClient())); manager.SetRequired(StatsdConsumer.RuntimeMetricsWriter, true); manager.Dispose(); @@ -727,7 +727,7 @@ public void Dispose_MultipleTimes_IsSafe() [Fact] public void DefaultLease_CanDisposeSafely() { - using var manager = new StatsdManager(new TracerSettings(), (_, _) => new(new MockStatsdClient())); + using var manager = new StatsdManager(new TracerSettings(), (_, _, _) => new(new MockStatsdClient())); var lease = manager.TryGetClientLease(); @@ -739,7 +739,7 @@ public void DefaultLease_CanDisposeSafely() public void DisposingLease_MultipleTimes_DoesNotDisposeStatsDMultipleTimes() { var holder = new StatsdManager.StatsdClientHolder(new MockStatsdClient()); - using var manager = new StatsdManager(new TracerSettings(), (_, _) => holder); + using var manager = new StatsdManager(new TracerSettings(), (_, _, _) => holder); manager.SetRequired(StatsdConsumer.RuntimeMetricsWriter, true); var lease = manager.TryGetClientLease(); diff --git a/tracer/test/Datadog.Trace.Tests/RuntimeMetrics/RuntimeEventListenerTests.cs b/tracer/test/Datadog.Trace.Tests/RuntimeMetrics/RuntimeEventListenerTests.cs index 2701dc39d087..2c834617bd86 100644 --- a/tracer/test/Datadog.Trace.Tests/RuntimeMetrics/RuntimeEventListenerTests.cs +++ b/tracer/test/Datadog.Trace.Tests/RuntimeMetrics/RuntimeEventListenerTests.cs @@ -151,7 +151,7 @@ public void UpdateStatsdOnReinitialization() var settings = TracerSettings.Create(new() { { ConfigurationKeys.ServiceName, "original" } }); var statsdManager = new StatsdManager( settings, - (m, e) => new(m.ServiceName == "original" ? originalStatsd.Object : newStatsd.Object)); + (m, e, p) => new(m.ServiceName == "original" ? originalStatsd.Object : newStatsd.Object)); using var listener = new RuntimeEventListener(statsdManager, TimeSpan.FromSeconds(1)); using var writer = new RuntimeMetricsWriter(statsdManager, TimeSpan.FromSeconds(1), false); From cf19a8513da4e28dfb1316b018b66becf35a6599 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Vandon?= Date: Thu, 8 Jan 2026 14:53:18 +0100 Subject: [PATCH 2/3] pass list rather than boolean --- tracer/src/Datadog.Trace/Debugger/DebuggerFactory.cs | 2 +- tracer/src/Datadog.Trace/DogStatsd/StatsdFactory.cs | 8 ++++---- tracer/src/Datadog.Trace/DogStatsd/StatsdManager.cs | 11 ++++++----- tracer/test/Datadog.Trace.Tests/DogStatsDTests.cs | 8 ++++---- 4 files changed, 15 insertions(+), 14 deletions(-) diff --git a/tracer/src/Datadog.Trace/Debugger/DebuggerFactory.cs b/tracer/src/Datadog.Trace/Debugger/DebuggerFactory.cs index 666d81e6cdbf..18806b73355d 100644 --- a/tracer/src/Datadog.Trace/Debugger/DebuggerFactory.cs +++ b/tracer/src/Datadog.Trace/Debugger/DebuggerFactory.cs @@ -72,7 +72,7 @@ private static IDogStatsd GetDogStatsd(TracerSettings tracerSettings, string ser tracerSettings.Manager.InitialMutableSettings, tracerSettings.Manager.InitialExporterSettings, includeDefaultTags: false, - tracerSettings.PropagateProcessTags, + tracerSettings.PropagateProcessTags ? ProcessTags.TagsList : [], prefix: DebuggerSettings.DebuggerMetricPrefix); } diff --git a/tracer/src/Datadog.Trace/DogStatsd/StatsdFactory.cs b/tracer/src/Datadog.Trace/DogStatsd/StatsdFactory.cs index ee79369721e4..42a51aa6c83d 100644 --- a/tracer/src/Datadog.Trace/DogStatsd/StatsdFactory.cs +++ b/tracer/src/Datadog.Trace/DogStatsd/StatsdFactory.cs @@ -18,10 +18,10 @@ internal static class StatsdFactory { private static readonly IDatadogLogger Log = DatadogLogging.GetLoggerFor(typeof(StatsdFactory)); - internal static IDogStatsd CreateDogStatsdClient(MutableSettings settings, ExporterSettings exporter, bool includeDefaultTags, bool includeProcessTags, string? prefix = null) + internal static IDogStatsd CreateDogStatsdClient(MutableSettings settings, ExporterSettings exporter, bool includeDefaultTags, IList processTags, string? prefix = null) { var customTagCount = settings.GlobalTags.Count; - var tagsCount = (includeDefaultTags ? 5 + customTagCount : 0) + (includeProcessTags ? ProcessTags.TagsList.Count : 0); + var tagsCount = (includeDefaultTags ? 5 + customTagCount : 0) + processTags.Count; var constantTags = new List(tagsCount); if (includeDefaultTags) { @@ -45,9 +45,9 @@ internal static IDogStatsd CreateDogStatsdClient(MutableSettings settings, Expor } } - if (includeProcessTags) + if (processTags.Count > 0) { - constantTags.AddRange(ProcessTags.TagsList); + constantTags.AddRange(processTags); } return CreateDogStatsdClient(settings, exporter, constantTags, prefix); diff --git a/tracer/src/Datadog.Trace/DogStatsd/StatsdManager.cs b/tracer/src/Datadog.Trace/DogStatsd/StatsdManager.cs index 8df3bbea4e08..f34330006406 100644 --- a/tracer/src/Datadog.Trace/DogStatsd/StatsdManager.cs +++ b/tracer/src/Datadog.Trace/DogStatsd/StatsdManager.cs @@ -6,6 +6,7 @@ #nullable enable using System; +using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Datadog.Trace.Configuration; @@ -33,13 +34,13 @@ public StatsdManager(TracerSettings tracerSettings) } // Internal for testing - internal StatsdManager(TracerSettings tracerSettings, Func statsdFactory) + internal StatsdManager(TracerSettings tracerSettings, Func, StatsdClientHolder> statsdFactory) { // The initial factory, assuming there's no updates _factory = () => statsdFactory( tracerSettings.Manager.InitialMutableSettings, tracerSettings.Manager.InitialExporterSettings, - tracerSettings.PropagateProcessTags); + tracerSettings.PropagateProcessTags ? ProcessTags.TagsList : []); // We don't create a new client unless we need one, and we rely on consumers of the manager to tell us when it's needed _current = null; @@ -62,7 +63,7 @@ internal StatsdManager(TracerSettings tracerSettings, Func statsdFactory( c.UpdatedMutable ?? c.PreviousMutable, c.UpdatedExporter ?? c.PreviousExporter, - tracerSettings.PropagateProcessTags)); + tracerSettings.PropagateProcessTags ? ProcessTags.TagsList : [])); // check if we actually need to do an update or if noone is using the client yet if (Volatile.Read(ref _isRequiredMask) != 0) @@ -175,9 +176,9 @@ internal static bool HasImpactingChanges(TracerSettings.SettingsManager.SettingC return hasChanges; } - private static StatsdClientHolder CreateClient(MutableSettings settings, ExporterSettings exporter, bool withProcessTags) + private static StatsdClientHolder CreateClient(MutableSettings settings, ExporterSettings exporter, IList processTags) { - return new StatsdClientHolder(StatsdFactory.CreateDogStatsdClient(settings, exporter, includeDefaultTags: true, withProcessTags)); + return new StatsdClientHolder(StatsdFactory.CreateDogStatsdClient(settings, exporter, includeDefaultTags: true, processTags)); } private void EnsureClient(bool ensureCreated, bool forceRecreate) diff --git a/tracer/test/Datadog.Trace.Tests/DogStatsDTests.cs b/tracer/test/Datadog.Trace.Tests/DogStatsDTests.cs index 4e69888dd417..530b8b5b26d4 100644 --- a/tracer/test/Datadog.Trace.Tests/DogStatsDTests.cs +++ b/tracer/test/Datadog.Trace.Tests/DogStatsDTests.cs @@ -139,7 +139,7 @@ public void CanCreateDogStatsD_UDP_FromTraceAgentSettings(string agentUri, strin settings.Manager.InitialMutableSettings, settings.Manager.InitialExporterSettings, includeDefaultTags: true, - includeProcessTags: true, + processTags: ["a:b", "c:d"], prefix: null); // If there's an error during configuration, we get a no-op instance, so using this as a test @@ -169,7 +169,7 @@ public void CanCreateDogStatsD_NamedPipes_FromTraceAgentSettings() settings.Manager.InitialMutableSettings, settings.Manager.InitialExporterSettings, includeDefaultTags: true, - includeProcessTags: true, + processTags: ["a:b", "c:d"], prefix: null); // If there's an error during configuration, we get a no-op instance, so using this as a test @@ -204,7 +204,7 @@ public void CanCreateDogStatsD_UDS_FromTraceAgentSettings() settings.Manager.InitialMutableSettings, settings.Manager.InitialExporterSettings, includeDefaultTags: true, - includeProcessTags: true, + processTags: ["a:b", "c:d"], prefix: null); // If there's an error during configuration, we get a no-op instance, so using this as a test @@ -237,7 +237,7 @@ public void CanCreateDogStatsD_UDS_FallsBackToUdp_FromTraceAgentSettings() settings.Manager.InitialMutableSettings, settings.Manager.InitialExporterSettings, includeDefaultTags: true, - includeProcessTags: true, + processTags: ["a:b", "c:d"], prefix: null); // If there's an error during configuration, we get a no-op instance, so using this as a test From 1290c65f291faefe06ba9cf38085cc694bd9fce2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Vandon?= Date: Thu, 8 Jan 2026 15:51:51 +0100 Subject: [PATCH 3/3] add tests --- .../RuntimeMetricsTests.cs | 54 +++++++++++++++++++ .../DogStatsd/StatsdManagerTests.cs | 36 +++++++++++++ 2 files changed, 90 insertions(+) diff --git a/tracer/test/Datadog.Trace.ClrProfiler.IntegrationTests/RuntimeMetricsTests.cs b/tracer/test/Datadog.Trace.ClrProfiler.IntegrationTests/RuntimeMetricsTests.cs index 2d01f7ef0993..6856dd5fbfc9 100644 --- a/tracer/test/Datadog.Trace.ClrProfiler.IntegrationTests/RuntimeMetricsTests.cs +++ b/tracer/test/Datadog.Trace.ClrProfiler.IntegrationTests/RuntimeMetricsTests.cs @@ -82,6 +82,60 @@ public async Task NamedPipesSubmitsMetrics() await RunTest(); } + [SkippableFact] + [Trait("Category", "EndToEnd")] + [Trait("RunOnWindows", "True")] + public async Task ProcessTagsIncludedInMetrics_WhenEnabled() + { + SkipOn.Platform(SkipOn.PlatformValue.MacOs); + EnvironmentHelper.EnableDefaultTransport(); + SetEnvironmentVariable("DD_RUNTIME_METRICS_ENABLED", "1"); + SetEnvironmentVariable("DD_EXPERIMENTAL_PROPAGATE_PROCESS_TAGS_ENABLED", "1"); + SetEnvironmentVariable("DD_SERVICE", "Samples.RuntimeMetrics"); + + using var agent = EnvironmentHelper.GetMockAgent(useStatsD: true); + using var processResult = await RunSampleAndWaitForExit(agent); + var requests = agent.StatsdRequests; + + Assert.True(requests.Count > 0, "No metrics received"); + + var metrics = requests.SelectMany(x => x.Split('\n')).ToList(); + + // Verify process tags are present in the metrics + // Process tags include: entrypoint.basedir, entrypoint.workdir, and optionally entrypoint.name + metrics + .Should() + .OnlyContain(s => s.Contains("entrypoint.basedir:"), "entrypoint.basedir process tag should be present") + .And.OnlyContain(s => s.Contains("entrypoint.workdir:"), "entrypoint.workdir process tag should be present"); + } + + [SkippableFact] + [Trait("Category", "EndToEnd")] + [Trait("RunOnWindows", "True")] + public async Task ProcessTagsNotIncludedInMetrics_WhenDisabled() + { + SkipOn.Platform(SkipOn.PlatformValue.MacOs); + EnvironmentHelper.EnableDefaultTransport(); + SetEnvironmentVariable("DD_RUNTIME_METRICS_ENABLED", "1"); + SetEnvironmentVariable("DD_EXPERIMENTAL_PROPAGATE_PROCESS_TAGS_ENABLED", "0"); + SetEnvironmentVariable("DD_SERVICE", "Samples.RuntimeMetrics"); + + using var agent = EnvironmentHelper.GetMockAgent(useStatsD: true); + using var processResult = await RunSampleAndWaitForExit(agent); + var requests = agent.StatsdRequests; + + Assert.True(requests.Count > 0, "No metrics received"); + + var metrics = requests.SelectMany(x => x.Split('\n')).ToList(); + + // Verify process tags are NOT present in the metrics when disabled + metrics + .Should() + .NotContain(s => s.Contains("entrypoint.basedir:"), "entrypoint.basedir should not be present when process tags are disabled") + .And.NotContain(s => s.Contains("entrypoint.workdir:"), "entrypoint.workdir should not be present when process tags are disabled") + .And.NotContain(s => s.Contains("entrypoint.name:"), "entrypoint.name should not be present when process tags are disabled"); + } + private async Task RunTest() { var inputServiceName = "12_$#Samples.$RuntimeMetrics"; diff --git a/tracer/test/Datadog.Trace.Tests/DogStatsd/StatsdManagerTests.cs b/tracer/test/Datadog.Trace.Tests/DogStatsd/StatsdManagerTests.cs index c0b0d31389b9..2c6e56b1247e 100644 --- a/tracer/test/Datadog.Trace.Tests/DogStatsd/StatsdManagerTests.cs +++ b/tracer/test/Datadog.Trace.Tests/DogStatsd/StatsdManagerTests.cs @@ -724,6 +724,42 @@ public void Dispose_MultipleTimes_IsSafe() manager.Dispose(); } + [Fact] + public void ProcessTags_PassedToFactory_WhenEnabled() + { + IList capturedProcessTags = null; + var settings = TracerSettings.Create(new() { { ConfigurationKeys.PropagateProcessTags, true } }); + using var manager = new StatsdManager(settings, (_, _, processTags) => + { + capturedProcessTags = processTags; + return new(new MockStatsdClient()); + }); + + manager.SetRequired(StatsdConsumer.RuntimeMetricsWriter, true); + + capturedProcessTags.Should().NotBeNull(); + capturedProcessTags.Should().NotBeEmpty("process tags should be passed to factory when enabled"); + // Verify the format is key:value + capturedProcessTags.Should().AllSatisfy(tag => tag.Should().Contain(":")); + } + + [Fact] + public void ProcessTags_NotPassedToFactory_WhenDisabled() + { + IList capturedProcessTags = null; + var settings = TracerSettings.Create(new() { { ConfigurationKeys.PropagateProcessTags, false } }); + using var manager = new StatsdManager(settings, (_, _, processTags) => + { + capturedProcessTags = processTags; + return new(new MockStatsdClient()); + }); + + manager.SetRequired(StatsdConsumer.RuntimeMetricsWriter, true); + + capturedProcessTags.Should().NotBeNull(); + capturedProcessTags.Should().BeEmpty("process tags should not be passed to factory when disabled"); + } + [Fact] public void DefaultLease_CanDisposeSafely() {