Skip to content

Commit 4657b42

Browse files
authored
Add tests for manual injection/extraction and guard invalid implementations (#6998)
## Summary of changes Guard against incorrect inject/extract propagation implementations ## Reason for change We saw an error where an injector/extractor was returning `null` during context extraction, which it should not do. #6991 introduced a `null` check to avoid throwing. This PR adds additional safety to the checks, explicitly related to the manual-instrumentation implementation, and adds extra tests. ## Implementation details - Wrap each of the functions passed in the manual API with a "safe" extractor/injector to guard against exceptions/nulls - We just swallow the errors, but that's what would happen _anyway_, and this way we don't unactionable logs. - It might be nice to have a way to _actually_ bubble-up specific non-custom exceptions someway, but not critical, and would require a bunch of changes, so meh. ## Test coverage - Add unit tests for the integrations to confirm the wrapping handles errors - Add integration tests for the instrumentation ## Other details Related to #6991
1 parent b9f4ebb commit 4657b42

File tree

8 files changed

+246
-6
lines changed

8 files changed

+246
-6
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
// <copyright file="SafeExtractor.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+
using System;
8+
using System.Collections.Generic;
9+
10+
namespace Datadog.Trace.ClrProfiler.AutoInstrumentation.ManualInstrumentation.Propagators;
11+
12+
/// <summary>
13+
/// An extractor we use to wrap customer extraction functions
14+
/// to force nullable constraints and handle exceptions.
15+
/// </summary>
16+
internal readonly struct SafeExtractor<TCarrier>
17+
{
18+
private readonly Func<TCarrier, string, IEnumerable<string?>> _extractor;
19+
20+
public SafeExtractor(Func<TCarrier, string, IEnumerable<string?>> extractor)
21+
{
22+
_extractor = extractor;
23+
}
24+
25+
public IEnumerable<string?> SafeExtract(TCarrier carrier, string key)
26+
{
27+
try
28+
{
29+
return _extractor(carrier, key) ?? [];
30+
}
31+
catch (Exception)
32+
{
33+
// This is a customer error, so we don't want to log it
34+
// There's a question of how/if we should throw _sometihng_ here to propagate the error
35+
// to the customer, but for now, just swallow it;
36+
return [];
37+
}
38+
}
39+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// <copyright file="SafeInjector.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+
using System;
8+
using System.Collections.Generic;
9+
10+
namespace Datadog.Trace.ClrProfiler.AutoInstrumentation.ManualInstrumentation.Propagators;
11+
12+
/// <summary>
13+
/// An injector we use to wrap customer extraction functions
14+
/// to force nullable constraints and handle exceptions.
15+
/// </summary>
16+
internal readonly struct SafeInjector<TCarrier>
17+
{
18+
private readonly Action<TCarrier, string, string> _injector;
19+
20+
public SafeInjector(Action<TCarrier, string, string> injector)
21+
{
22+
_injector = injector;
23+
}
24+
25+
public void SafeInject(TCarrier carrier, string key, string value)
26+
{
27+
try
28+
{
29+
_injector(carrier, key, value);
30+
}
31+
catch (Exception)
32+
{
33+
// This is a customer error, so we don't want to log it
34+
// There's a question of how/if we should throw _sometihng_ here to propagate the error
35+
// to the customer, but for now, just swallow it;
36+
}
37+
}
38+
}

tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/ManualInstrumentation/Propagators/SpanContextExtractorExtractIncludingDsmIntegration.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,13 @@ public class SpanContextExtractorExtractIncludingDsmIntegration
3535
internal static CallTargetState OnMethodBegin<TTarget, TCarrier, TAction>(TTarget instance, in TCarrier carrier, in TAction getter, string messageType, string source)
3636
{
3737
TelemetryFactory.Metrics.Record(PublicApiUsage.SpanContextExtractor_ExtractIncludingDsm);
38+
39+
// we can't trust that the customer honours the nullable reference types here,
40+
// so we wrap the method in a try/catch and ensure we always return non-null
3841
var extract = (Func<TCarrier, string, IEnumerable<string?>>)(object)getter!;
39-
var extracted = SpanContextExtractor.ExtractInternal(carrier, extract, messageType, source);
42+
var extractor = new SafeExtractor<TCarrier>(extract);
43+
44+
var extracted = SpanContextExtractor.ExtractInternal(carrier, extractor.SafeExtract, messageType, source);
4045
return new CallTargetState(scope: null, extracted);
4146
}
4247

tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/ManualInstrumentation/Propagators/SpanContextExtractorExtractIntegration.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,13 @@ public class SpanContextExtractorExtractIntegration
3434
internal static CallTargetState OnMethodBegin<TTarget, TCarrier, TAction>(TTarget instance, in TCarrier carrier, in TAction getter)
3535
{
3636
TelemetryFactory.Metrics.Record(PublicApiUsage.SpanContextExtractor_Extract);
37+
38+
// we can't trust that the customer honours the nullable reference types here,
39+
// so we wrap the method in a try/catch and ensure we always return non-null
3740
var extract = (Func<TCarrier, string, IEnumerable<string?>>)(object)getter!;
38-
var extracted = SpanContextExtractor.ExtractInternal(carrier, extract);
41+
var extractor = new SafeExtractor<TCarrier>(extract);
42+
43+
var extracted = SpanContextExtractor.ExtractInternal(carrier, extractor.SafeExtract);
3944
return new CallTargetState(scope: null, state: extracted);
4045
}
4146

tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/ManualInstrumentation/Propagators/SpanContextInjectorInjectIncludingDsmIntegration.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,14 @@ internal static CallTargetState OnMethodBegin<TTarget, TCarrier, TAction, TSpanC
3333
// The Injector.Inject method currently _only_ works with SpanContext objects
3434
// Therefore, there's no point calling inject unless we can remap it to a SpanContext
3535
TelemetryFactory.Metrics.Record(PublicApiUsage.SpanContextInjector_Inject);
36-
var inject = (Action<TCarrier, string, string>)(object)setter!;
3736

3837
if (SpanContextHelper.GetContext(context) is { } spanContext)
3938
{
40-
SpanContextInjector.InjectInternal(carrier, inject, spanContext, messageType, target);
39+
// We can't trust that the customer function is safe,
40+
// so we wrap the method in a try/catch to ensure we don't throw
41+
var inject = (Action<TCarrier, string, string>)(object)setter!;
42+
var injector = new SafeInjector<TCarrier>(inject);
43+
SpanContextInjector.InjectInternal(carrier, injector.SafeInject, spanContext, messageType, target);
4144
}
4245

4346
return CallTargetState.GetDefault();

tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/ManualInstrumentation/Propagators/SpanContextInjectorInjectIntegration.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,14 @@ internal static CallTargetState OnMethodBegin<TTarget, TCarrier, TAction, TSpanC
3333
// The Injector.Inject method currently _only_ works with SpanContext objects
3434
// Therefore, there's no point calling inject unless we can remap it to a SpanContext
3535
TelemetryFactory.Metrics.Record(PublicApiUsage.SpanContextInjector_Inject);
36-
var inject = (Action<TCarrier, string, string>)(object)setter!;
3736

3837
if (SpanContextHelper.GetContext(context) is { } spanContext)
3938
{
40-
SpanContextInjector.InjectInternal(carrier, inject, spanContext);
39+
// We can't trust that the customer function is safe,
40+
// so we wrap the method in a try/catch to ensure we don't throw
41+
var inject = (Action<TCarrier, string, string>)(object)setter!;
42+
var injector = new SafeInjector<TCarrier>(inject);
43+
SpanContextInjector.InjectInternal(carrier, injector.SafeInject, spanContext);
4144
}
4245

4346
return CallTargetState.GetDefault();
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
// <copyright file="SpanContextPropagationTests.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;
7+
using System.Collections.Generic;
8+
using Datadog.Trace.ClrProfiler.AutoInstrumentation.ManualInstrumentation.Propagators;
9+
using FluentAssertions;
10+
using Xunit;
11+
12+
namespace Datadog.Trace.Tests.ManualInstrumentation;
13+
14+
public class SpanContextPropagationTests
15+
{
16+
[Fact]
17+
public void SpanContextExtractorExtractIntegration_WhenUserProvidedGetterReturnsNull_IntegrationCatchesException()
18+
{
19+
var headers = new Dictionary<string, string[]>();
20+
Func<Dictionary<string, string[]>, string, IEnumerable<string>> getter = (dict, key) => null;
21+
22+
// instance isn't used, so just passing this to avoid generic gymnastics
23+
var state = SpanContextExtractorExtractIntegration.OnMethodBegin(this, headers, getter);
24+
var result = state.State as SpanContext;
25+
result.Should().BeNull();
26+
}
27+
28+
[Fact]
29+
public void SpanContextExtractorExtractIntegration_WhenUserProvidedGetterThrowsException_IntegrationCatchesException()
30+
{
31+
var headers = new Dictionary<string, string[]>();
32+
Func<Dictionary<string, string[]>, string, IEnumerable<string>> getter = (dict, key) => throw new Exception("oops!");
33+
34+
// instance isn't used, so just passing this to avoid generic gymnastics
35+
var state = SpanContextExtractorExtractIntegration.OnMethodBegin(this, headers, getter);
36+
var result = state.State as SpanContext;
37+
result.Should().BeNull();
38+
}
39+
40+
[Fact]
41+
public void SpanContextExtractorExtractIncludingDsmIntegration_WhenUserProvidedGetterReturnsNull_IntegrationCatchesException()
42+
{
43+
var headers = new Dictionary<string, string[]>();
44+
Func<Dictionary<string, string[]>, string, IEnumerable<string>> getter = (dict, key) => null;
45+
46+
// instance isn't used, so just passing this to avoid generic gymnastics
47+
var state = SpanContextExtractorExtractIncludingDsmIntegration.OnMethodBegin(this, headers, getter, "messageType", "source");
48+
var result = state.State as SpanContext;
49+
result.Should().BeNull();
50+
}
51+
52+
[Fact]
53+
public void SpanContextExtractorExtractIncludingDsmIntegration_WhenUserProvidedGetterThrowsException_IntegrationCatchesException()
54+
{
55+
var headers = new Dictionary<string, string[]>();
56+
Func<Dictionary<string, string[]>, string, IEnumerable<string>> getter = (dict, key) => throw new Exception("oops!");
57+
58+
// instance isn't used, so just passing this to avoid generic gymnastics
59+
var state = SpanContextExtractorExtractIncludingDsmIntegration.OnMethodBegin(this, headers, getter, "messageType", "source");
60+
var result = state.State as SpanContext;
61+
result.Should().BeNull();
62+
}
63+
64+
[Fact]
65+
public void SpanContextInjectorInjectIntegration_WhenUserProvidedGetterThrowsException_IntegrationCatchesException()
66+
{
67+
var spanContext = new SpanContext(123, 123, SamplingPriority.UserKeep);
68+
var headers = new Dictionary<string, string[]>();
69+
Action<Dictionary<string, string[]>, string, string> setter = (dict, key, value) => throw new Exception("oops!");
70+
71+
// instance isn't used, so just passing this to avoid generic gymnastics
72+
var state = SpanContextInjectorInjectIntegration.OnMethodBegin(this, headers, setter, spanContext);
73+
var result = state.State as SpanContext;
74+
result.Should().BeNull();
75+
}
76+
77+
[Fact]
78+
public void SpanContextInjectorInjectIncludingDsmIntegration_WhenUserProvidedGetterThrowsException_IntegrationCatchesException()
79+
{
80+
var headers = new Dictionary<string, string[]>();
81+
var spanContext = new SpanContext(123, 123, SamplingPriority.UserKeep);
82+
Action<Dictionary<string, string[]>, string, string> setter = (dict, key, value) => throw new Exception("oops!");
83+
84+
// instance isn't used, so just passing this to avoid generic gymnastics
85+
var state = SpanContextInjectorInjectIncludingDsmIntegration.OnMethodBegin(this, headers, setter, spanContext, "messageType", "source");
86+
var result = state.State as SpanContext;
87+
result.Should().BeNull();
88+
}
89+
}

tracer/test/test-applications/integrations/Samples.ManualInstrumentation/Program.cs

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,64 @@ async Task OtherStuff()
255255
{
256256
s1.Span.ServiceName = Tracer.Instance.DefaultServiceName;
257257
await SendHttpRequest("CustomContext");
258+
259+
// Test injection
260+
Dictionary<string, List<string>> headers = new();
261+
new SpanContextInjector().Inject(
262+
headers,
263+
setter: (dict, key, value) => headers[key] = new List<string> { value },
264+
s1.Span.Context);
265+
var context = new SpanContextExtractor().Extract(
266+
headers,
267+
getter: (dict, key) => dict.TryGetValue(key, out var values) ? values : Enumerable.Empty<string>());
268+
269+
Expect(context is not null, "Extracted context should not be null");
270+
Expect(s1.Span.Context.SpanId == context?.SpanId, "SpanId should be extracted");
271+
Expect(s1.Span.Context.TraceId == context?.TraceId, "TraceId should be extracted");
272+
273+
// Test DSM injection
274+
Dictionary<string, List<string>> dsmHeaders = new();
275+
new SpanContextInjector().InjectIncludingDsm(
276+
dsmHeaders,
277+
setter: (dict, key, value) => dsmHeaders[key] = new List<string> { value },
278+
s1.Span.Context,
279+
"messageType",
280+
"messageId");
281+
var dsmContext = new SpanContextExtractor().ExtractIncludingDsm(
282+
dsmHeaders,
283+
getter: (dict, key) => dict.TryGetValue(key, out var values) ? values : Enumerable.Empty<string>(),
284+
"messageType",
285+
"messageId");
286+
Expect(dsmContext is not null, "Extracted context should not be null");
287+
Expect(s1.Span.Context.SpanId == dsmContext?.SpanId, "SpanId should be extracted");
288+
Expect(s1.Span.Context.TraceId == dsmContext?.TraceId, "TraceId should be extracted");
289+
290+
// Test that we handle incorrect (null returning) implementations
291+
var nullContext1 = new SpanContextExtractor().Extract(
292+
headers,
293+
getter: (dict, key) => null); // Always return null
294+
Expect(nullContext1 is null, "Extracted context should be null");
295+
296+
var nullContext2 = new SpanContextExtractor().ExtractIncludingDsm(
297+
dsmHeaders,
298+
getter: (dict, key) => null, // Always return null
299+
"messageType",
300+
"messageId");
301+
Expect(nullContext2 is null, "Extracted context should be null");
302+
303+
// Exceptions thrown in the faulty injector/extractor will not bubble up
304+
// as they're caught in the integration, but they will write error logs if we don't handle them
305+
// so will be caught in the CheckLogsForErrors stage in CI.
306+
new SpanContextInjector().Inject(
307+
headers,
308+
setter: (dict, key, value) => throw new Exception("Throwing exception that should be ignored"),
309+
s1.Span.Context);
310+
new SpanContextInjector().InjectIncludingDsm(
311+
headers,
312+
setter: (dict, key, value) => throw new Exception("Throwing exception that should be ignored"),
313+
s1.Span.Context,
314+
"messageType",
315+
"messageId");
258316
}
259317

260318
// Manually disable debug logs

0 commit comments

Comments
 (0)