Skip to content

Commit b35deb9

Browse files
committed
wip
1 parent 8a0eacf commit b35deb9

10 files changed

Lines changed: 1330 additions & 93 deletions

File tree

tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Activity/ActivityCustomPropertyAccessor.cs

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ namespace Datadog.Trace.ClrProfiler.AutoInstrumentation.Activity
2424
internal static class ActivityCustomPropertyAccessor<TTarget>
2525
{
2626
private const string SpanPropertyKey = "__dd_span__";
27+
private const string InitialOpNameKey = "__dd_initial_op__";
2728

2829
/// <summary>
2930
/// Open-instance delegate for <c>Activity.GetCustomProperty(string)</c>.
@@ -50,6 +51,20 @@ internal static class ActivityCustomPropertyAccessor<TTarget>
5051
public static void SetScope(TTarget instance, Scope? scope)
5152
=> SetCustomProperty?.Invoke(instance, SpanPropertyKey, scope);
5253

54+
/// <summary>
55+
/// Retrieves the initial operation name saved at Activity.Start() time.
56+
/// Used by <c>ActivityStopIntegration</c> to detect whether the user explicitly
57+
/// overrode the operation name via an "operation.name" tag.
58+
/// </summary>
59+
public static string? GetInitialOperationName(TTarget instance)
60+
=> GetCustomProperty?.Invoke(instance, InitialOpNameKey) as string;
61+
62+
/// <summary>
63+
/// Stores the initial operation name on the activity.
64+
/// </summary>
65+
public static void SetInitialOperationName(TTarget instance, string? operationName)
66+
=> SetCustomProperty?.Invoke(instance, InitialOpNameKey, operationName);
67+
5368
private static Func<TTarget, string, object?>? CreateGetDelegate()
5469
{
5570
try
@@ -108,7 +123,12 @@ public static void SetScope(TTarget instance, Scope? scope)
108123
il.Emit(OpCodes.Ldarg_1); // string propertyName
109124
il.Emit(OpCodes.Ldarg_2); // object? propertyValue
110125
il.Emit(OpCodes.Callvirt, method);
111-
il.Emit(OpCodes.Pop); // discard Activity return value
126+
127+
if (method.ReturnType != typeof(void))
128+
{
129+
il.Emit(OpCodes.Pop); // discard non-void return value
130+
}
131+
112132
il.Emit(OpCodes.Ret);
113133

114134
return (Action<TTarget, string, object?>)dm.CreateDelegate(typeof(Action<TTarget, string, object?>));

tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Activity/ActivitySetStatusIntegration.cs

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -94,12 +94,6 @@ private static void ApplyStatus(Span span, int statusCodeInt, string? descriptio
9494
span.SetTag(Tags.ErrorMsg, description);
9595
}
9696

97-
// Also set status description tag
98-
if (!string.IsNullOrEmpty(description))
99-
{
100-
span.SetTag("otel.status_description", description);
101-
}
102-
10397
break;
10498
default:
10599
tags.OtelStatusCode = "STATUS_CODE_UNSET";

tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Activity/ActivitySourceFilter.cs

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,16 @@
66
#nullable enable
77

88
using System;
9+
using System.Collections.Generic;
10+
using System.Text.RegularExpressions;
11+
using Datadog.Trace.Sampling;
912

1013
namespace Datadog.Trace.ClrProfiler.AutoInstrumentation.Activity
1114
{
1215
/// <summary>
1316
/// Shared filter for Activity source names that should be ignored because they are already handled
14-
/// by dedicated Datadog integrations (e.g. ASP.NET Core, HttpClient, SqlClient).
17+
/// by dedicated Datadog integrations (e.g. ASP.NET Core, HttpClient, SqlClient) or because they
18+
/// have been explicitly disabled via <c>DD_TRACE_DISABLED_ACTIVITY_SOURCES</c>.
1519
/// This mirrors the <c>IgnoreActivityHandler.SourcesNames</c> list used by the managed ActivityListener.
1620
/// </summary>
1721
internal static class ActivitySourceFilter
@@ -31,9 +35,13 @@ internal static class ActivitySourceFilter
3135
"Experimental.System.Net.Sockets",
3236
};
3337

38+
private static List<Regex>? _disabledSourceGlobs;
39+
private static bool _disableAll;
40+
3441
/// <summary>
3542
/// Returns true if the Activity from the given source should be ignored by the CallTarget
36-
/// interception path (because it is handled by a separate Datadog integration).
43+
/// interception path (because it is handled by a separate Datadog integration or was
44+
/// explicitly disabled via configuration).
3745
/// </summary>
3846
public static bool ShouldIgnore(string sourceName, string? version)
3947
{
@@ -50,7 +58,46 @@ public static bool ShouldIgnore(string sourceName, string? version)
5058
}
5159
}
5260

61+
// Check DD_TRACE_DISABLED_ACTIVITY_SOURCES glob patterns
62+
if (_disableAll)
63+
{
64+
return true;
65+
}
66+
67+
_disabledSourceGlobs ??= PopulateDisabledGlobs();
68+
foreach (var regex in _disabledSourceGlobs)
69+
{
70+
if (regex.IsMatch(sourceName))
71+
{
72+
return true;
73+
}
74+
}
75+
5376
return false;
5477
}
78+
79+
private static List<Regex> PopulateDisabledGlobs()
80+
{
81+
var globs = new List<Regex>();
82+
var toDisable = Tracer.Instance.Settings.DisabledActivitySources;
83+
if (toDisable is null || toDisable.Length == 0)
84+
{
85+
return globs;
86+
}
87+
88+
foreach (var disabledSourceNameGlob in toDisable)
89+
{
90+
var globRegex = RegexBuilder.Build(disabledSourceNameGlob, SamplingRulesFormat.Glob, RegexBuilder.DefaultTimeout);
91+
if (globRegex is null)
92+
{
93+
_disableAll = true;
94+
return [];
95+
}
96+
97+
globs.Add(globRegex);
98+
}
99+
100+
return globs;
101+
}
55102
}
56103
}

tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Activity/ActivityStartIntegration.cs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,12 @@
66
#nullable enable
77

88
using System;
9+
using System.Collections.Generic;
910
using System.ComponentModel;
1011
using Datadog.Trace.Activity;
1112
using Datadog.Trace.Activity.DuckTypes;
1213
using Datadog.Trace.Activity.Handlers;
14+
using Datadog.Trace.Activity.Helpers;
1315
using Datadog.Trace.ClrProfiler.CallTarget;
1416
using Datadog.Trace.Configuration;
1517
using Datadog.Trace.DuckTyping;
@@ -209,8 +211,42 @@ private static void CreateAndLinkScope<TTarget>(TTarget instance, IActivity5 act
209211
tracer.TracerManager.Telemetry.IntegrationGeneratedSpan(IntegrationId);
210212
var scope = tracer.ActivateSpan(span, finishOnClose: false);
211213

214+
// Set ResourceName before copying tags so that reserved tag "operation.name"
215+
// (which changes OperationName) doesn't accidentally override the resource.
216+
// ResourceName will default to OperationName at Finish() if still null, but we
217+
// want to preserve the original activity name as the resource.
218+
span.ResourceName = activity5.DisplayName is { Length: > 0 } displayName
219+
? displayName
220+
: activity5.OperationName;
221+
222+
// Save the initial operation name BEFORE copying tags so ActivityStopIntegration can
223+
// detect explicit overrides via "operation.name" tag. Initial tags (passed to StartActivity)
224+
// may include "operation.name" which changes OperationName during tag copy below.
225+
var initialOperationName = span.OperationName;
226+
227+
// Copy existing tags from the Activity to the Span. Tags may have been set before
228+
// Activity.Start() returned (e.g., via initial tags passed to StartActivity()), so
229+
// they're in Activity.TagObjects but haven't been captured by our AddTag/SetTag integrations
230+
// (those require the scope to be set on the Activity's custom property, which isn't done yet).
231+
if (activity5.HasTagObjects())
232+
{
233+
var state = new OtelTagsEnumerationState(span);
234+
ActivityEnumerationHelper.EnumerateTagObjects(activity5, ref state, static (ref OtelTagsEnumerationState s, KeyValuePair<string, object?> kvp) =>
235+
{
236+
OtlpHelpers.SetTagObject(s.Span, kvp.Key, kvp.Value);
237+
return true;
238+
});
239+
}
240+
241+
// Apply OTel resource attributes (service.name, service.version, etc.) to the span.
242+
// In the managed ActivityListener path, resource attributes are applied by the
243+
// ResourceAttributeProcessor.OnStart callback which fires during Activity.Start().
244+
// With interception, that callback fires before we create the span, so we apply them here.
245+
OpenTelemetry.ResourceAttributeProcessorHelper.ApplyCachedResourceAttributes(span);
246+
212247
// Bi-directional link: Activity → Scope (via custom property — zero-alloc cached delegate)
213248
ActivityCustomPropertyAccessor<TTarget>.SetScope(instance, scope);
249+
ActivityCustomPropertyAccessor<TTarget>.SetInitialOperationName(instance, initialOperationName);
214250

215251
// Ensure IsAllDataRequested is true so that user code guarded by
216252
// `if (activity.IsAllDataRequested) { activity.AddTag(...); }` will actually run.

tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Activity/ActivityStopIntegration.cs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,10 +52,11 @@ internal static CallTargetState OnMethodBegin<TTarget>(TTarget instance)
5252
}
5353

