From 231f0bdf5c9fc08bb5b2d32a227ccd1c9af502bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Vandon?= Date: Wed, 8 Oct 2025 14:11:18 +0200 Subject: [PATCH 01/23] create processtags class --- tracer/src/Datadog.Trace/ProcessTags.cs | 62 +++++++++++++++++++++++++ tracer/src/Datadog.Trace/Tags.cs | 6 +++ 2 files changed, 68 insertions(+) create mode 100644 tracer/src/Datadog.Trace/ProcessTags.cs diff --git a/tracer/src/Datadog.Trace/ProcessTags.cs b/tracer/src/Datadog.Trace/ProcessTags.cs new file mode 100644 index 000000000000..2b5bd2f4c2b7 --- /dev/null +++ b/tracer/src/Datadog.Trace/ProcessTags.cs @@ -0,0 +1,62 @@ +// +// 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. +// + +#nullable enable + +using System; +using System.Linq; +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using Datadog.Trace.Processors; + +namespace Datadog.Trace; + +internal static class ProcessTags +{ + public const string EntrypointName = "entrypoint.name"; + public const string EntrypointBasedir = "entrypoint.basedir"; + public const string EntrypointWorkdir = "entrypoint.workdir"; + + private static Lazy> _tags = new(LoadBaseTags); + + private static Dictionary LoadBaseTags() + { + var tags = new Dictionary(); + + var entrypointFullName = Assembly.GetEntryAssembly()?.EntryPoint?.DeclaringType?.FullName; + if (!string.IsNullOrEmpty(entrypointFullName)) + { + tags.Add(EntrypointName, entrypointFullName); + } + + tags.Add(EntrypointBasedir, GetLastPathSegment(AppContext.BaseDirectory)); + tags.Add(EntrypointWorkdir, GetLastPathSegment(Environment.CurrentDirectory)); // ⚠️ can be changed by the code, not constant + + return tags; + } + + /// + /// From the full path of a directory, get the name of the leaf directory. + /// + private static string GetLastPathSegment(string directoryPath) + { + // Path.GetFileName returns an empty string if the path ends with a '/'. + // We could use Path.TrimEndingDirectorySeparator instead of the trim here, but it's not available on .netframework + return Path.GetFileName(directoryPath.TrimEnd('\\', '/')); + } + + public static string GetSerializedTags() + { + return string.Join(separator: ',', _tags.Value.Select(kv => $"{kv.Key}:{NormalizeTagValue(kv.Value)}")); + } + + private static string NormalizeTagValue(string tagValue) + { + // TraceUtil.NormalizeTag does almost exactly what we want, except it allows ':', + // which we don't want because we use it as a key/value separator. + return TraceUtil.NormalizeTag(tagValue).Replace(oldChar: ':', newChar: '_'); + } +} diff --git a/tracer/src/Datadog.Trace/Tags.cs b/tracer/src/Datadog.Trace/Tags.cs index 4a4e930f9ba2..ea90f33b5d45 100644 --- a/tracer/src/Datadog.Trace/Tags.cs +++ b/tracer/src/Datadog.Trace/Tags.cs @@ -506,6 +506,12 @@ public static partial class Tags /// internal const string RuntimeFamily = "_dd.runtime_family"; + /// + /// contains a serialized list of process tags, that can be used in the backend for service renaming + /// + /// + internal const string ProcessTags = "_dd.tags.process"; + /// /// The resource ID of the site instance in Azure App Services where the traced application is running. /// From 49c47fbd5f5b464b572327e3062801700006e31d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Vandon?= Date: Mon, 13 Oct 2025 15:11:47 +0200 Subject: [PATCH 02/23] add config flag --- .../Configuration/ConfigurationKeys.cs | 5 +++++ .../Datadog.Trace/Configuration/TracerSettings.cs | 13 +++++++++++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/tracer/src/Datadog.Trace/Configuration/ConfigurationKeys.cs b/tracer/src/Datadog.Trace/Configuration/ConfigurationKeys.cs index cbb60b0d32f8..a59103a2290c 100644 --- a/tracer/src/Datadog.Trace/Configuration/ConfigurationKeys.cs +++ b/tracer/src/Datadog.Trace/Configuration/ConfigurationKeys.cs @@ -168,6 +168,11 @@ internal static partial class ConfigurationKeys /// public const string GrpcTags = "DD_TRACE_GRPC_TAGS"; + /// + /// Propagate the process tags in every supported payload + /// + public const string PropagateProcessTags = "DD_EXPERIMENTAL_PROPAGATE_PROCESS_TAGS_ENABLED"; + /// /// Configuration key for a map of services to rename. /// diff --git a/tracer/src/Datadog.Trace/Configuration/TracerSettings.cs b/tracer/src/Datadog.Trace/Configuration/TracerSettings.cs index fec447014ff1..79be2ad11dbf 100644 --- a/tracer/src/Datadog.Trace/Configuration/TracerSettings.cs +++ b/tracer/src/Datadog.Trace/Configuration/TracerSettings.cs @@ -32,9 +32,11 @@ namespace Datadog.Trace.Configuration public record TracerSettings { private static readonly IDatadogLogger Log = DatadogLogging.GetLoggerFor(); - private static readonly HashSet DefaultExperimentalFeatures = new HashSet() + + private static readonly HashSet DefaultExperimentalFeatures = new() { - "DD_TAGS" + "DD_TAGS", + ConfigurationKeys.PropagateProcessTags }; private readonly IConfigurationTelemetry _telemetry; @@ -103,6 +105,11 @@ internal TracerSettings(IConfigurationSource? source, IConfigurationTelemetry te string s => new HashSet(s.Split([','], StringSplitOptions.RemoveEmptyEntries)), }; + PropagateProcessTags = config + .WithKeys(ConfigurationKeys.PropagateProcessTags) + .AsBool(false) + || ExperimentalFeaturesEnabled.Contains(ConfigurationKeys.PropagateProcessTags); + GCPFunctionSettings = new ImmutableGCPFunctionSettings(source, _telemetry); IsRunningInGCPFunctions = GCPFunctionSettings.IsGCPFunction; @@ -647,6 +654,8 @@ not null when string.Equals(x, "lowmemory", StringComparison.OrdinalIgnoreCase) internal HashSet ExperimentalFeaturesEnabled { get; } + internal bool PropagateProcessTags { get; } + internal OverrideErrorLog ErrorLog { get; } internal IConfigurationTelemetry Telemetry => _telemetry; From f252a35f035348d8b06e07663e6ae871534c8dc8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Vandon?= Date: Tue, 14 Oct 2025 15:01:21 +0200 Subject: [PATCH 03/23] write in messagepack + take config into account --- .../MessagePack/MessagePackStringCache.cs | 6 +++ .../MessagePack/SpanMessagePackFormatter.cs | 14 +++++ tracer/src/Datadog.Trace/ProcessTags.cs | 29 ++++++++--- tracer/src/Datadog.Trace/Tags.cs | 2 +- .../Datadog.Trace.Tests/ProcessTagsTests.cs | 51 +++++++++++++++++++ 5 files changed, 95 insertions(+), 7 deletions(-) create mode 100644 tracer/test/Datadog.Trace.Tests/ProcessTagsTests.cs diff --git a/tracer/src/Datadog.Trace/Agent/MessagePack/MessagePackStringCache.cs b/tracer/src/Datadog.Trace/Agent/MessagePack/MessagePackStringCache.cs index bee7b0ea3bb7..71367cfb0b6e 100644 --- a/tracer/src/Datadog.Trace/Agent/MessagePack/MessagePackStringCache.cs +++ b/tracer/src/Datadog.Trace/Agent/MessagePack/MessagePackStringCache.cs @@ -34,6 +34,7 @@ internal static class MessagePackStringCache // NOTE: all of these can be cached in SpanMessagePackFormatter as static byte[] // since they never change over the lifetime of a process + private static CachedBytes _processTags; private static CachedBytes _gitCommitSha; private static CachedBytes _gitRepositoryUrl; private static CachedBytes _aasSiteNameBytes; @@ -99,6 +100,11 @@ public static void Clear() return GetBytes(service, ref _service); } + public static byte[]? GetProcessTagsBytes(string processTags) + { + return GetBytes(processTags, ref _processTags); + } + public static byte[]? GetAzureAppServiceKeyBytes(string key, string? value) { switch (key) diff --git a/tracer/src/Datadog.Trace/Agent/MessagePack/SpanMessagePackFormatter.cs b/tracer/src/Datadog.Trace/Agent/MessagePack/SpanMessagePackFormatter.cs index 73250936c9be..12c3e788e89c 100644 --- a/tracer/src/Datadog.Trace/Agent/MessagePack/SpanMessagePackFormatter.cs +++ b/tracer/src/Datadog.Trace/Agent/MessagePack/SpanMessagePackFormatter.cs @@ -75,6 +75,7 @@ internal class SpanMessagePackFormatter : IMessagePackFormatter private readonly byte[] _originNameBytes = StringEncoding.UTF8.GetBytes(Trace.Tags.Origin); private readonly byte[] _lastParentIdBytes = StringEncoding.UTF8.GetBytes(Trace.Tags.LastParentId); private readonly byte[] _baseServiceNameBytes = StringEncoding.UTF8.GetBytes(Trace.Tags.BaseService); + private readonly byte[] _processTagsNameBytes = StringEncoding.UTF8.GetBytes(Tags.ProcessTags); // numeric tags private readonly byte[] _metricsBytes = StringEncoding.UTF8.GetBytes("metrics"); @@ -603,6 +604,19 @@ private int WriteTags(ref byte[] bytes, int offset, in SpanModel model, ITagProc } } + // Process tags will be sent only once per trace + if (model.IsFirstSpanInChunk) + { + var processTags = ProcessTags.SerializedTags; + var processTagsRawBytes = MessagePackStringCache.GetProcessTagsBytes(processTags); + if (!string.IsNullOrEmpty(processTags)) + { + count++; + offset += MessagePackBinary.WriteStringBytes(ref bytes, offset, _processTagsNameBytes); + offset += MessagePackBinary.WriteRaw(ref bytes, offset, processTagsRawBytes); + } + } + // SCI tags will be sent only once per trace if (model.IsFirstSpanInChunk) { diff --git a/tracer/src/Datadog.Trace/ProcessTags.cs b/tracer/src/Datadog.Trace/ProcessTags.cs index 2b5bd2f4c2b7..35f848138849 100644 --- a/tracer/src/Datadog.Trace/ProcessTags.cs +++ b/tracer/src/Datadog.Trace/ProcessTags.cs @@ -6,9 +6,9 @@ #nullable enable using System; -using System.Linq; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Reflection; using Datadog.Trace.Processors; @@ -20,20 +20,32 @@ internal static class ProcessTags public const string EntrypointBasedir = "entrypoint.basedir"; public const string EntrypointWorkdir = "entrypoint.workdir"; - private static Lazy> _tags = new(LoadBaseTags); + private static Lazy _lazySerializedTags = new(GetSerializedTags); + + public static string SerializedTags + { + get => _lazySerializedTags.Value; + } private static Dictionary LoadBaseTags() { var tags = new Dictionary(); + if (!Tracer.Instance.Settings.PropagateProcessTags) + { + // do not collect anything when disabled + return tags; + } + var entrypointFullName = Assembly.GetEntryAssembly()?.EntryPoint?.DeclaringType?.FullName; if (!string.IsNullOrEmpty(entrypointFullName)) { - tags.Add(EntrypointName, entrypointFullName); + tags.Add(EntrypointName, entrypointFullName!); } tags.Add(EntrypointBasedir, GetLastPathSegment(AppContext.BaseDirectory)); - tags.Add(EntrypointWorkdir, GetLastPathSegment(Environment.CurrentDirectory)); // ⚠️ can be changed by the code, not constant + // workdir can be changed by the code, but we consider that capturing the value when this is called is good enough + tags.Add(EntrypointWorkdir, GetLastPathSegment(Environment.CurrentDirectory)); return tags; } @@ -48,9 +60,9 @@ private static string GetLastPathSegment(string directoryPath) return Path.GetFileName(directoryPath.TrimEnd('\\', '/')); } - public static string GetSerializedTags() + private static string GetSerializedTags() { - return string.Join(separator: ',', _tags.Value.Select(kv => $"{kv.Key}:{NormalizeTagValue(kv.Value)}")); + return string.Join(",", LoadBaseTags().OrderBy(kv => kv.Key).Select(kv => $"{kv.Key}:{NormalizeTagValue(kv.Value)}")); } private static string NormalizeTagValue(string tagValue) @@ -59,4 +71,9 @@ private static string NormalizeTagValue(string tagValue) // which we don't want because we use it as a key/value separator. return TraceUtil.NormalizeTag(tagValue).Replace(oldChar: ':', newChar: '_'); } + + internal static void ResetForTests() + { + _lazySerializedTags = new Lazy(GetSerializedTags); + } } diff --git a/tracer/src/Datadog.Trace/Tags.cs b/tracer/src/Datadog.Trace/Tags.cs index ea90f33b5d45..027e56e25d49 100644 --- a/tracer/src/Datadog.Trace/Tags.cs +++ b/tracer/src/Datadog.Trace/Tags.cs @@ -511,7 +511,7 @@ public static partial class Tags /// /// internal const string ProcessTags = "_dd.tags.process"; - + /// /// The resource ID of the site instance in Azure App Services where the traced application is running. /// diff --git a/tracer/test/Datadog.Trace.Tests/ProcessTagsTests.cs b/tracer/test/Datadog.Trace.Tests/ProcessTagsTests.cs new file mode 100644 index 000000000000..cac5efd6cda1 --- /dev/null +++ b/tracer/test/Datadog.Trace.Tests/ProcessTagsTests.cs @@ -0,0 +1,51 @@ +// +// 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.Collections.Generic; +using System.Linq; +using Datadog.Trace.Configuration; +using FluentAssertions; +using Xunit; + +namespace Datadog.Trace.Tests; + +public class ProcessTagsTests +{ + public ProcessTagsTests() + { + ProcessTags.ResetForTests(); + } + + [Fact] + public void EmptyIfDisabled() + { + Tracer.Configure(TracerSettings.Create(new Dictionary())); + + ProcessTags.SerializedTags.Should().BeEmpty(); + } + + [Fact] + public void TagsPresentWhenEnabled() + { + Tracer.Configure(TracerSettings.Create(new Dictionary + { + [ConfigurationKeys.PropagateProcessTags] = "true" + })); + + var tags = ProcessTags.SerializedTags; + + tags.Should().ContainAll(ProcessTags.EntrypointName, ProcessTags.EntrypointBasedir, ProcessTags.EntrypointWorkdir); + tags.Split(',').Should().BeInAscendingOrder(); + + // assert on the format, which is key:values in a string, comma separated. + tags.Split(',') + .Should() + .AllSatisfy(s => + { + s.Count(c => c == ':').Should().Be(1); + }); + // cannot really assert on content because it depends on how the tests are run. + } +} From 415a4f4341b904d7ccf5da6f3499ea336a2f7047 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Vandon?= Date: Tue, 14 Oct 2025 16:29:42 +0200 Subject: [PATCH 04/23] take the responsibility of checking the config away from process tags class --- .../MessagePack/SpanMessagePackFormatter.cs | 2 +- .../Agent/MessagePack/TraceChunkModel.cs | 3 +++ tracer/src/Datadog.Trace/ProcessTags.cs | 11 --------- .../Datadog.Trace.Tests/ProcessTagsTests.cs | 23 ++----------------- 4 files changed, 6 insertions(+), 33 deletions(-) diff --git a/tracer/src/Datadog.Trace/Agent/MessagePack/SpanMessagePackFormatter.cs b/tracer/src/Datadog.Trace/Agent/MessagePack/SpanMessagePackFormatter.cs index 12c3e788e89c..c0b53d28b1a5 100644 --- a/tracer/src/Datadog.Trace/Agent/MessagePack/SpanMessagePackFormatter.cs +++ b/tracer/src/Datadog.Trace/Agent/MessagePack/SpanMessagePackFormatter.cs @@ -605,7 +605,7 @@ private int WriteTags(ref byte[] bytes, int offset, in SpanModel model, ITagProc } // Process tags will be sent only once per trace - if (model.IsFirstSpanInChunk) + if (model.IsFirstSpanInChunk && model.TraceChunk.ShouldPropagateProcessTags) { var processTags = ProcessTags.SerializedTags; var processTagsRawBytes = MessagePackStringCache.GetProcessTagsBytes(processTags); diff --git a/tracer/src/Datadog.Trace/Agent/MessagePack/TraceChunkModel.cs b/tracer/src/Datadog.Trace/Agent/MessagePack/TraceChunkModel.cs index eb3288c877e9..1dda97f204b5 100644 --- a/tracer/src/Datadog.Trace/Agent/MessagePack/TraceChunkModel.cs +++ b/tracer/src/Datadog.Trace/Agent/MessagePack/TraceChunkModel.cs @@ -63,6 +63,8 @@ internal readonly struct TraceChunkModel public readonly ImmutableAzureAppServiceSettings? AzureAppServiceSettings = null; + public readonly bool ShouldPropagateProcessTags = true; + public readonly bool IsApmEnabled = true; /// @@ -108,6 +110,7 @@ private TraceChunkModel(in ArraySegment spans, TraceContext? traceContext, { IsRunningInAzureAppService = settings.IsRunningInAzureAppService; AzureAppServiceSettings = settings.AzureAppServiceMetadata; + ShouldPropagateProcessTags = settings.PropagateProcessTags; IsApmEnabled = settings.ApmTracingEnabled; } diff --git a/tracer/src/Datadog.Trace/ProcessTags.cs b/tracer/src/Datadog.Trace/ProcessTags.cs index 35f848138849..24bb1c38c3be 100644 --- a/tracer/src/Datadog.Trace/ProcessTags.cs +++ b/tracer/src/Datadog.Trace/ProcessTags.cs @@ -31,12 +31,6 @@ private static Dictionary LoadBaseTags() { var tags = new Dictionary(); - if (!Tracer.Instance.Settings.PropagateProcessTags) - { - // do not collect anything when disabled - return tags; - } - var entrypointFullName = Assembly.GetEntryAssembly()?.EntryPoint?.DeclaringType?.FullName; if (!string.IsNullOrEmpty(entrypointFullName)) { @@ -71,9 +65,4 @@ private static string NormalizeTagValue(string tagValue) // which we don't want because we use it as a key/value separator. return TraceUtil.NormalizeTag(tagValue).Replace(oldChar: ':', newChar: '_'); } - - internal static void ResetForTests() - { - _lazySerializedTags = new Lazy(GetSerializedTags); - } } diff --git a/tracer/test/Datadog.Trace.Tests/ProcessTagsTests.cs b/tracer/test/Datadog.Trace.Tests/ProcessTagsTests.cs index cac5efd6cda1..0476bbe781ca 100644 --- a/tracer/test/Datadog.Trace.Tests/ProcessTagsTests.cs +++ b/tracer/test/Datadog.Trace.Tests/ProcessTagsTests.cs @@ -3,9 +3,7 @@ // This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. // -using System.Collections.Generic; using System.Linq; -using Datadog.Trace.Configuration; using FluentAssertions; using Xunit; @@ -13,33 +11,16 @@ namespace Datadog.Trace.Tests; public class ProcessTagsTests { - public ProcessTagsTests() - { - ProcessTags.ResetForTests(); - } - - [Fact] - public void EmptyIfDisabled() - { - Tracer.Configure(TracerSettings.Create(new Dictionary())); - - ProcessTags.SerializedTags.Should().BeEmpty(); - } - [Fact] public void TagsPresentWhenEnabled() { - Tracer.Configure(TracerSettings.Create(new Dictionary - { - [ConfigurationKeys.PropagateProcessTags] = "true" - })); - var tags = ProcessTags.SerializedTags; tags.Should().ContainAll(ProcessTags.EntrypointName, ProcessTags.EntrypointBasedir, ProcessTags.EntrypointWorkdir); + tags.Split(',').Should().BeInAscendingOrder(); - // assert on the format, which is key:values in a string, comma separated. + // assert on the format, which is key:values in a string, comma separated. tags.Split(',') .Should() .AllSatisfy(s => From dab09766fc750d2fef4fa84925933ad5d03b6e11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Vandon?= Date: Tue, 14 Oct 2025 16:44:26 +0200 Subject: [PATCH 05/23] add settings tests --- .../Configuration/TracerSettingsTests.cs | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tracer/test/Datadog.Trace.Tests/Configuration/TracerSettingsTests.cs b/tracer/test/Datadog.Trace.Tests/Configuration/TracerSettingsTests.cs index cd280601583c..3d4700516e1f 100644 --- a/tracer/test/Datadog.Trace.Tests/Configuration/TracerSettingsTests.cs +++ b/tracer/test/Datadog.Trace.Tests/Configuration/TracerSettingsTests.cs @@ -987,5 +987,29 @@ public void PartialFlushMinSpans(string value, int expected) var settings = new TracerSettings(source); settings.PartialFlushMinSpans.Should().Be(expected); } + + [Theory] + [MemberData(nameof(BooleanTestCases), false)] + public void ProcessTagsEnabled(string value, bool expected) + { + var source = CreateConfigurationSource((ConfigurationKeys.PropagateProcessTags, value)); + var settings = new TracerSettings(source); + + settings.PropagateProcessTags.Should().Be(expected); + } + + [Theory] + [InlineData(null, false)] + [InlineData("", false)] + [InlineData("none", false)] + [InlineData("all", true)] + [InlineData(ConfigurationKeys.PropagateProcessTags, true)] + public void ProcessTagsEnabledIfExperimentalEnabled(string value, bool expected) + { + var source = CreateConfigurationSource((ConfigurationKeys.ExperimentalFeaturesEnabled, value)); + var settings = new TracerSettings(source); + + settings.PropagateProcessTags.Should().Be(expected); + } } } From 1899d1e2e85c1abe6f2eaf2ee7fb82335bc31f2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Vandon?= Date: Tue, 14 Oct 2025 17:36:08 +0200 Subject: [PATCH 06/23] add messagepack test --- .../SpanMessagePackFormatterTests.cs | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/tracer/test/Datadog.Trace.Tests/Agent/MessagePack/SpanMessagePackFormatterTests.cs b/tracer/test/Datadog.Trace.Tests/Agent/MessagePack/SpanMessagePackFormatterTests.cs index 847807e6716d..08198804a199 100644 --- a/tracer/test/Datadog.Trace.Tests/Agent/MessagePack/SpanMessagePackFormatterTests.cs +++ b/tracer/test/Datadog.Trace.Tests/Agent/MessagePack/SpanMessagePackFormatterTests.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; +using System.Collections.Specialized; using System.Diagnostics; using System.IO; using System.Linq; @@ -123,6 +124,49 @@ public void SerializeSpans() } } + [Theory] + [InlineData(true)] + [InlineData(false)] + public void ProcessTags_Serialization(bool enabled) + { + var formatter = SpanFormatterResolver.Instance.GetFormatter(); + + var settings = new TracerSettings(new NameValueConfigurationSource(new NameValueCollection { { ConfigurationKeys.PropagateProcessTags, enabled ? "true" : "false" } })); + var tracerMock = new Mock(); + tracerMock.Setup(t => t.Settings).Returns(settings); + + var parentContext = new SpanContext(new TraceId(Upper: 0, Lower: 1), spanId: 2, (int)SamplingPriority.UserKeep, "ServiceName1", "origin1"); + + var spans = new[] + { + new Span(new SpanContext(parentContext, new TraceContext(tracerMock.Object), "ServiceName1"), DateTimeOffset.UtcNow), + new Span(new SpanContext(parentContext, new TraceContext(tracerMock.Object), "ServiceName1"), DateTimeOffset.UtcNow) + }; + + foreach (var span in spans) + { + span.SetDuration(TimeSpan.FromSeconds(1)); + } + + var traceChunk = new TraceChunkModel(new ArraySegment(spans)); + + byte[] bytes = []; + + var length = formatter.Serialize(ref bytes, offset: 0, traceChunk, SpanFormatterResolver.Instance); + var result = global::MessagePack.MessagePackSerializer.Deserialize(new ArraySegment(bytes, offset: 0, length)); + + if (enabled) + { + result[0].Tags.Should().ContainKey(Tags.ProcessTags); + } + else + { + result[0].Tags.Should().NotContainKey(Tags.ProcessTags); + } + + result[1].Tags.Should().NotContainKey(Tags.ProcessTags, "process tags only added to first span of trace"); + } + [Fact] public void SpanLink_Tag_Serialization() { From 0226c4952b25784c5c18d06a24f36a3a4a9d5ea0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Vandon?= Date: Wed, 15 Oct 2025 10:16:10 +0200 Subject: [PATCH 07/23] fix --- tracer/src/Datadog.Trace/Agent/MessagePack/TraceChunkModel.cs | 2 +- tracer/test/Datadog.Trace.Tests/ProcessTagsTests.cs | 3 ++- .../test/Datadog.Trace.Tests/Telemetry/config_norm_rules.json | 3 ++- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/tracer/src/Datadog.Trace/Agent/MessagePack/TraceChunkModel.cs b/tracer/src/Datadog.Trace/Agent/MessagePack/TraceChunkModel.cs index 1dda97f204b5..33ee7a249645 100644 --- a/tracer/src/Datadog.Trace/Agent/MessagePack/TraceChunkModel.cs +++ b/tracer/src/Datadog.Trace/Agent/MessagePack/TraceChunkModel.cs @@ -63,7 +63,7 @@ internal readonly struct TraceChunkModel public readonly ImmutableAzureAppServiceSettings? AzureAppServiceSettings = null; - public readonly bool ShouldPropagateProcessTags = true; + public readonly bool ShouldPropagateProcessTags = false; public readonly bool IsApmEnabled = true; diff --git a/tracer/test/Datadog.Trace.Tests/ProcessTagsTests.cs b/tracer/test/Datadog.Trace.Tests/ProcessTagsTests.cs index 0476bbe781ca..669825df92e1 100644 --- a/tracer/test/Datadog.Trace.Tests/ProcessTagsTests.cs +++ b/tracer/test/Datadog.Trace.Tests/ProcessTagsTests.cs @@ -16,7 +16,8 @@ public void TagsPresentWhenEnabled() { var tags = ProcessTags.SerializedTags; - tags.Should().ContainAll(ProcessTags.EntrypointName, ProcessTags.EntrypointBasedir, ProcessTags.EntrypointWorkdir); + tags.Should().ContainAll(ProcessTags.EntrypointBasedir, ProcessTags.EntrypointWorkdir); + // EntrypointName may not be present, especially when ran in the CI tags.Split(',').Should().BeInAscendingOrder(); diff --git a/tracer/test/Datadog.Trace.Tests/Telemetry/config_norm_rules.json b/tracer/test/Datadog.Trace.Tests/Telemetry/config_norm_rules.json index c5aea09b0c15..70d7ce2653d4 100644 --- a/tracer/test/Datadog.Trace.Tests/Telemetry/config_norm_rules.json +++ b/tracer/test/Datadog.Trace.Tests/Telemetry/config_norm_rules.json @@ -695,5 +695,6 @@ "DD_CALLSITE_MANAGED_ACTIVATION_ENABLED": "trace_callsite_managed_activation_enabled", "DD_TRACE_GRAPHQL_ERROR_EXTENSIONS": "trace_graphql_error_extensions", "DD_APPLICATION_MONITORING_CONFIG_FILE_ENABLED":"application_monitoring_config_file_enabled", - "DD_TRACE_AZURE_SERVICEBUS_BATCH_LINKS_ENABLED": "trace_azure_servicebus_batch_links_enabled" + "DD_TRACE_AZURE_SERVICEBUS_BATCH_LINKS_ENABLED": "trace_azure_servicebus_batch_links_enabled", + "DD_EXPERIMENTAL_PROPAGATE_PROCESS_TAGS_ENABLED": "experimental_propagate_process_tags_enabled" } From 99eba2505977edcb7c8785396a9b2b13cf0ed314 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Vandon?= Date: Tue, 21 Oct 2025 17:38:20 +0200 Subject: [PATCH 08/23] only write process tags once per buffer --- .../MessagePack/SpanMessagePackFormatter.cs | 2 +- .../Agent/MessagePack/SpanModel.cs | 6 +++++- .../Agent/MessagePack/TraceChunkModel.cs | 17 +++++++++++------ tracer/src/Datadog.Trace/Agent/SpanBuffer.cs | 2 +- 4 files changed, 18 insertions(+), 9 deletions(-) diff --git a/tracer/src/Datadog.Trace/Agent/MessagePack/SpanMessagePackFormatter.cs b/tracer/src/Datadog.Trace/Agent/MessagePack/SpanMessagePackFormatter.cs index c0b53d28b1a5..776b43400884 100644 --- a/tracer/src/Datadog.Trace/Agent/MessagePack/SpanMessagePackFormatter.cs +++ b/tracer/src/Datadog.Trace/Agent/MessagePack/SpanMessagePackFormatter.cs @@ -605,7 +605,7 @@ private int WriteTags(ref byte[] bytes, int offset, in SpanModel model, ITagProc } // Process tags will be sent only once per trace - if (model.IsFirstSpanInChunk && model.TraceChunk.ShouldPropagateProcessTags) + if (model.IsFirstSpanInChunk && model.IsInFirstChunkOfBuffer && model.TraceChunk.ShouldPropagateProcessTags) { var processTags = ProcessTags.SerializedTags; var processTagsRawBytes = MessagePackStringCache.GetProcessTagsBytes(processTags); diff --git a/tracer/src/Datadog.Trace/Agent/MessagePack/SpanModel.cs b/tracer/src/Datadog.Trace/Agent/MessagePack/SpanModel.cs index 57ea85167e39..f1a0568ecf50 100644 --- a/tracer/src/Datadog.Trace/Agent/MessagePack/SpanModel.cs +++ b/tracer/src/Datadog.Trace/Agent/MessagePack/SpanModel.cs @@ -31,17 +31,21 @@ internal readonly struct SpanModel public readonly bool IsFirstSpanInChunk; + public readonly bool IsInFirstChunkOfBuffer; + public SpanModel( Span span, in TraceChunkModel traceChunk, bool isLocalRoot, bool isChunkOrphan, - bool isFirstSpanInChunk) + bool isFirstSpanInChunk, + bool isInFirstChunkOfBuffer) { Span = span; TraceChunk = traceChunk; IsLocalRoot = isLocalRoot; IsChunkOrphan = isChunkOrphan; IsFirstSpanInChunk = isFirstSpanInChunk; + IsInFirstChunkOfBuffer = isInFirstChunkOfBuffer; } } diff --git a/tracer/src/Datadog.Trace/Agent/MessagePack/TraceChunkModel.cs b/tracer/src/Datadog.Trace/Agent/MessagePack/TraceChunkModel.cs index 33ee7a249645..ac3ee11d5a1b 100644 --- a/tracer/src/Datadog.Trace/Agent/MessagePack/TraceChunkModel.cs +++ b/tracer/src/Datadog.Trace/Agent/MessagePack/TraceChunkModel.cs @@ -33,6 +33,8 @@ internal readonly struct TraceChunkModel public readonly int? SamplingPriority = null; + public readonly bool IsFirstChunkInBuffer = false; + public readonly string? SamplingMechanism = null; public readonly double? AppliedSamplingRate = null; @@ -72,8 +74,9 @@ internal readonly struct TraceChunkModel /// /// The spans that will be within this . /// Optional sampling priority to override the sampling priority. - public TraceChunkModel(in ArraySegment spans, int? samplingPriority = null) - : this(spans, TraceContext.GetTraceContext(spans), samplingPriority) + /// marks if this is the first chunk being written to the buffer that then gets sent to the agent + public TraceChunkModel(in ArraySegment spans, int? samplingPriority = null, bool isFirstChunkInBuffer = false) + : this(spans, TraceContext.GetTraceContext(spans), samplingPriority, isFirstChunkInBuffer) { // since all we have is an array of spans, use the trace context from the first span // to get the other values we need (sampling priority, origin, trace tags, etc) for now. @@ -82,11 +85,12 @@ public TraceChunkModel(in ArraySegment spans, int? samplingPriority = null } // used only to chain constructors - private TraceChunkModel(in ArraySegment spans, TraceContext? traceContext, int? samplingPriority) + private TraceChunkModel(in ArraySegment spans, TraceContext? traceContext, int? samplingPriority, bool isFirstChunkInBuffer) : this(spans, traceContext?.RootSpan) { // sampling decision override takes precedence over TraceContext.SamplingPriority SamplingPriority = samplingPriority; + IsFirstChunkInBuffer = isFirstChunkInBuffer; if (traceContext is not null) { @@ -203,9 +207,10 @@ public SpanModel GetSpanModel(int spanIndex) return new SpanModel( span, this, - isLocalRoot: isLocalRoot, - isChunkOrphan: isChunkOrphan, - isFirstSpanInChunk: isFirstSpan); + isLocalRoot, + isChunkOrphan, + isFirstSpan, + IsFirstChunkInBuffer); } /// diff --git a/tracer/src/Datadog.Trace/Agent/SpanBuffer.cs b/tracer/src/Datadog.Trace/Agent/SpanBuffer.cs index 66c142b0c8af..354547cd7f4e 100644 --- a/tracer/src/Datadog.Trace/Agent/SpanBuffer.cs +++ b/tracer/src/Datadog.Trace/Agent/SpanBuffer.cs @@ -91,7 +91,7 @@ public WriteStatus TryWrite(ArraySegment spans, ref byte[] temporaryBuffer // to get the other values we need (sampling priority, origin, trace tags, etc) for now. // the idea is that as we refactor further, we can pass more than just the spans, // and these values can come directly from the trace context. - var traceChunk = new TraceChunkModel(spans, samplingPriority); + var traceChunk = new TraceChunkModel(spans, samplingPriority, TraceCount == 0); // We don't know what the serialized size of the payload will be, // so we need to write to a temporary buffer first From b49fc26810e96d8711e80dd2c2a9cb83b52d7c91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Vandon?= Date: Wed, 22 Oct 2025 14:49:58 +0200 Subject: [PATCH 09/23] more testing --- .../SpanMessagePackFormatterTests.cs | 18 ++++---- .../Agent/SpanBufferTests.cs | 42 +++++++++++++++++++ 2 files changed, 51 insertions(+), 9 deletions(-) diff --git a/tracer/test/Datadog.Trace.Tests/Agent/MessagePack/SpanMessagePackFormatterTests.cs b/tracer/test/Datadog.Trace.Tests/Agent/MessagePack/SpanMessagePackFormatterTests.cs index ae845f9e90df..97b008b57194 100644 --- a/tracer/test/Datadog.Trace.Tests/Agent/MessagePack/SpanMessagePackFormatterTests.cs +++ b/tracer/test/Datadog.Trace.Tests/Agent/MessagePack/SpanMessagePackFormatterTests.cs @@ -128,22 +128,22 @@ public void SerializeSpans() } [Theory] - [InlineData(true)] - [InlineData(false)] - public void ProcessTags_Serialization(bool enabled) + [InlineData(true, true)] + [InlineData(true, false)] + [InlineData(false, true)] + public void ProcessTags_Serialization(bool enabled, bool firstChunk) { var formatter = SpanFormatterResolver.Instance.GetFormatter(); var settings = new TracerSettings(new NameValueConfigurationSource(new NameValueCollection { { ConfigurationKeys.PropagateProcessTags, enabled ? "true" : "false" } })); - var tracerMock = new Mock(); - tracerMock.Setup(t => t.Settings).Returns(settings); + var tracer = TracerHelper.Create(settings); var parentContext = new SpanContext(new TraceId(Upper: 0, Lower: 1), spanId: 2, (int)SamplingPriority.UserKeep, "ServiceName1", "origin1"); var spans = new[] { - new Span(new SpanContext(parentContext, new TraceContext(tracerMock.Object), "ServiceName1"), DateTimeOffset.UtcNow), - new Span(new SpanContext(parentContext, new TraceContext(tracerMock.Object), "ServiceName1"), DateTimeOffset.UtcNow) + new Span(new SpanContext(parentContext, new TraceContext(tracer), "ServiceName1"), DateTimeOffset.UtcNow), + new Span(new SpanContext(parentContext, new TraceContext(tracer), "ServiceName1"), DateTimeOffset.UtcNow) }; foreach (var span in spans) @@ -151,14 +151,14 @@ public void ProcessTags_Serialization(bool enabled) span.SetDuration(TimeSpan.FromSeconds(1)); } - var traceChunk = new TraceChunkModel(new ArraySegment(spans)); + var traceChunk = new TraceChunkModel(new ArraySegment(spans), isFirstChunkInBuffer: firstChunk); byte[] bytes = []; var length = formatter.Serialize(ref bytes, offset: 0, traceChunk, SpanFormatterResolver.Instance); var result = global::MessagePack.MessagePackSerializer.Deserialize(new ArraySegment(bytes, offset: 0, length)); - if (enabled) + if (enabled && firstChunk) { result[0].Tags.Should().ContainKey(Tags.ProcessTags); } diff --git a/tracer/test/Datadog.Trace.Tests/Agent/SpanBufferTests.cs b/tracer/test/Datadog.Trace.Tests/Agent/SpanBufferTests.cs index 53f388c586f5..08259c207715 100644 --- a/tracer/test/Datadog.Trace.Tests/Agent/SpanBufferTests.cs +++ b/tracer/test/Datadog.Trace.Tests/Agent/SpanBufferTests.cs @@ -4,12 +4,15 @@ // using System; +using System.Collections.Generic; using System.Linq; using Datadog.Trace.Agent; using Datadog.Trace.Agent.MessagePack; using Datadog.Trace.TestHelpers; +using Datadog.Trace.Vendors.MessagePack.Formatters; using FluentAssertions; using MessagePack; // use nuget MessagePack to deserialize +using Moq; using Xunit; namespace Datadog.Trace.Tests.Agent @@ -126,6 +129,28 @@ public void TemporaryBufferSizeLimit() temporaryBuffer.Length.Should().BeLessThanOrEqualTo(512, because: "the size of the temporary buffer shouldn't exceed twice the limit"); } + [Fact] + public void IsFirstChunkInBuffer_FirstChunkIsTrue_SubsequentChunksAreFalse() + { + var interceptedChunks = new List(); + var interceptingFormatter = new InterceptingTraceChunkFormatter(interceptedChunks); + var mockResolver = new Mock(); + mockResolver.Setup(r => r.GetFormatter()).Returns(interceptingFormatter); + + var buffer = new SpanBuffer(maxBufferSize: 256, mockResolver.Object); + var temporaryBuffer = new byte[256]; + + var firstSpanArray = CreateTraceChunk(2); + var secondSpanArray = CreateTraceChunk(spanCount: 2, startingId: 10); + + buffer.TryWrite(firstSpanArray, ref temporaryBuffer).Should().Be(SpanBuffer.WriteStatus.Success); + buffer.TryWrite(secondSpanArray, ref temporaryBuffer).Should().Be(SpanBuffer.WriteStatus.Success); + + interceptedChunks.Should().HaveCount(2); + interceptedChunks[0].IsFirstChunkInBuffer.Should().BeTrue(); + interceptedChunks[1].IsFirstChunkInBuffer.Should().BeFalse(); + } + private static ArraySegment CreateTraceChunk(int spanCount, ulong startingId = 1) { var spans = new Span[spanCount]; @@ -138,5 +163,22 @@ private static ArraySegment CreateTraceChunk(int spanCount, ulong starting return new ArraySegment(spans); } + + /// + /// practical mock, because the presence of the ref modifier on bytes makes it not work well with Moq. + /// + private class InterceptingTraceChunkFormatter(List interceptedChunks) : IMessagePackFormatter + { + public int Serialize(ref byte[] bytes, int offset, TraceChunkModel value, Vendors.MessagePack.IFormatterResolver formatterResolver) + { + interceptedChunks.Add(value); + return 50; // Return a reasonable serialized size + } + + public TraceChunkModel Deserialize(byte[] bytes, int offset, Vendors.MessagePack.IFormatterResolver formatterResolver, out int readSize) + { + throw new NotImplementedException("Deserialization not needed for this test"); + } + } } } From 39083e81e3e35f30173be4b29fdae4a3035b16a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Vandon?= Date: Thu, 23 Oct 2025 10:57:06 +0200 Subject: [PATCH 10/23] name arg Co-authored-by: Lucas Pimentel --- tracer/src/Datadog.Trace/Agent/SpanBuffer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tracer/src/Datadog.Trace/Agent/SpanBuffer.cs b/tracer/src/Datadog.Trace/Agent/SpanBuffer.cs index 354547cd7f4e..b077ef996d44 100644 --- a/tracer/src/Datadog.Trace/Agent/SpanBuffer.cs +++ b/tracer/src/Datadog.Trace/Agent/SpanBuffer.cs @@ -91,7 +91,7 @@ public WriteStatus TryWrite(ArraySegment spans, ref byte[] temporaryBuffer // to get the other values we need (sampling priority, origin, trace tags, etc) for now. // the idea is that as we refactor further, we can pass more than just the spans, // and these values can come directly from the trace context. - var traceChunk = new TraceChunkModel(spans, samplingPriority, TraceCount == 0); + var traceChunk = new TraceChunkModel(spans, samplingPriority, isFirstChunkInBuffer: TraceCount == 0); // We don't know what the serialized size of the payload will be, // so we need to write to a temporary buffer first From bcb45ad4c18af77a740387586550caf9fc99c06c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Vandon?= Date: Thu, 23 Oct 2025 11:06:07 +0200 Subject: [PATCH 11/23] typo in comment Co-authored-by: Lucas Pimentel --- tracer/src/Datadog.Trace/ProcessTags.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tracer/src/Datadog.Trace/ProcessTags.cs b/tracer/src/Datadog.Trace/ProcessTags.cs index 24bb1c38c3be..904531e5dbef 100644 --- a/tracer/src/Datadog.Trace/ProcessTags.cs +++ b/tracer/src/Datadog.Trace/ProcessTags.cs @@ -50,7 +50,7 @@ private static Dictionary LoadBaseTags() private static string GetLastPathSegment(string directoryPath) { // Path.GetFileName returns an empty string if the path ends with a '/'. - // We could use Path.TrimEndingDirectorySeparator instead of the trim here, but it's not available on .netframework + // We could use Path.TrimEndingDirectorySeparator instead of the trim here, but it's not available on .NET Framework return Path.GetFileName(directoryPath.TrimEnd('\\', '/')); } From c941b825bbdedbe486758f37400d58f833b2cce4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Vandon?= Date: Thu, 23 Oct 2025 11:08:59 +0200 Subject: [PATCH 12/23] comment typo Co-authored-by: Lucas Pimentel --- tracer/src/Datadog.Trace/Tags.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tracer/src/Datadog.Trace/Tags.cs b/tracer/src/Datadog.Trace/Tags.cs index 027e56e25d49..a4205be8a446 100644 --- a/tracer/src/Datadog.Trace/Tags.cs +++ b/tracer/src/Datadog.Trace/Tags.cs @@ -507,7 +507,7 @@ public static partial class Tags internal const string RuntimeFamily = "_dd.runtime_family"; /// - /// contains a serialized list of process tags, that can be used in the backend for service renaming + /// Contains a serialized list of process tags, that can be used in the backend for service renaming. /// /// internal const string ProcessTags = "_dd.tags.process"; From e4b5c5897ffa0e063e718a1e14858fdc2ccf5fec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Vandon?= Date: Thu, 23 Oct 2025 11:29:41 +0200 Subject: [PATCH 13/23] adress comments # Conflicts: # tracer/src/Datadog.Trace/ProcessTags.cs --- .../MessagePack/MessagePackStringCache.cs | 6 ----- .../MessagePack/SpanMessagePackFormatter.cs | 18 +++++++++------ .../Agent/MessagePack/SpanModel.cs | 6 +---- .../Agent/MessagePack/TraceChunkModel.cs | 15 ++++++------- tracer/src/Datadog.Trace/ProcessTags.cs | 22 ++++++++++++++----- .../SpanMessagePackFormatterTests.cs | 2 +- .../Agent/SpanBufferTests.cs | 4 ++-- 7 files changed, 38 insertions(+), 35 deletions(-) diff --git a/tracer/src/Datadog.Trace/Agent/MessagePack/MessagePackStringCache.cs b/tracer/src/Datadog.Trace/Agent/MessagePack/MessagePackStringCache.cs index 71367cfb0b6e..bee7b0ea3bb7 100644 --- a/tracer/src/Datadog.Trace/Agent/MessagePack/MessagePackStringCache.cs +++ b/tracer/src/Datadog.Trace/Agent/MessagePack/MessagePackStringCache.cs @@ -34,7 +34,6 @@ internal static class MessagePackStringCache // NOTE: all of these can be cached in SpanMessagePackFormatter as static byte[] // since they never change over the lifetime of a process - private static CachedBytes _processTags; private static CachedBytes _gitCommitSha; private static CachedBytes _gitRepositoryUrl; private static CachedBytes _aasSiteNameBytes; @@ -100,11 +99,6 @@ public static void Clear() return GetBytes(service, ref _service); } - public static byte[]? GetProcessTagsBytes(string processTags) - { - return GetBytes(processTags, ref _processTags); - } - public static byte[]? GetAzureAppServiceKeyBytes(string key, string? value) { switch (key) diff --git a/tracer/src/Datadog.Trace/Agent/MessagePack/SpanMessagePackFormatter.cs b/tracer/src/Datadog.Trace/Agent/MessagePack/SpanMessagePackFormatter.cs index 776b43400884..bfb3707d2241 100644 --- a/tracer/src/Datadog.Trace/Agent/MessagePack/SpanMessagePackFormatter.cs +++ b/tracer/src/Datadog.Trace/Agent/MessagePack/SpanMessagePackFormatter.cs @@ -68,6 +68,12 @@ internal class SpanMessagePackFormatter : IMessagePackFormatter private readonly byte[] _runtimeIdNameBytes = StringEncoding.UTF8.GetBytes(Trace.Tags.RuntimeId); private readonly byte[] _runtimeIdValueBytes = StringEncoding.UTF8.GetBytes(Tracer.RuntimeId); + // using a Lazy here to make sure we don't compute the value of the process tags too early in the life of the app, + // some values may neeb a bit of time to be accessible. + // With this construct, it should be queried after the first span(s) get closed, which should be late enough. + private readonly Lazy _processTagsValueBytes = new(() => StringEncoding.UTF8.GetBytes(ProcessTags.SerializedTags)); + private readonly byte[] _processTagsNameBytes = StringEncoding.UTF8.GetBytes(Tags.ProcessTags); + private readonly byte[] _environmentNameBytes = StringEncoding.UTF8.GetBytes(Trace.Tags.Env); private readonly byte[] _gitCommitShaNameBytes = StringEncoding.UTF8.GetBytes(Trace.Tags.GitCommitSha); private readonly byte[] _gitRepositoryUrlNameBytes = StringEncoding.UTF8.GetBytes(Trace.Tags.GitRepositoryUrl); @@ -75,7 +81,6 @@ internal class SpanMessagePackFormatter : IMessagePackFormatter private readonly byte[] _originNameBytes = StringEncoding.UTF8.GetBytes(Trace.Tags.Origin); private readonly byte[] _lastParentIdBytes = StringEncoding.UTF8.GetBytes(Trace.Tags.LastParentId); private readonly byte[] _baseServiceNameBytes = StringEncoding.UTF8.GetBytes(Trace.Tags.BaseService); - private readonly byte[] _processTagsNameBytes = StringEncoding.UTF8.GetBytes(Tags.ProcessTags); // numeric tags private readonly byte[] _metricsBytes = StringEncoding.UTF8.GetBytes("metrics"); @@ -604,16 +609,15 @@ private int WriteTags(ref byte[] bytes, int offset, in SpanModel model, ITagProc } } - // Process tags will be sent only once per trace - if (model.IsFirstSpanInChunk && model.IsInFirstChunkOfBuffer && model.TraceChunk.ShouldPropagateProcessTags) + // Process tags will be sent only once per buffer/payload (one payload can contain many chunks from different traces) + if (model.IsFirstSpanInChunk && model.TraceChunk.IsFirstChunkInPayload && model.TraceChunk.ShouldPropagateProcessTags) { - var processTags = ProcessTags.SerializedTags; - var processTagsRawBytes = MessagePackStringCache.GetProcessTagsBytes(processTags); - if (!string.IsNullOrEmpty(processTags)) + var processTagsRawBytes = _processTagsValueBytes.Value; + if (processTagsRawBytes.Length > 0) { count++; offset += MessagePackBinary.WriteStringBytes(ref bytes, offset, _processTagsNameBytes); - offset += MessagePackBinary.WriteRaw(ref bytes, offset, processTagsRawBytes); + offset += MessagePackBinary.WriteStringBytes(ref bytes, offset, processTagsRawBytes); } } diff --git a/tracer/src/Datadog.Trace/Agent/MessagePack/SpanModel.cs b/tracer/src/Datadog.Trace/Agent/MessagePack/SpanModel.cs index f1a0568ecf50..57ea85167e39 100644 --- a/tracer/src/Datadog.Trace/Agent/MessagePack/SpanModel.cs +++ b/tracer/src/Datadog.Trace/Agent/MessagePack/SpanModel.cs @@ -31,21 +31,17 @@ internal readonly struct SpanModel public readonly bool IsFirstSpanInChunk; - public readonly bool IsInFirstChunkOfBuffer; - public SpanModel( Span span, in TraceChunkModel traceChunk, bool isLocalRoot, bool isChunkOrphan, - bool isFirstSpanInChunk, - bool isInFirstChunkOfBuffer) + bool isFirstSpanInChunk) { Span = span; TraceChunk = traceChunk; IsLocalRoot = isLocalRoot; IsChunkOrphan = isChunkOrphan; IsFirstSpanInChunk = isFirstSpanInChunk; - IsInFirstChunkOfBuffer = isInFirstChunkOfBuffer; } } diff --git a/tracer/src/Datadog.Trace/Agent/MessagePack/TraceChunkModel.cs b/tracer/src/Datadog.Trace/Agent/MessagePack/TraceChunkModel.cs index ac3ee11d5a1b..96c44a0127b0 100644 --- a/tracer/src/Datadog.Trace/Agent/MessagePack/TraceChunkModel.cs +++ b/tracer/src/Datadog.Trace/Agent/MessagePack/TraceChunkModel.cs @@ -33,7 +33,7 @@ internal readonly struct TraceChunkModel public readonly int? SamplingPriority = null; - public readonly bool IsFirstChunkInBuffer = false; + public readonly bool IsFirstChunkInPayload = false; public readonly string? SamplingMechanism = null; @@ -74,9 +74,9 @@ internal readonly struct TraceChunkModel /// /// The spans that will be within this . /// Optional sampling priority to override the sampling priority. - /// marks if this is the first chunk being written to the buffer that then gets sent to the agent - public TraceChunkModel(in ArraySegment spans, int? samplingPriority = null, bool isFirstChunkInBuffer = false) - : this(spans, TraceContext.GetTraceContext(spans), samplingPriority, isFirstChunkInBuffer) + /// marks if this is the first chunk being written to the buffer that then gets sent to the agent + public TraceChunkModel(in ArraySegment spans, int? samplingPriority = null, bool isFirstChunkInPayload = false) + : this(spans, TraceContext.GetTraceContext(spans), samplingPriority, isFirstChunkInPayload) { // since all we have is an array of spans, use the trace context from the first span // to get the other values we need (sampling priority, origin, trace tags, etc) for now. @@ -85,12 +85,12 @@ public TraceChunkModel(in ArraySegment spans, int? samplingPriority = null } // used only to chain constructors - private TraceChunkModel(in ArraySegment spans, TraceContext? traceContext, int? samplingPriority, bool isFirstChunkInBuffer) + private TraceChunkModel(in ArraySegment spans, TraceContext? traceContext, int? samplingPriority, bool isFirstChunkInPayload) : this(spans, traceContext?.RootSpan) { // sampling decision override takes precedence over TraceContext.SamplingPriority SamplingPriority = samplingPriority; - IsFirstChunkInBuffer = isFirstChunkInBuffer; + IsFirstChunkInPayload = isFirstChunkInPayload; if (traceContext is not null) { @@ -209,8 +209,7 @@ public SpanModel GetSpanModel(int spanIndex) this, isLocalRoot, isChunkOrphan, - isFirstSpan, - IsFirstChunkInBuffer); + isFirstSpan); } /// diff --git a/tracer/src/Datadog.Trace/ProcessTags.cs b/tracer/src/Datadog.Trace/ProcessTags.cs index 904531e5dbef..87a507f39e93 100644 --- a/tracer/src/Datadog.Trace/ProcessTags.cs +++ b/tracer/src/Datadog.Trace/ProcessTags.cs @@ -10,7 +10,10 @@ using System.IO; using System.Linq; using System.Reflection; +using System.Text; using Datadog.Trace.Processors; +using Datadog.Trace.VendoredMicrosoftCode.System.Collections.Immutable; +using Datadog.Trace.Vendors.Newtonsoft.Json.Utilities; namespace Datadog.Trace; @@ -20,16 +23,16 @@ internal static class ProcessTags public const string EntrypointBasedir = "entrypoint.basedir"; public const string EntrypointWorkdir = "entrypoint.workdir"; - private static Lazy _lazySerializedTags = new(GetSerializedTags); + private static readonly Lazy LazySerializedTags = new(GetSerializedTags); public static string SerializedTags { - get => _lazySerializedTags.Value; + get => LazySerializedTags.Value; } - private static Dictionary LoadBaseTags() + private static SortedDictionary LoadBaseTags() { - var tags = new Dictionary(); + var tags = new SortedDictionary(); var entrypointFullName = Assembly.GetEntryAssembly()?.EntryPoint?.DeclaringType?.FullName; if (!string.IsNullOrEmpty(entrypointFullName)) @@ -51,12 +54,19 @@ private static string GetLastPathSegment(string directoryPath) { // Path.GetFileName returns an empty string if the path ends with a '/'. // We could use Path.TrimEndingDirectorySeparator instead of the trim here, but it's not available on .NET Framework - return Path.GetFileName(directoryPath.TrimEnd('\\', '/')); + return Path.GetFileName(directoryPath.TrimEnd('\\').TrimEnd('/')); } private static string GetSerializedTags() { - return string.Join(",", LoadBaseTags().OrderBy(kv => kv.Key).Select(kv => $"{kv.Key}:{NormalizeTagValue(kv.Value)}")); + var serializedTags = new StringBuilder(); + foreach (var kvp in LoadBaseTags()) + { + serializedTags.Append($"{kvp.Key}:{NormalizeTagValue(kvp.Value)},"); + } + + serializedTags.Remove(serializedTags.Length - 1, length: 1); // remove last comma + return serializedTags.ToString(); } private static string NormalizeTagValue(string tagValue) diff --git a/tracer/test/Datadog.Trace.Tests/Agent/MessagePack/SpanMessagePackFormatterTests.cs b/tracer/test/Datadog.Trace.Tests/Agent/MessagePack/SpanMessagePackFormatterTests.cs index 97b008b57194..5348ecd64397 100644 --- a/tracer/test/Datadog.Trace.Tests/Agent/MessagePack/SpanMessagePackFormatterTests.cs +++ b/tracer/test/Datadog.Trace.Tests/Agent/MessagePack/SpanMessagePackFormatterTests.cs @@ -151,7 +151,7 @@ public void ProcessTags_Serialization(bool enabled, bool firstChunk) span.SetDuration(TimeSpan.FromSeconds(1)); } - var traceChunk = new TraceChunkModel(new ArraySegment(spans), isFirstChunkInBuffer: firstChunk); + var traceChunk = new TraceChunkModel(new ArraySegment(spans), isFirstChunkInPayload: firstChunk); byte[] bytes = []; diff --git a/tracer/test/Datadog.Trace.Tests/Agent/SpanBufferTests.cs b/tracer/test/Datadog.Trace.Tests/Agent/SpanBufferTests.cs index 08259c207715..723d868b7c22 100644 --- a/tracer/test/Datadog.Trace.Tests/Agent/SpanBufferTests.cs +++ b/tracer/test/Datadog.Trace.Tests/Agent/SpanBufferTests.cs @@ -147,8 +147,8 @@ public void IsFirstChunkInBuffer_FirstChunkIsTrue_SubsequentChunksAreFalse() buffer.TryWrite(secondSpanArray, ref temporaryBuffer).Should().Be(SpanBuffer.WriteStatus.Success); interceptedChunks.Should().HaveCount(2); - interceptedChunks[0].IsFirstChunkInBuffer.Should().BeTrue(); - interceptedChunks[1].IsFirstChunkInBuffer.Should().BeFalse(); + interceptedChunks[0].IsFirstChunkInPayload.Should().BeTrue(); + interceptedChunks[1].IsFirstChunkInPayload.Should().BeFalse(); } private static ArraySegment CreateTraceChunk(int spanCount, ulong startingId = 1) From 206784bb5babfd5248a9d0ca78ec516149becb06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Vandon?= Date: Thu, 23 Oct 2025 13:44:03 +0200 Subject: [PATCH 14/23] fix build --- .../src/Datadog.Trace.Trimming/build/Datadog.Trace.Trimming.xml | 1 + tracer/src/Datadog.Trace/Agent/SpanBuffer.cs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/tracer/src/Datadog.Trace.Trimming/build/Datadog.Trace.Trimming.xml b/tracer/src/Datadog.Trace.Trimming/build/Datadog.Trace.Trimming.xml index 9ab08b324954..dc874fc9c298 100644 --- a/tracer/src/Datadog.Trace.Trimming/build/Datadog.Trace.Trimming.xml +++ b/tracer/src/Datadog.Trace.Trimming/build/Datadog.Trace.Trimming.xml @@ -155,6 +155,7 @@ + diff --git a/tracer/src/Datadog.Trace/Agent/SpanBuffer.cs b/tracer/src/Datadog.Trace/Agent/SpanBuffer.cs index b077ef996d44..e0aa492931e8 100644 --- a/tracer/src/Datadog.Trace/Agent/SpanBuffer.cs +++ b/tracer/src/Datadog.Trace/Agent/SpanBuffer.cs @@ -91,7 +91,7 @@ public WriteStatus TryWrite(ArraySegment spans, ref byte[] temporaryBuffer // to get the other values we need (sampling priority, origin, trace tags, etc) for now. // the idea is that as we refactor further, we can pass more than just the spans, // and these values can come directly from the trace context. - var traceChunk = new TraceChunkModel(spans, samplingPriority, isFirstChunkInBuffer: TraceCount == 0); + var traceChunk = new TraceChunkModel(spans, samplingPriority, isFirstChunkInPayload: TraceCount == 0); // We don't know what the serialized size of the payload will be, // so we need to write to a temporary buffer first From 2ff6f917e59a3f85f5535707289e427c7bcb072e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Vandon?= Date: Mon, 27 Oct 2025 11:14:23 +0100 Subject: [PATCH 15/23] typo Co-authored-by: Lucas Pimentel --- .../Datadog.Trace/Agent/MessagePack/SpanMessagePackFormatter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tracer/src/Datadog.Trace/Agent/MessagePack/SpanMessagePackFormatter.cs b/tracer/src/Datadog.Trace/Agent/MessagePack/SpanMessagePackFormatter.cs index bfb3707d2241..13dab77db260 100644 --- a/tracer/src/Datadog.Trace/Agent/MessagePack/SpanMessagePackFormatter.cs +++ b/tracer/src/Datadog.Trace/Agent/MessagePack/SpanMessagePackFormatter.cs @@ -69,7 +69,7 @@ internal class SpanMessagePackFormatter : IMessagePackFormatter private readonly byte[] _runtimeIdValueBytes = StringEncoding.UTF8.GetBytes(Tracer.RuntimeId); // using a Lazy here to make sure we don't compute the value of the process tags too early in the life of the app, - // some values may neeb a bit of time to be accessible. + // some values may need a bit of time to be accessible. // With this construct, it should be queried after the first span(s) get closed, which should be late enough. private readonly Lazy _processTagsValueBytes = new(() => StringEncoding.UTF8.GetBytes(ProcessTags.SerializedTags)); private readonly byte[] _processTagsNameBytes = StringEncoding.UTF8.GetBytes(Tags.ProcessTags); From b02cb335e393f624b9cd9d38725006f299049300 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Vandon?= Date: Mon, 27 Oct 2025 11:22:16 +0100 Subject: [PATCH 16/23] rephrase comment Co-authored-by: Lucas Pimentel --- tracer/src/Datadog.Trace/Agent/MessagePack/TraceChunkModel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tracer/src/Datadog.Trace/Agent/MessagePack/TraceChunkModel.cs b/tracer/src/Datadog.Trace/Agent/MessagePack/TraceChunkModel.cs index 96c44a0127b0..d37221ee36d2 100644 --- a/tracer/src/Datadog.Trace/Agent/MessagePack/TraceChunkModel.cs +++ b/tracer/src/Datadog.Trace/Agent/MessagePack/TraceChunkModel.cs @@ -74,7 +74,7 @@ internal readonly struct TraceChunkModel /// /// The spans that will be within this . /// Optional sampling priority to override the sampling priority. - /// marks if this is the first chunk being written to the buffer that then gets sent to the agent + /// Indicates if this is the first trace chunk being written to the output buffer. public TraceChunkModel(in ArraySegment spans, int? samplingPriority = null, bool isFirstChunkInPayload = false) : this(spans, TraceContext.GetTraceContext(spans), samplingPriority, isFirstChunkInPayload) { From cab50d8b0b4fd90ae546a368f23e4a5650a6e9cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Vandon?= Date: Mon, 27 Oct 2025 11:23:00 +0100 Subject: [PATCH 17/23] address comments pt.1 --- .../src/Datadog.Trace/Agent/MessagePack/TraceChunkModel.cs | 6 +++--- tracer/src/Datadog.Trace/ProcessTags.cs | 3 --- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/tracer/src/Datadog.Trace/Agent/MessagePack/TraceChunkModel.cs b/tracer/src/Datadog.Trace/Agent/MessagePack/TraceChunkModel.cs index d37221ee36d2..4e5dbb41540f 100644 --- a/tracer/src/Datadog.Trace/Agent/MessagePack/TraceChunkModel.cs +++ b/tracer/src/Datadog.Trace/Agent/MessagePack/TraceChunkModel.cs @@ -207,9 +207,9 @@ public SpanModel GetSpanModel(int spanIndex) return new SpanModel( span, this, - isLocalRoot, - isChunkOrphan, - isFirstSpan); + isLocalRoot: isLocalRoot, + isChunkOrphan: isChunkOrphan, + isFirstSpanInChunk: isFirstSpan); } /// diff --git a/tracer/src/Datadog.Trace/ProcessTags.cs b/tracer/src/Datadog.Trace/ProcessTags.cs index 87a507f39e93..62ade3cb5975 100644 --- a/tracer/src/Datadog.Trace/ProcessTags.cs +++ b/tracer/src/Datadog.Trace/ProcessTags.cs @@ -8,12 +8,9 @@ using System; using System.Collections.Generic; using System.IO; -using System.Linq; using System.Reflection; using System.Text; using Datadog.Trace.Processors; -using Datadog.Trace.VendoredMicrosoftCode.System.Collections.Immutable; -using Datadog.Trace.Vendors.Newtonsoft.Json.Utilities; namespace Datadog.Trace; From fb6ffdbc3833ef3fc3d20cdd32b2765787702492 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Vandon?= Date: Mon, 27 Oct 2025 15:01:36 +0100 Subject: [PATCH 18/23] use existing code to get entry assembly --- tracer/src/Datadog.Trace/ProcessTags.cs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/tracer/src/Datadog.Trace/ProcessTags.cs b/tracer/src/Datadog.Trace/ProcessTags.cs index 62ade3cb5975..2cf22236472e 100644 --- a/tracer/src/Datadog.Trace/ProcessTags.cs +++ b/tracer/src/Datadog.Trace/ProcessTags.cs @@ -10,6 +10,7 @@ using System.IO; using System.Reflection; using System.Text; +using Datadog.Trace.Configuration; using Datadog.Trace.Processors; namespace Datadog.Trace; @@ -31,10 +32,13 @@ private static SortedDictionary LoadBaseTags() { var tags = new SortedDictionary(); - var entrypointFullName = Assembly.GetEntryAssembly()?.EntryPoint?.DeclaringType?.FullName; - if (!string.IsNullOrEmpty(entrypointFullName)) + if (EntryAssemblyLocator.GetEntryAssembly() is { } assembly) { - tags.Add(EntrypointName, entrypointFullName!); + var entrypointFullName = assembly.EntryPoint?.DeclaringType?.FullName; + if (!string.IsNullOrEmpty(entrypointFullName)) + { + tags.Add(EntrypointName, entrypointFullName!); + } } tags.Add(EntrypointBasedir, GetLastPathSegment(AppContext.BaseDirectory)); From 2d21dd6bc7781565933e4a2ce465fdb2abdcf8c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Vandon?= Date: Mon, 27 Oct 2025 17:22:24 +0100 Subject: [PATCH 19/23] fix json after bad conflict resolution --- .../test/Datadog.Trace.Tests/Telemetry/config_norm_rules.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tracer/test/Datadog.Trace.Tests/Telemetry/config_norm_rules.json b/tracer/test/Datadog.Trace.Tests/Telemetry/config_norm_rules.json index 0552fa8eb5b5..fe0f460a0adb 100644 --- a/tracer/test/Datadog.Trace.Tests/Telemetry/config_norm_rules.json +++ b/tracer/test/Datadog.Trace.Tests/Telemetry/config_norm_rules.json @@ -701,7 +701,6 @@ "DD_TRACE_GRAPHQL_ERROR_EXTENSIONS": "trace_graphql_error_extensions", "DD_APPLICATION_MONITORING_CONFIG_FILE_ENABLED":"application_monitoring_config_file_enabled", "DD_TRACE_AZURE_SERVICEBUS_BATCH_LINKS_ENABLED": "trace_azure_servicebus_batch_links_enabled", - "DD_TRACE_AZURE_EVENTHUBS_BATCH_LINKS_ENABLED": "trace_azure_eventhubs_batch_links_enabled" - "DD_TRACE_AZURE_SERVICEBUS_BATCH_LINKS_ENABLED": "trace_azure_servicebus_batch_links_enabled", + "DD_TRACE_AZURE_EVENTHUBS_BATCH_LINKS_ENABLED": "trace_azure_eventhubs_batch_links_enabled", "DD_EXPERIMENTAL_PROPAGATE_PROCESS_TAGS_ENABLED": "experimental_propagate_process_tags_enabled" } From d0902d95676e95d1acdcf7a8a3fd3befb48d7134 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Vandon?= Date: Wed, 29 Oct 2025 10:41:39 +0100 Subject: [PATCH 20/23] move test on process tags to its own file --- .../Tagging/ProcessTagsTests.cs | 66 +++++++++++++++++++ .../SpanMessagePackFormatterTests.cs | 44 ------------- 2 files changed, 66 insertions(+), 44 deletions(-) create mode 100644 tracer/test/Datadog.Trace.IntegrationTests/Tagging/ProcessTagsTests.cs diff --git a/tracer/test/Datadog.Trace.IntegrationTests/Tagging/ProcessTagsTests.cs b/tracer/test/Datadog.Trace.IntegrationTests/Tagging/ProcessTagsTests.cs new file mode 100644 index 000000000000..1b2dfd5a2094 --- /dev/null +++ b/tracer/test/Datadog.Trace.IntegrationTests/Tagging/ProcessTagsTests.cs @@ -0,0 +1,66 @@ +// +// 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.Collections.Specialized; +using System.Linq; +using System.Threading.Tasks; +using Datadog.Trace.Agent; +using Datadog.Trace.Configuration; +using Datadog.Trace.TestHelpers; +using Datadog.Trace.TestHelpers.TestTracer; +using FluentAssertions; +using Xunit; + +namespace Datadog.Trace.IntegrationTests.Tagging; + +public class ProcessTagsTests +{ + private readonly MockApi _testApi; + + public ProcessTagsTests() + { + _testApi = new MockApi(); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task ProcessTags_Only_In_First_Span(bool enabled) + { + var settings = new TracerSettings(new NameValueConfigurationSource(new NameValueCollection { { ConfigurationKeys.PropagateProcessTags, enabled ? "true" : "false" } })); + var agentWriter = new AgentWriter(_testApi, statsAggregator: null, statsd: null, automaticFlush: false); + await using var tracer = TracerHelper.Create(settings, agentWriter); + + using (tracer.StartActiveInternal("A")) + using (tracer.StartActiveInternal("AA")) + { + } + + // other trace + using (tracer.StartActiveInternal("B")) + using (tracer.StartActiveInternal("BB")) + { + } + + await tracer.FlushAsync(); + var traceChunks = _testApi.Wait(); + + traceChunks.Should().HaveCount(2); // 2 (small) traces = 2 chunks + if (enabled) + { + // process tags written only to first span of first chunk + traceChunks[0][0].Tags.Should().ContainKey(Tags.ProcessTags); + } + else + { + traceChunks[0][0].Tags.Should().NotContainKey(Tags.ProcessTags); + } + + traceChunks.SelectMany(x => x) // flatten + .Skip(1) // exclude first item that we just checked above + .Should() + .AllSatisfy(s => s.Tags.Should().NotContainKey(Tags.ProcessTags)); + } +} diff --git a/tracer/test/Datadog.Trace.Tests/Agent/MessagePack/SpanMessagePackFormatterTests.cs b/tracer/test/Datadog.Trace.Tests/Agent/MessagePack/SpanMessagePackFormatterTests.cs index 5348ecd64397..d1240bc04027 100644 --- a/tracer/test/Datadog.Trace.Tests/Agent/MessagePack/SpanMessagePackFormatterTests.cs +++ b/tracer/test/Datadog.Trace.Tests/Agent/MessagePack/SpanMessagePackFormatterTests.cs @@ -5,7 +5,6 @@ using System; using System.Collections.Generic; -using System.Collections.Specialized; using System.Diagnostics; using System.IO; using System.Linq; @@ -127,49 +126,6 @@ public void SerializeSpans() } } - [Theory] - [InlineData(true, true)] - [InlineData(true, false)] - [InlineData(false, true)] - public void ProcessTags_Serialization(bool enabled, bool firstChunk) - { - var formatter = SpanFormatterResolver.Instance.GetFormatter(); - - var settings = new TracerSettings(new NameValueConfigurationSource(new NameValueCollection { { ConfigurationKeys.PropagateProcessTags, enabled ? "true" : "false" } })); - var tracer = TracerHelper.Create(settings); - - var parentContext = new SpanContext(new TraceId(Upper: 0, Lower: 1), spanId: 2, (int)SamplingPriority.UserKeep, "ServiceName1", "origin1"); - - var spans = new[] - { - new Span(new SpanContext(parentContext, new TraceContext(tracer), "ServiceName1"), DateTimeOffset.UtcNow), - new Span(new SpanContext(parentContext, new TraceContext(tracer), "ServiceName1"), DateTimeOffset.UtcNow) - }; - - foreach (var span in spans) - { - span.SetDuration(TimeSpan.FromSeconds(1)); - } - - var traceChunk = new TraceChunkModel(new ArraySegment(spans), isFirstChunkInPayload: firstChunk); - - byte[] bytes = []; - - var length = formatter.Serialize(ref bytes, offset: 0, traceChunk, SpanFormatterResolver.Instance); - var result = global::MessagePack.MessagePackSerializer.Deserialize(new ArraySegment(bytes, offset: 0, length)); - - if (enabled && firstChunk) - { - result[0].Tags.Should().ContainKey(Tags.ProcessTags); - } - else - { - result[0].Tags.Should().NotContainKey(Tags.ProcessTags); - } - - result[1].Tags.Should().NotContainKey(Tags.ProcessTags, "process tags only added to first span of trace"); - } - [Fact] public void SpanLink_Tag_Serialization() { From 208c05e496218b88b2cbd746b2ed0720904bdf40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Vandon?= Date: Wed, 29 Oct 2025 12:22:51 +0100 Subject: [PATCH 21/23] replace sorteddict with list --- tracer/src/Datadog.Trace/ProcessTags.cs | 38 ++++++++++--------------- 1 file changed, 15 insertions(+), 23 deletions(-) diff --git a/tracer/src/Datadog.Trace/ProcessTags.cs b/tracer/src/Datadog.Trace/ProcessTags.cs index 2cf22236472e..16baed70310d 100644 --- a/tracer/src/Datadog.Trace/ProcessTags.cs +++ b/tracer/src/Datadog.Trace/ProcessTags.cs @@ -8,7 +8,6 @@ using System; using System.Collections.Generic; using System.IO; -using System.Reflection; using System.Text; using Datadog.Trace.Configuration; using Datadog.Trace.Processors; @@ -28,26 +27,6 @@ public static string SerializedTags get => LazySerializedTags.Value; } - private static SortedDictionary LoadBaseTags() - { - var tags = new SortedDictionary(); - - if (EntryAssemblyLocator.GetEntryAssembly() is { } assembly) - { - var entrypointFullName = assembly.EntryPoint?.DeclaringType?.FullName; - if (!string.IsNullOrEmpty(entrypointFullName)) - { - tags.Add(EntrypointName, entrypointFullName!); - } - } - - tags.Add(EntrypointBasedir, GetLastPathSegment(AppContext.BaseDirectory)); - // workdir can be changed by the code, but we consider that capturing the value when this is called is good enough - tags.Add(EntrypointWorkdir, GetLastPathSegment(Environment.CurrentDirectory)); - - return tags; - } - /// /// From the full path of a directory, get the name of the leaf directory. /// @@ -60,10 +39,23 @@ private static string GetLastPathSegment(string directoryPath) private static string GetSerializedTags() { + // ⚠️ make sure entries are added in alphabetical order of keys + var tags = new List> + { + new(EntrypointBasedir, GetLastPathSegment(AppContext.BaseDirectory)), + new(EntrypointName, EntryAssemblyLocator.GetEntryAssembly()?.EntryPoint?.DeclaringType?.FullName), + // workdir can be changed by the code, but we consider that capturing the value when this is called is good enough + new(EntrypointWorkdir, GetLastPathSegment(Environment.CurrentDirectory)) + }; + + // then normalize values and put all tags in a string var serializedTags = new StringBuilder(); - foreach (var kvp in LoadBaseTags()) + foreach (var kvp in tags) { - serializedTags.Append($"{kvp.Key}:{NormalizeTagValue(kvp.Value)},"); + if (!string.IsNullOrEmpty(kvp.Value)) + { + serializedTags.Append($"{kvp.Key}:{NormalizeTagValue(kvp.Value!)},"); + } } serializedTags.Remove(serializedTags.Length - 1, length: 1); // remove last comma From b6b0fde66884be1a19047114799127eb1c49f6e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Vandon?= Date: Wed, 29 Oct 2025 13:15:06 +0100 Subject: [PATCH 22/23] trimming file --- .../src/Datadog.Trace.Trimming/build/Datadog.Trace.Trimming.xml | 1 - 1 file changed, 1 deletion(-) diff --git a/tracer/src/Datadog.Trace.Trimming/build/Datadog.Trace.Trimming.xml b/tracer/src/Datadog.Trace.Trimming/build/Datadog.Trace.Trimming.xml index 2d572b52243b..6611cdd628ff 100644 --- a/tracer/src/Datadog.Trace.Trimming/build/Datadog.Trace.Trimming.xml +++ b/tracer/src/Datadog.Trace.Trimming/build/Datadog.Trace.Trimming.xml @@ -158,7 +158,6 @@ - From 6238eba3d02458be4dbee7c8ec753ea924fc18e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Vandon?= Date: Thu, 30 Oct 2025 10:49:19 +0100 Subject: [PATCH 23/23] use SB cache --- tracer/src/Datadog.Trace/ProcessTags.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tracer/src/Datadog.Trace/ProcessTags.cs b/tracer/src/Datadog.Trace/ProcessTags.cs index 16baed70310d..699f98396bc3 100644 --- a/tracer/src/Datadog.Trace/ProcessTags.cs +++ b/tracer/src/Datadog.Trace/ProcessTags.cs @@ -11,6 +11,7 @@ using System.Text; using Datadog.Trace.Configuration; using Datadog.Trace.Processors; +using Datadog.Trace.Util; namespace Datadog.Trace; @@ -49,7 +50,7 @@ private static string GetSerializedTags() }; // then normalize values and put all tags in a string - var serializedTags = new StringBuilder(); + var serializedTags = StringBuilderCache.Acquire(); foreach (var kvp in tags) { if (!string.IsNullOrEmpty(kvp.Value)) @@ -59,7 +60,7 @@ private static string GetSerializedTags() } serializedTags.Remove(serializedTags.Length - 1, length: 1); // remove last comma - return serializedTags.ToString(); + return StringBuilderCache.GetStringAndRelease(serializedTags); } private static string NormalizeTagValue(string tagValue)