Skip to content

Commit fb53844

Browse files
[Tracer] Add new process tags (#7651)
## Summary of changes this is the first PR introducing process tags to dotnet, see [RFC](https://docs.google.com/document/d/1AFdLUuVk70i0bJd5335-RxqsvwAV9ovAqcO2z5mEMbA) The goal is to pass as much data as possible to the backend to enable customers to set the "correct" names for their services. ## Reason for change overarching product need ## Implementation details - didn't add the config to MutableSettings because I don't think it's something that needs to support live updates ? ## Test coverage ## Other details <!-- Fixes #{issue} --> <!-- ⚠️ Note: Where possible, please obtain 2 approvals prior to merging. Unless CODEOWNERS specifies otherwise, for external teams it is typically best to have one review from a team member, and one review from apm-dotnet. Trivial changes do not require 2 reviews. MergeQueue is NOT enabled in this repository. If you have write access to the repo, the PR has 1-2 approvals (see above), and all of the required checks have passed, you can use the Squash and Merge button to merge the PR. If you don't have write access, or you need help, reach out in the #apm-dotnet channel in Slack. --> --------- Co-authored-by: Lucas Pimentel <lucas.pimentel@datadoghq.com>
1 parent 068a6d8 commit fb53844

File tree

12 files changed

+287
-6
lines changed

12 files changed

+287
-6
lines changed

tracer/src/Datadog.Trace/Agent/MessagePack/SpanMessagePackFormatter.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,12 @@ internal class SpanMessagePackFormatter : IMessagePackFormatter<TraceChunkModel>
6868
private readonly byte[] _runtimeIdNameBytes = StringEncoding.UTF8.GetBytes(Trace.Tags.RuntimeId);
6969
private readonly byte[] _runtimeIdValueBytes = StringEncoding.UTF8.GetBytes(Tracer.RuntimeId);
7070

71+
// 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,
72+
// some values may need a bit of time to be accessible.
73+
// With this construct, it should be queried after the first span(s) get closed, which should be late enough.
74+
private readonly Lazy<byte[]> _processTagsValueBytes = new(() => StringEncoding.UTF8.GetBytes(ProcessTags.SerializedTags));
75+
private readonly byte[] _processTagsNameBytes = StringEncoding.UTF8.GetBytes(Tags.ProcessTags);
76+
7177
private readonly byte[] _environmentNameBytes = StringEncoding.UTF8.GetBytes(Trace.Tags.Env);
7278
private readonly byte[] _gitCommitShaNameBytes = StringEncoding.UTF8.GetBytes(Trace.Tags.GitCommitSha);
7379
private readonly byte[] _gitRepositoryUrlNameBytes = StringEncoding.UTF8.GetBytes(Trace.Tags.GitRepositoryUrl);
@@ -603,6 +609,18 @@ private int WriteTags(ref byte[] bytes, int offset, in SpanModel model, ITagProc
603609
}
604610
}
605611