5454
// Retrieve scope early; save in state so OnMethodEnd can access it after Stop() sets Duration.
55-
// Also save the initial operation name so OnMethodEnd can detect whether the user explicitly
56-
// changed it via an "operation.name" tag (which routes through AgentSetOtlpTag → span.OperationName).
55+
// Also retrieve the initial operation name saved at Start() time so OnMethodEnd can detect
56+
// whether the user explicitly changed it via an "operation.name" tag.
5757
var scope = ActivityCustomPropertyAccessor<TTarget>.GetScope(instance);
58-
return new CallTargetState(scope, state: scope?.Span.OperationName);
58+
var initialOpName = ActivityCustomPropertyAccessor<TTarget>.GetInitialOperationName(instance);
59+
return new CallTargetState(scope, state: initialOpName);
5960
}
6061

6162
/// <summary>

tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/OpenTelemetry/ResourceAttributeProcessorHelper.cs

Lines changed: 86 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,36 @@ internal static class ResourceAttributeProcessorHelper
1717
{
1818
private static Func<object, object>? _getResourceDelegate;
1919

20+
/// <summary>
21+
/// Cached OTel resource, populated on the first <see cref="OnStart"/> call from
22+
/// the dynamic ResourceAttributeProcessor. Used by the CallTarget Activity
23+
/// interception path to apply resource attributes after Activity.Start() returns,
24+
/// because the processor's OnStart fires *during* Activity.Start() — before
25+
/// the interception integration has had a chance to create the Datadog span.
26+
/// </summary>
27+
private static volatile IResource? _cachedResource;
28+
2029
static ResourceAttributeProcessorHelper()
2130
{
2231
_getResourceDelegate = CreateGetResourceDelegate();
2332
}
2433

34+
/// <summary>
35+
/// Applies the cached OTel resource attributes (service.name, service.version, etc.)
36+
/// to the given <paramref name="span"/>. Called from <c>ActivityStartIntegration.OnMethodEnd</c>
37+
/// after the span has been created.
38+
/// </summary>
39+
internal static void ApplyCachedResourceAttributes(Span span)
40+
{
41+
var resource = _cachedResource;
42+
if (resource is null)
43+
{
44+
return;
45+
}
46+
47+
ApplyResourceToSpan(span, resource);
48+
}
49+
2550
public static void OnStart(object processor, object activityData)
2651
{
2752
if (_getResourceDelegate is null
@@ -31,75 +56,83 @@ public static void OnStart(object processor, object activityData)
3156
return;
3257
}
3358

34-
// When CallTarget-based Activity interception is enabled, look up the span via the
35-
// custom property stored directly on the Activity object (no ConcurrentDictionary lookup).
36-
Span? span = null;
37-
if (Tracer.Instance.Settings.IsActivityInterceptionEnabled)
59+
// Cache the resource from the TracerProvider on first call so the interception path can use it later
60+
if (_cachedResource is null && baseProcessor.ParentProvider is not null)
3861
{
39-
if (activityData.TryDuckCast<IActivity5>(out var activity5ForInterception))
62+
var resourceObject = _getResourceDelegate(baseProcessor.ParentProvider);
63+
if (resourceObject.TryDuckCast<IResource>(out var resource))
4064
{
41-
span = (activity5ForInterception.GetCustomProperty("__dd_span__") as Scope)?.Span;
65+
_cachedResource = resource;
4266
}
4367
}
4468

45-
if (span is null)
69+
// When CallTarget-based Activity interception is enabled, the span is set on the Activity
70+
// custom property. However, the processor's OnStart fires during Activity.Start() — before
71+
// our OnMethodEnd integration runs. So the custom property will be null at this point.
72+
// The interception path applies resource attributes itself via ApplyCachedResourceAttributes().
73+
if (Tracer.Instance.Settings.IsActivityInterceptionEnabled)
4674
{
47-
// Fallback: legacy ConcurrentDictionary lookup for the managed ActivityListener path
48-
ActivityKey key;
49-
if (activityData.TryDuckCast<IW3CActivity>(out var w3cActivity) && w3cActivity.TraceId is { } traceId && w3cActivity.SpanId is { } spanId)
50-
{
51-
key = new(traceId, spanId);
52-
}
53-
else
54-
{
55-
key = new(activity.Id);
56-
}
75+
return;
76+
}
77+
78+
// Managed ActivityListener path: look up the span via ConcurrentDictionary
79+
Span? span;
80+
ActivityKey key;
81+
if (activityData.TryDuckCast<IW3CActivity>(out var w3cActivity) && w3cActivity.TraceId is { } traceId && w3cActivity.SpanId is { } spanId)
82+
{
83+
key = new(traceId, spanId);
84+
}
85+
else
86+
{
87+
key = new(activity.Id);
88+
}
5789

58-
if (!key.IsValid() || !ActivityHandlerCommon.ActivityMappingById.TryGetValue(key, out var activityMapping))
90+
if (!key.IsValid() || !ActivityHandlerCommon.ActivityMappingById.TryGetValue(key, out var activityMapping))
91+
{
92+
return;
93+
}
94+
95+
span = activityMapping.Scope?.Span;
96+
97+
if (span is not null && baseProcessor.ParentProvider is not null)
98+
{
99+
var resourceObject = _getResourceDelegate(baseProcessor.ParentProvider);
100+
if (resourceObject.TryDuckCast<IResource>(out var resource))
59101
{
60-
return;
102+
ApplyResourceToSpan(span, resource);
61103
}
62-
63-
span = activityMapping.Scope?.Span;
64104
}
105+
}
65106

66-
if (span is not null)
107+
private static void ApplyResourceToSpan(Span span, IResource resource)
108+
{
109+
foreach (var attribute in resource.Attributes)
67110
{
68-
if (baseProcessor.ParentProvider is not null)
111+
span.SetTag(attribute.Key, attribute.Value?.ToString());
112+
113+
// In addition to copying the attribute as a tag, update span fields for specific keys
114+
if (attribute.Value is not null)
69115
{
70-
var resourceObject = _getResourceDelegate(baseProcessor.ParentProvider);
71-
if (resourceObject.TryDuckCast<IResource>(out var resource))
116+
if (attribute.Key == "service.name")
72117
{
73-
foreach (var attribute in resource.Attributes)
118+
var resourceServiceName = attribute.Value.ToString();
119+
120+
// if OTEL_SERVICE_NAME isn't set, OpenTelemetry will set "service.name" to:
121+
// "unknown_service" or "unknown_service:ProcessName"
122+
if (string.IsNullOrEmpty(resourceServiceName)
123+
|| string.Equals(resourceServiceName, "unknown_service", StringComparison.Ordinal)
124+
|| resourceServiceName.StartsWith("unknown_service:", StringComparison.Ordinal))
74125
{
75-
span.SetTag(attribute.Key, attribute.Value?.ToString());
76-
77-
// In addition to copying the attribute as a tag, update span fields for specific keys
78-
if (attribute.Value is not null)
79-
{
80-
if (attribute.Key == "service.name")
81-
{
82-
var resourceServiceName = attribute.Value.ToString();
83-
84-
// if OTEL_SERVICE_NAME isn't set, OpenTelemetry will set "service.name" to:
85-
// "unknown_service" or "unknown_service:ProcessName"
86-
if (string.IsNullOrEmpty(resourceServiceName)
87-
|| string.Equals(resourceServiceName, "unknown_service", StringComparison.Ordinal)
88-
|| resourceServiceName.StartsWith("unknown_service:", StringComparison.Ordinal))
89-
{
90-
resourceServiceName = Tracer.Instance.DefaultServiceName;
91-
92-
span.SetTag(attribute.Key, resourceServiceName);
93-
}
94-
95-
span.SetService(resourceServiceName, null);
96-
}
97-
else if (attribute.Key == "service.version")
98-
{
99-
span.SetTag(Tags.Version, attribute.Value.ToString());
100-
}
101-
}
126+
resourceServiceName = Tracer.Instance.DefaultServiceName;
127+
128+
span.SetTag(attribute.Key, resourceServiceName);
102129
}
130+
131+
span.SetService(resourceServiceName, null);
132+
}
133+
else if (attribute.Key == "service.version")
134+
{
135+
span.SetTag(Tags.Version, attribute.Value.ToString());
103136
}
104137
}
105138
}

tracer/src/Datadog.Tracer.Native/dd_profiler_constants.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ const shared::WSTRING include_assemblies[]{
5757
WStr("Microsoft.Extensions.Logging"),
5858
WStr("Microsoft.Extensions.Logging.Abstractions"),
5959
WStr("Microsoft.Extensions.Telemetry"),
60+
WStr("System.Diagnostics.DiagnosticSource"),
6061
WStr("System.Diagnostics.Process"),
6162
WStr("Microsoft.Extensions.Identity.Core"),
6263
WStr("System.Runtime.Remoting"),

tracer/test/Datadog.Trace.ClrProfiler.IntegrationTests/NetActivitySdkTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ public async Task SubmitsTracesWithInterception()
125125
settings.AddRegexScrubber(traceIdRegexLow, "TraceIdLow: LinkIdLow");
126126
settings.AddRegexScrubber(_timeUnixNanoRegex, @"time_unix_nano"":<DateTimeOffset.Now>");
127127
await VerifyHelper.VerifySpans(spans, settings)
128-
.UseFileName(nameof(NetActivitySdkTests))
128+
.UseFileName(nameof(NetActivitySdkTests) + ".Interception")
129129
.DisableRequireUniquePrefix();
130130

131131
await telemetry.AssertIntegrationEnabledAsync(IntegrationId.OpenTelemetry);

0 commit comments

Comments
 (0)