Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,12 @@ internal class SpanMessagePackFormatter : IMessagePackFormatter<TraceChunkModel>
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 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<byte[]> _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);
Expand Down Expand Up @@ -603,6 +609,18 @@ private int WriteTags(ref byte[] bytes, int offset, in SpanModel model, ITagProc
}
}

// 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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are process tags supposed to be added to CI Visibility payloads? Because they have their own formatters if so

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm 🤔 that's a good question, does CI visibility have a notion of service name too ? (if yes yes, if no no)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

confirmed that we don't need them on CI visibility

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ci Vis does have a notion of service name, yes, but I guess this isn't an issue 🤷‍♂️

{
var processTagsRawBytes = _processTagsValueBytes.Value;
if (processTagsRawBytes.Length > 0)
{
count++;
offset += MessagePackBinary.WriteStringBytes(ref bytes, offset, _processTagsNameBytes);
offset += MessagePackBinary.WriteStringBytes(ref bytes, offset, processTagsRawBytes);
}
}

// SCI tags will be sent only once per trace
if (model.IsFirstSpanInChunk)
{
Expand Down
13 changes: 10 additions & 3 deletions tracer/src/Datadog.Trace/Agent/MessagePack/TraceChunkModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ internal readonly struct TraceChunkModel

public readonly int? SamplingPriority = null;

public readonly bool IsFirstChunkInPayload = false;

public readonly string? SamplingMechanism = null;

public readonly double? AppliedSamplingRate = null;
Expand Down Expand Up @@ -63,15 +65,18 @@ internal readonly struct TraceChunkModel

public readonly ImmutableAzureAppServiceSettings? AzureAppServiceSettings = null;

public readonly bool ShouldPropagateProcessTags = false;

public readonly bool IsApmEnabled = true;

/// <summary>
/// Initializes a new instance of the <see cref="TraceChunkModel"/> struct.
/// </summary>
/// <param name="spans">The spans that will be within this <see cref="TraceChunkModel"/>.</param>
/// <param name="samplingPriority">Optional sampling priority to override the <see cref="TraceContext"/> sampling priority.</param>
public TraceChunkModel(in ArraySegment<Span> spans, int? samplingPriority = null)
: this(spans, TraceContext.GetTraceContext(spans), samplingPriority)
/// <param name="isFirstChunkInPayload">Indicates if this is the first trace chunk being written to the output buffer.</param>
public TraceChunkModel(in ArraySegment<Span> 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.
Expand All @@ -80,11 +85,12 @@ public TraceChunkModel(in ArraySegment<Span> spans, int? samplingPriority = null
}

// used only to chain constructors
private TraceChunkModel(in ArraySegment<Span> spans, TraceContext? traceContext, int? samplingPriority)
private TraceChunkModel(in ArraySegment<Span> spans, TraceContext? traceContext, int? samplingPriority, bool isFirstChunkInPayload)
: this(spans, traceContext?.RootSpan)
{
// sampling decision override takes precedence over TraceContext.SamplingPriority
SamplingPriority = samplingPriority;
IsFirstChunkInPayload = isFirstChunkInPayload;

if (traceContext is not null)
{
Expand All @@ -108,6 +114,7 @@ private TraceChunkModel(in ArraySegment<Span> spans, TraceContext? traceContext,
{
IsRunningInAzureAppService = settings.IsRunningInAzureAppService;
AzureAppServiceSettings = settings.AzureAppServiceMetadata;
ShouldPropagateProcessTags = settings.PropagateProcessTags;
IsApmEnabled = settings.ApmTracingEnabled;
}

Expand Down
2 changes: 1 addition & 1 deletion tracer/src/Datadog.Trace/Agent/SpanBuffer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ public WriteStatus TryWrite(ArraySegment<Span> 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, 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
Expand Down
5 changes: 5 additions & 0 deletions tracer/src/Datadog.Trace/Configuration/ConfigurationKeys.cs
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,11 @@ internal static partial class ConfigurationKeys
/// <seealso cref="TracerSettings.HeaderTags"/>
public const string GrpcTags = "DD_TRACE_GRPC_TAGS";

/// <summary>
/// Propagate the process tags in every supported payload
/// </summary>
public const string PropagateProcessTags = "DD_EXPERIMENTAL_PROPAGATE_PROCESS_TAGS_ENABLED";

/// <summary>
/// Configuration key for a map of services to rename.
/// </summary>
Expand Down
9 changes: 8 additions & 1 deletion tracer/src/Datadog.Trace/Configuration/TracerSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ namespace Datadog.Trace.Configuration
public record TracerSettings
{
private static readonly IDatadogLogger Log = DatadogLogging.GetLoggerFor<TracerSettings>();
private static readonly HashSet<string> DefaultExperimentalFeatures = ["DD_TAGS"];
private static readonly HashSet<string> DefaultExperimentalFeatures = ["DD_TAGS", ConfigurationKeys.PropagateProcessTags];

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

PropagateProcessTags = config
.WithKeys(ConfigurationKeys.PropagateProcessTags)
.AsBool(false)
|| ExperimentalFeaturesEnabled.Contains(ConfigurationKeys.PropagateProcessTags);

GCPFunctionSettings = new ImmutableGCPFunctionSettings(source, _telemetry);
IsRunningInGCPFunctions = GCPFunctionSettings.IsGCPFunction;

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

internal HashSet<string> ExperimentalFeaturesEnabled { get; }

internal bool PropagateProcessTags { get; }

internal OverrideErrorLog ErrorLog { get; }

internal IConfigurationTelemetry Telemetry => _telemetry;
Expand Down
72 changes: 72 additions & 0 deletions tracer/src/Datadog.Trace/ProcessTags.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// <copyright file="ProcessTags.cs" company="Datadog">
// 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.
// </copyright>

#nullable enable

using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using Datadog.Trace.Configuration;
using Datadog.Trace.Processors;
using Datadog.Trace.Util;

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 readonly Lazy<string> LazySerializedTags = new(GetSerializedTags);

public static string SerializedTags
{
get => LazySerializedTags.Value;
}

/// <summary>
/// From the full path of a directory, get the name of the leaf directory.
/// </summary>
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('\\').TrimEnd('/'));
Comment on lines +37 to +38
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is going to allocate multiple times, we can use ReadOnlySpan on netcore3.1+ to reduce the allocations

#if NETCOREAPP3_1_OR_GREATER
        return Path.GetFileName(directoryPath.AsSpan().TrimEnd('\\').TrimEnd('/'));
#else
        return Path.GetFileName(directoryPath.TrimEnd('\\').TrimEnd('/'));
#endif

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd say the same thing as as said to Lucas above, sick trick, but do we really want to start having different code path for different fwk in there, given that this code runs only once ?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, that's fair. I would but meh 😄

}

private static string GetSerializedTags()
{
// ⚠️ make sure entries are added in alphabetical order of keys
var tags = new List<KeyValuePair<string, string?>>
{
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 = StringBuilderCache.Acquire();
foreach (var kvp in tags)
{
if (!string.IsNullOrEmpty(kvp.Value))
{
serializedTags.Append($"{kvp.Key}:{NormalizeTagValue(kvp.Value!)},");
}
}

serializedTags.Remove(serializedTags.Length - 1, length: 1); // remove last comma
return StringBuilderCache.GetStringAndRelease(serializedTags);
}

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: '_');
}
}
6 changes: 6 additions & 0 deletions tracer/src/Datadog.Trace/Tags.cs
Original file line number Diff line number Diff line change
Expand Up @@ -506,6 +506,12 @@ public static partial class Tags
/// </summary>
internal const string RuntimeFamily = "_dd.runtime_family";

/// <summary>
/// Contains a serialized list of process tags, that can be used in the backend for service renaming.
/// <see cref="ProcessTags"/>
/// </summary>
internal const string ProcessTags = "_dd.tags.process";

/// <summary>
/// The resource ID of the site instance in Azure App Services where the traced application is running.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// <copyright file="ProcessTagsTests.cs" company="Datadog">
// 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.
// </copyright>

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));
}
}
42 changes: 42 additions & 0 deletions tracer/test/Datadog.Trace.Tests/Agent/SpanBufferTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,15 @@
// </copyright>

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
Expand Down Expand Up @@ -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<TraceChunkModel>();
var interceptingFormatter = new InterceptingTraceChunkFormatter(interceptedChunks);
var mockResolver = new Mock<Vendors.MessagePack.IFormatterResolver>();
mockResolver.Setup(r => r.GetFormatter<TraceChunkModel>()).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].IsFirstChunkInPayload.Should().BeTrue();
interceptedChunks[1].IsFirstChunkInPayload.Should().BeFalse();
}

private static ArraySegment<Span> CreateTraceChunk(int spanCount, ulong startingId = 1)
{
var spans = new Span[spanCount];
Expand All @@ -138,5 +163,22 @@ private static ArraySegment<Span> CreateTraceChunk(int spanCount, ulong starting

return new ArraySegment<Span>(spans);
}

/// <summary>
/// practical mock, because the presence of the ref modifier on bytes makes it not work well with Moq.
/// </summary>
private class InterceptingTraceChunkFormatter(List<TraceChunkModel> interceptedChunks) : IMessagePackFormatter<TraceChunkModel>
{
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");
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1069,5 +1069,29 @@ public void OtlpLogsTimeoutMsFallback(string logsTimeout, string generalTimeout,

settings.OtlpLogsTimeoutMs.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);
}
}
}
Loading
Loading