612+
// Process tags will be sent only once per buffer/payload (one payload can contain many chunks from different traces)
613+
if (model.IsFirstSpanInChunk && model.TraceChunk.IsFirstChunkInPayload && model.TraceChunk.ShouldPropagateProcessTags)
614+
{
615+
var processTagsRawBytes = _processTagsValueBytes.Value;
616+
if (processTagsRawBytes.Length > 0)
617+
{
618+
count++;
619+
offset += MessagePackBinary.WriteStringBytes(ref bytes, offset, _processTagsNameBytes);
620+
offset += MessagePackBinary.WriteStringBytes(ref bytes, offset, processTagsRawBytes);
621+
}
622+
}
623+
606624
// SCI tags will be sent only once per trace
607625
if (model.IsFirstSpanInChunk)
608626
{

tracer/src/Datadog.Trace/Agent/MessagePack/TraceChunkModel.cs

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ internal readonly struct TraceChunkModel
3333

3434
public readonly int? SamplingPriority = null;
3535

36+
public readonly bool IsFirstChunkInPayload = false;
37+
3638
public readonly string? SamplingMechanism = null;
3739

3840
public readonly double? AppliedSamplingRate = null;
@@ -63,15 +65,18 @@ internal readonly struct TraceChunkModel
6365

6466
public readonly ImmutableAzureAppServiceSettings? AzureAppServiceSettings = null;
6567

68+
public readonly bool ShouldPropagateProcessTags = false;
69+
6670
public readonly bool IsApmEnabled = true;
6771

6872
/// <summary>
6973
/// Initializes a new instance of the <see cref="TraceChunkModel"/> struct.
7074
/// </summary>
7175
/// <param name="spans">The spans that will be within this <see cref="TraceChunkModel"/>.</param>
7276
/// <param name="samplingPriority">Optional sampling priority to override the <see cref="TraceContext"/> sampling priority.</param>
73-
public TraceChunkModel(in ArraySegment<Span> spans, int? samplingPriority = null)
74-
: this(spans, TraceContext.GetTraceContext(spans), samplingPriority)
77+
/// <param name="isFirstChunkInPayload">Indicates if this is the first trace chunk being written to the output buffer.</param>
78+
public TraceChunkModel(in ArraySegment<Span> spans, int? samplingPriority = null, bool isFirstChunkInPayload = false)
79+
: this(spans, TraceContext.GetTraceContext(spans), samplingPriority, isFirstChunkInPayload)
7580
{
7681
// since all we have is an array of spans, use the trace context from the first span
7782
// to get the other values we need (sampling priority, origin, trace tags, etc) for now.
@@ -80,11 +85,12 @@ public TraceChunkModel(in ArraySegment<Span> spans, int? samplingPriority = null
8085
}
8186

8287
// used only to chain constructors
83-
private TraceChunkModel(in ArraySegment<Span> spans, TraceContext? traceContext, int? samplingPriority)
88+
private TraceChunkModel(in ArraySegment<Span> spans, TraceContext? traceContext, int? samplingPriority, bool isFirstChunkInPayload)
8489
: this(spans, traceContext?.RootSpan)
8590
{
8691
// sampling decision override takes precedence over TraceContext.SamplingPriority
8792
SamplingPriority = samplingPriority;
93+
IsFirstChunkInPayload = isFirstChunkInPayload;
8894

8995
if (traceContext is not null)
9096
{
@@ -108,6 +114,7 @@ private TraceChunkModel(in ArraySegment<Span> spans, TraceContext? traceContext,
108114
{
109115
IsRunningInAzureAppService = settings.IsRunningInAzureAppService;
110116
AzureAppServiceSettings = settings.AzureAppServiceMetadata;
117+
ShouldPropagateProcessTags = settings.PropagateProcessTags;
111118
IsApmEnabled = settings.ApmTracingEnabled;
112119
}
113120

tracer/src/Datadog.Trace/Agent/SpanBuffer.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ public WriteStatus TryWrite(ArraySegment<Span> spans, ref byte[] temporaryBuffer
9191
// to get the other values we need (sampling priority, origin, trace tags, etc) for now.
9292
// the idea is that as we refactor further, we can pass more than just the spans,
9393
// and these values can come directly from the trace context.
94-
var traceChunk = new TraceChunkModel(spans, samplingPriority);
94+
var traceChunk = new TraceChunkModel(spans, samplingPriority, isFirstChunkInPayload: TraceCount == 0);
9595

9696
// We don't know what the serialized size of the payload will be,
9797
// so we need to write to a temporary buffer first

tracer/src/Datadog.Trace/Configuration/ConfigurationKeys.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,11 @@ internal static partial class ConfigurationKeys
168168
/// <seealso cref="TracerSettings.HeaderTags"/>
169169
public const string GrpcTags = "DD_TRACE_GRPC_TAGS";
170170

171+
/// <summary>
172+
/// Propagate the process tags in every supported payload
173+
/// </summary>
174+
public const string PropagateProcessTags = "DD_EXPERIMENTAL_PROPAGATE_PROCESS_TAGS_ENABLED";
175+
171176
/// <summary>
172177
/// Configuration key for a map of services to rename.
173178
/// </summary>

tracer/src/Datadog.Trace/Configuration/TracerSettings.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ namespace Datadog.Trace.Configuration
3333
public record TracerSettings
3434
{
3535
private static readonly IDatadogLogger Log = DatadogLogging.GetLoggerFor<TracerSettings>();
36-
private static readonly HashSet<string> DefaultExperimentalFeatures = ["DD_TAGS"];
36+
private static readonly HashSet<string> DefaultExperimentalFeatures = ["DD_TAGS", ConfigurationKeys.PropagateProcessTags];
3737

3838
private readonly IConfigurationTelemetry _telemetry;
3939
private readonly Lazy<string> _fallbackApplicationName;
@@ -102,6 +102,11 @@ internal TracerSettings(IConfigurationSource? source, IConfigurationTelemetry te
102102
string s => new HashSet<string>(s.Split([','], StringSplitOptions.RemoveEmptyEntries)),
103103
};
104104

105+
PropagateProcessTags = config
106+
.WithKeys(ConfigurationKeys.PropagateProcessTags)
107+
.AsBool(false)
108+
|| ExperimentalFeaturesEnabled.Contains(ConfigurationKeys.PropagateProcessTags);
109+
105110
GCPFunctionSettings = new ImmutableGCPFunctionSettings(source, _telemetry);
106111
IsRunningInGCPFunctions = GCPFunctionSettings.IsGCPFunction;
107112

@@ -742,6 +747,8 @@ not null when string.Equals(value, "otlp", StringComparison.OrdinalIgnoreCase) =
742747

743748
internal HashSet<string> ExperimentalFeaturesEnabled { get; }
744749

750+
internal bool PropagateProcessTags { get; }
751+
745752
internal OverrideErrorLog ErrorLog { get; }
746753

747754
internal IConfigurationTelemetry Telemetry => _telemetry;
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
// <copyright file="ProcessTags.cs" company="Datadog">
2+
// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License.
3+
// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc.
4+
// </copyright>
5+
6+
#nullable enable
7+
8+
using System;
9+
using System.Collections.Generic;
10+
using System.IO;
11+
using System.Text;
12+
using Datadog.Trace.Configuration;
13+
using Datadog.Trace.Processors;
14+
using Datadog.Trace.Util;
15+
16+
namespace Datadog.Trace;
17+
18+
internal static class ProcessTags
19+
{
20+
public const string EntrypointName = "entrypoint.name";
21+
public const string EntrypointBasedir = "entrypoint.basedir";
22+
public const string EntrypointWorkdir = "entrypoint.workdir";
23+
24+
private static readonly Lazy<string> LazySerializedTags = new(GetSerializedTags);
25+
26+
public static string SerializedTags
27+
{
28+
get => LazySerializedTags.Value;
29+
}
30+
31+
/// <summary>
32+
/// From the full path of a directory, get the name of the leaf directory.
33+
/// </summary>
34+
private static string GetLastPathSegment(string directoryPath)
35+
{
36+
// Path.GetFileName returns an empty string if the path ends with a '/'.
37+
// We could use Path.TrimEndingDirectorySeparator instead of the trim here, but it's not available on .NET Framework
38+
return Path.GetFileName(directoryPath.TrimEnd('\\').TrimEnd('/'));
39+
}
40+
41+
private static string GetSerializedTags()
42+
{
43+
// ⚠️ make sure entries are added in alphabetical order of keys
44+
var tags = new List<KeyValuePair<string, string?>>
45+
{
46+
new(EntrypointBasedir, GetLastPathSegment(AppContext.BaseDirectory)),
47+
new(EntrypointName, EntryAssemblyLocator.GetEntryAssembly()?.EntryPoint?.DeclaringType?.FullName),
48+
// workdir can be changed by the code, but we consider that capturing the value when this is called is good enough
49+
new(EntrypointWorkdir, GetLastPathSegment(Environment.CurrentDirectory))
50+
};
51+
52+
// then normalize values and put all tags in a string
53+
var serializedTags = StringBuilderCache.Acquire();
54+
foreach (var kvp in tags)
55+
{
56+
if (!string.IsNullOrEmpty(kvp.Value))
57+
{
58+
serializedTags.Append($"{kvp.Key}:{NormalizeTagValue(kvp.Value!)},");
59+
}
60+
}
61+
62+
serializedTags.Remove(serializedTags.Length - 1, length: 1); // remove last comma
63+
return StringBuilderCache.GetStringAndRelease(serializedTags);
64+
}
65+
66+
private static string NormalizeTagValue(string tagValue)
67+
{
68+
// TraceUtil.NormalizeTag does almost exactly what we want, except it allows ':',
69+
// which we don't want because we use it as a key/value separator.
70+
return TraceUtil.NormalizeTag(tagValue).Replace(oldChar: ':', newChar: '_');
71+
}
72+
}

tracer/src/Datadog.Trace/Tags.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -506,6 +506,12 @@ public static partial class Tags
506506
/// </summary>
507507
internal const string RuntimeFamily = "_dd.runtime_family";
508508

509+
/// <summary>
510+
/// Contains a serialized list of process tags, that can be used in the backend for service renaming.
511+
/// <see cref="ProcessTags"/>
512+
/// </summary>
513+
internal const string ProcessTags = "_dd.tags.process";
514+
509515
/// <summary>
510516
/// The resource ID of the site instance in Azure App Services where the traced application is running.
511517
/// </summary>
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
// <copyright file="ProcessTagsTests.cs" company="Datadog">
2+
// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License.
3+
// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc.
4+
// </copyright>
5+
6+
using System.Collections.Specialized;
7+
using System.Linq;
8+
using System.Threading.Tasks;
9+
using Datadog.Trace.Agent;
10+
using Datadog.Trace.Configuration;
11+
using Datadog.Trace.TestHelpers;
12+
using Datadog.Trace.TestHelpers.TestTracer;
13+
using FluentAssertions;
14+
using Xunit;
15+
16+
namespace Datadog.Trace.IntegrationTests.Tagging;
17+
18+
public class ProcessTagsTests
19+
{
20+
private readonly MockApi _testApi;
21+
22+
public ProcessTagsTests()
23+
{
24+
_testApi = new MockApi();
25+
}
26+
27+
[Theory]
28+
[InlineData(true)]
29+
[InlineData(false)]
30+
public async Task ProcessTags_Only_In_First_Span(bool enabled)
31+
{
32+
var settings = new TracerSettings(new NameValueConfigurationSource(new NameValueCollection { { ConfigurationKeys.PropagateProcessTags, enabled ? "true" : "false" } }));
33+
var agentWriter = new AgentWriter(_testApi, statsAggregator: null, statsd: null, automaticFlush: false);
34+
await using var tracer = TracerHelper.Create(settings, agentWriter);
35+
36+
using (tracer.StartActiveInternal("A"))
37+
using (tracer.StartActiveInternal("AA"))
38+
{
39+
}
40+
41+
// other trace
42+
using (tracer.StartActiveInternal("B"))
43+
using (tracer.StartActiveInternal("BB"))
44+
{
45+
}
46+
47+
await tracer.FlushAsync();
48+
var traceChunks = _testApi.Wait();
49+
50+
traceChunks.Should().HaveCount(2); // 2 (small) traces = 2 chunks
51+
if (enabled)
52+
{
53+
// process tags written only to first span of first chunk
54+
traceChunks[0][0].Tags.Should().ContainKey(Tags.ProcessTags);
55+
}
56+
else
57+
{
58+
traceChunks[0][0].Tags.Should().NotContainKey(Tags.ProcessTags);
59+
}
60+
61+
traceChunks.SelectMany(x => x) // flatten
62+
.Skip(1) // exclude first item that we just checked above
63+
.Should()
64+
.AllSatisfy(s => s.Tags.Should().NotContainKey(Tags.ProcessTags));
65+
}
66+
}

tracer/test/Datadog.Trace.Tests/Agent/SpanBufferTests.cs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,15 @@
44
// </copyright>
55

66
using System;
7+
using System.Collections.Generic;
78
using System.Linq;
89
using Datadog.Trace.Agent;
910
using Datadog.Trace.Agent.MessagePack;
1011
using Datadog.Trace.TestHelpers;
12+
using Datadog.Trace.Vendors.MessagePack.Formatters;
1113
using FluentAssertions;
1214
using MessagePack; // use nuget MessagePack to deserialize
15+
using Moq;
1316
using Xunit;
1417

1518
namespace Datadog.Trace.Tests.Agent
@@ -126,6 +129,28 @@ public void TemporaryBufferSizeLimit()
126129
temporaryBuffer.Length.Should().BeLessThanOrEqualTo(512, because: "the size of the temporary buffer shouldn't exceed twice the limit");
127130
}
128131

132+
[Fact]
133+
public void IsFirstChunkInBuffer_FirstChunkIsTrue_SubsequentChunksAreFalse()
134+
{
135+
var interceptedChunks = new List<TraceChunkModel>();
136+
var interceptingFormatter = new InterceptingTraceChunkFormatter(interceptedChunks);
137+
var mockResolver = new Mock<Vendors.MessagePack.IFormatterResolver>();
138+
mockResolver.Setup(r => r.GetFormatter<TraceChunkModel>()).Returns(interceptingFormatter);
139+
140+
var buffer = new SpanBuffer(maxBufferSize: 256, mockResolver.Object);
141+
var temporaryBuffer = new byte[256];
142+
143+
var firstSpanArray = CreateTraceChunk(2);
144+
var secondSpanArray = CreateTraceChunk(spanCount: 2, startingId: 10);
145+
146+
buffer.TryWrite(firstSpanArray, ref temporaryBuffer).Should().Be(SpanBuffer.WriteStatus.Success);
147+
buffer.TryWrite(secondSpanArray, ref temporaryBuffer).Should().Be(SpanBuffer.WriteStatus.Success);
148+
149+
interceptedChunks.Should().HaveCount(2);
150+
interceptedChunks[0].IsFirstChunkInPayload.Should().BeTrue();
151+
interceptedChunks[1].IsFirstChunkInPayload.Should().BeFalse();
152+
}
153+
129154
private static ArraySegment<Span> CreateTraceChunk(int spanCount, ulong startingId = 1)
130155
{
131156
var spans = new Span[spanCount];
@@ -138,5 +163,22 @@ private static ArraySegment<Span> CreateTraceChunk(int spanCount, ulong starting
138163

139164
return new ArraySegment<Span>(spans);
140165
}
166+
167+
/// <summary>
168+
/// practical mock, because the presence of the ref modifier on bytes makes it not work well with Moq.
169+
/// </summary>
170+
private class InterceptingTraceChunkFormatter(List<TraceChunkModel> interceptedChunks) : IMessagePackFormatter<TraceChunkModel>
171+
{
172+
public int Serialize(ref byte[] bytes, int offset, TraceChunkModel value, Vendors.MessagePack.IFormatterResolver formatterResolver)
173+
{
174+
interceptedChunks.Add(value);
175+
return 50; // Return a reasonable serialized size
176+
}
177+
178+
public TraceChunkModel Deserialize(byte[] bytes, int offset, Vendors.MessagePack.IFormatterResolver formatterResolver, out int readSize)
179+
{
180+
throw new NotImplementedException("Deserialization not needed for this test");
181+
}
182+
}
141183
}
142184
}

tracer/test/Datadog.Trace.Tests/Configuration/TracerSettingsTests.cs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1069,5 +1069,29 @@ public void OtlpLogsTimeoutMsFallback(string logsTimeout, string generalTimeout,
10691069

10701070
settings.OtlpLogsTimeoutMs.Should().Be(expected);
10711071
}
1072+
1073+
[Theory]
1074+
[MemberData(nameof(BooleanTestCases), false)]
1075+
public void ProcessTagsEnabled(string value, bool expected)
1076+
{
1077+
var source = CreateConfigurationSource((ConfigurationKeys.PropagateProcessTags, value));
1078+
var settings = new TracerSettings(source);
1079+
1080+
settings.PropagateProcessTags.Should().Be(expected);
1081+
}
1082+
1083+
[Theory]
1084+
[InlineData(null, false)]
1085+
[InlineData("", false)]
1086+
[InlineData("none", false)]
1087+
[InlineData("all", true)]
1088+
[InlineData(ConfigurationKeys.PropagateProcessTags, true)]
1089+
public void ProcessTagsEnabledIfExperimentalEnabled(string value, bool expected)
1090+
{
1091+
var source = CreateConfigurationSource((ConfigurationKeys.ExperimentalFeaturesEnabled, value));
1092+
var settings = new TracerSettings(source);
1093+
1094+
settings.PropagateProcessTags.Should().Be(expected);
1095+
}
10721096
}
10731097
}

0 commit comments

Comments
 (0)