Skip to content

Commit 66c065c

Browse files
[Extensions.Enrichment] Add exception handling to trace enrichment (#4165)
Co-authored-by: Martin Costello <martin@martincostello.com>
1 parent 34f31f4 commit 66c065c

8 files changed

Lines changed: 294 additions & 3 deletions

File tree

src/OpenTelemetry.Extensions.Enrichment/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@
55
* Updated OpenTelemetry core component version(s) to `1.15.3`.
66
([#4166](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/4166))
77

8+
* Fixed trace enrichment callbacks so exceptions thrown by user-provided
9+
enrichers or enrichment actions no longer interrupt trace processing.
10+
([#4165](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/4165))
11+
812
## 1.15.0-beta.1
913

1014
Released 2026-Jan-21
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
// Copyright The OpenTelemetry Authors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
using System.Diagnostics.Tracing;
5+
using OpenTelemetry.Internal;
6+
7+
namespace OpenTelemetry.Extensions.Enrichment;
8+
9+
[EventSource(Name = "OpenTelemetry-Extensions-Enrichment")]
10+
internal sealed class EnrichmentEventSource : EventSource
11+
{
12+
public static EnrichmentEventSource Log = new();
13+
14+
private EnrichmentEventSource()
15+
{
16+
}
17+
18+
[NonEvent]
19+
public void TraceEnricherException(string operationName, TraceEnricher enricher, Exception ex)
20+
{
21+
if (this.IsEnabled(EventLevel.Warning, EventKeywords.All))
22+
{
23+
var enricherType = enricher.GetType();
24+
this.TraceEnricherException(operationName, enricherType.FullName ?? enricherType.Name, ex.ToInvariantString());
25+
}
26+
}
27+
28+
[NonEvent]
29+
public void TraceEnrichmentActionException(Action<TraceEnrichmentBag> action, Exception ex)
30+
{
31+
if (this.IsEnabled(EventLevel.Warning, EventKeywords.All))
32+
{
33+
var method = action.Method;
34+
var declaringType = method.DeclaringType;
35+
var actionName = declaringType is null
36+
? method.Name
37+
: $"{declaringType.FullName ?? declaringType.Name}.{method.Name}";
38+
39+
this.TraceEnrichmentActionException(actionName, ex.ToInvariantString());
40+
}
41+
}
42+
43+
[Event(1, Message = "Trace enricher '{0}' threw during '{1}'. Trace processing will continue. Exception: '{2}'.", Level = EventLevel.Warning)]
44+
public void TraceEnricherException(string enricherType, string operationName, string exception)
45+
{
46+
this.WriteEvent(1, enricherType, operationName, exception);
47+
}
48+
49+
[Event(2, Message = "Trace enrichment action '{0}' threw. Trace processing will continue. Exception: '{1}'.", Level = EventLevel.Warning)]
50+
public void TraceEnrichmentActionException(string actionName, string exception)
51+
{
52+
this.WriteEvent(2, actionName, exception);
53+
}
54+
}

src/OpenTelemetry.Extensions.Enrichment/Internal/TraceEnrichmentActions.cs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,15 @@ public override void Enrich(in TraceEnrichmentBag bag)
1818
{
1919
for (var i = 0; i < this.actions.Length; i++)
2020
{
21-
this.actions[i].Invoke(bag);
21+
var action = this.actions[i];
22+
try
23+
{
24+
action.Invoke(bag);
25+
}
26+
catch (Exception ex)
27+
{
28+
EnrichmentEventSource.Log.TraceEnrichmentActionException(action, ex);
29+
}
2230
}
2331
}
2432
}

src/OpenTelemetry.Extensions.Enrichment/Internal/TraceEnrichmentProcessor.cs

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,14 @@ public override void OnStart(Activity activity)
2222

2323
foreach (var enricher in this.traceEnrichers)
2424
{
25-
enricher.EnrichOnActivityStart(bag);
25+
try
26+
{
27+
enricher.EnrichOnActivityStart(bag);
28+
}
29+
catch (Exception ex)
30+
{
31+
EnrichmentEventSource.Log.TraceEnricherException(nameof(this.OnStart), enricher, ex);
32+
}
2633
}
2734
}
2835

@@ -32,7 +39,14 @@ public override void OnEnd(Activity activity)
3239

3340
foreach (var enricher in this.traceEnrichers)
3441
{
35-
enricher.Enrich(bag);
42+
try
43+
{
44+
enricher.Enrich(bag);
45+
}
46+
catch (Exception ex)
47+
{
48+
EnrichmentEventSource.Log.TraceEnricherException(nameof(this.OnEnd), enricher, ex);
49+
}
3650
}
3751
}
3852
}

src/OpenTelemetry.Extensions.Enrichment/OpenTelemetry.Extensions.Enrichment.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
</ItemGroup>
2424

2525
<ItemGroup>
26+
<Compile Include="$(RepoRoot)\src\Shared\ExceptionExtensions.cs" Link="Includes\ExceptionExtensions.cs" />
2627
<Compile Include="$(RepoRoot)\src\Shared\Guard.cs" Link="Includes\Guard.cs" />
2728
</ItemGroup>
2829

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// Copyright The OpenTelemetry Authors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
using OpenTelemetry.Tests;
5+
using Xunit;
6+
7+
namespace OpenTelemetry.Extensions.Enrichment.Tests;
8+
9+
public class EventSourceTests
10+
{
11+
[Fact]
12+
public void EventSourceTests_EnrichmentEventSource()
13+
{
14+
var eventSourceType = typeof(TraceEnricher).Assembly.GetType("OpenTelemetry.Extensions.Enrichment.EnrichmentEventSource", throwOnError: true)!;
15+
EventSourceTestHelper.ValidateEventSourceIds(eventSourceType);
16+
}
17+
}

test/OpenTelemetry.Extensions.Enrichment.Tests/OpenTelemetry.Extensions.Enrichment.Tests.csproj

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,9 @@
1717
<ProjectReference Include="..\..\src\OpenTelemetry.Extensions.Enrichment\OpenTelemetry.Extensions.Enrichment.csproj" />
1818
</ItemGroup>
1919

20+
<ItemGroup>
21+
<Compile Include="$(RepoRoot)\test\Shared\EventSourceTestHelper.cs" Link="Includes\EventSourceTestHelper.cs" />
22+
<Compile Include="$(RepoRoot)\test\Shared\InMemoryEventListener.cs" Link="Includes\InMemoryEventListener.cs" />
23+
</ItemGroup>
24+
2025
</Project>

test/OpenTelemetry.Extensions.Enrichment.Tests/OpenTelemetryEnrichmentServiceCollectionExtensionsTests.cs

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,11 @@
22
// SPDX-License-Identifier: Apache-2.0
33

44
using System.Diagnostics;
5+
using System.Diagnostics.Tracing;
6+
using System.Reflection;
57
using Microsoft.Extensions.DependencyInjection;
68
using Microsoft.Extensions.Hosting;
9+
using OpenTelemetry.Tests;
710
using OpenTelemetry.Trace;
811
using Xunit;
912

@@ -183,4 +186,189 @@ public async Task FactoryMethod_RegistersEnricher()
183186

184187
await host.StopAsync();
185188
}
189+
190+
[Fact]
191+
public async Task DelegateMethod_SwallowsExceptionsFromEnrichmentActions()
192+
{
193+
var exportedItems = new List<Activity>();
194+
using var eventListener = new InMemoryEventListener(GetEnrichmentEventSource(), EventLevel.Verbose);
195+
196+
const string testKey = "safe-key";
197+
const string testValue = "safe-value";
198+
199+
using var host = Host.CreateDefaultBuilder()
200+
.ConfigureServices(services => services
201+
.AddOpenTelemetry()
202+
.WithTracing(builder => builder
203+
.AddSource(SourceName)
204+
.AddInMemoryExporter(exportedItems))
205+
.Services
206+
.AddTraceEnricher(ThrowingEnrichmentAction)
207+
.AddTraceEnricher((Action<TraceEnrichmentBag>)(bag => bag.Add(testKey, testValue))))
208+
.Build();
209+
210+
await host.StartAsync();
211+
212+
using var source = new ActivitySource(SourceName);
213+
214+
var exception = Record.Exception(() =>
215+
{
216+
using var activity = source.StartActivity(SourceName);
217+
Assert.NotNull(activity);
218+
activity.Stop();
219+
});
220+
221+
Assert.Null(exception);
222+
Assert.Single(exportedItems);
223+
Assert.Equal(testValue, exportedItems[0].TagObjects.Single(tag => tag.Key == testKey).Value);
224+
225+
var loggedEvent = Assert.Single(eventListener.Events);
226+
var payload = loggedEvent.Payload!.Select(Assert.IsType<string>).ToArray();
227+
Assert.Equal(2, loggedEvent.EventId);
228+
Assert.Contains(payload, value => value.Contains(nameof(ThrowingEnrichmentAction), StringComparison.Ordinal));
229+
Assert.Contains(payload, value => value.Contains("boom", StringComparison.Ordinal));
230+
231+
await host.StopAsync();
232+
}
233+
234+
[Fact]
235+
public async Task InstanceMethod_SwallowsExceptionsFromEnrich()
236+
{
237+
var exportedItems = new List<Activity>();
238+
using var eventListener = new InMemoryEventListener(GetEnrichmentEventSource(), EventLevel.Verbose);
239+
var trackingEnricher = new TrackingTraceEnricher();
240+
241+
using var host = Host.CreateDefaultBuilder()
242+
.ConfigureServices(services => services
243+
.AddOpenTelemetry()
244+
.WithTracing(builder => builder
245+
.AddSource(SourceName)
246+
.AddInMemoryExporter(exportedItems))
247+
.Services
248+
.TryAddTraceEnricher(new ThrowingOnEndTraceEnricher())
249+
.TryAddTraceEnricher(trackingEnricher))
250+
.Build();
251+
252+
await host.StartAsync();
253+
254+
using var source = new ActivitySource(SourceName);
255+
256+
var exception = Record.Exception(() =>
257+
{
258+
using var activity = source.StartActivity(SourceName);
259+
Assert.NotNull(activity);
260+
activity.Stop();
261+
});
262+
263+
Assert.Null(exception);
264+
Assert.Equal(1, trackingEnricher.StartCalls);
265+
Assert.Equal(1, trackingEnricher.EndCalls);
266+
Assert.Single(exportedItems);
267+
Assert.Equal(TrackingTraceEnricher.EndValue, exportedItems[0].TagObjects.Single(tag => tag.Key == TrackingTraceEnricher.EndKey).Value);
268+
269+
var loggedEvent = Assert.Single(eventListener.Events);
270+
var payload = loggedEvent.Payload!.Select(Assert.IsType<string>).ToArray();
271+
Assert.Equal(1, loggedEvent.EventId);
272+
Assert.Contains(payload, value => value.Contains(nameof(ThrowingOnEndTraceEnricher), StringComparison.Ordinal));
273+
Assert.Contains("OnEnd", payload);
274+
Assert.Contains(payload, value => value.Contains("boom", StringComparison.Ordinal));
275+
276+
await host.StopAsync();
277+
}
278+
279+
[Fact]
280+
public async Task InstanceMethod_SwallowsExceptionsFromEnrichOnActivityStart()
281+
{
282+
var exportedItems = new List<Activity>();
283+
using var eventListener = new InMemoryEventListener(GetEnrichmentEventSource(), EventLevel.Verbose);
284+
var trackingEnricher = new TrackingTraceEnricher();
285+
286+
using var host = Host.CreateDefaultBuilder()
287+
.ConfigureServices(services => services
288+
.AddOpenTelemetry()
289+
.WithTracing(builder => builder
290+
.AddSource(SourceName)
291+
.AddInMemoryExporter(exportedItems))
292+
.Services
293+
.TryAddTraceEnricher(new ThrowingOnStartTraceEnricher())
294+
.TryAddTraceEnricher(trackingEnricher))
295+
.Build();
296+
297+
await host.StartAsync();
298+
299+
using var source = new ActivitySource(SourceName);
300+
301+
var exception = Record.Exception(() =>
302+
{
303+
using var activity = source.StartActivity(SourceName);
304+
Assert.NotNull(activity);
305+
activity.Stop();
306+
});
307+
308+
Assert.Null(exception);
309+
Assert.Equal(1, trackingEnricher.StartCalls);
310+
Assert.Equal(1, trackingEnricher.EndCalls);
311+
Assert.Single(exportedItems);
312+
Assert.Equal(TrackingTraceEnricher.StartValue, exportedItems[0].TagObjects.Single(tag => tag.Key == TrackingTraceEnricher.StartKey).Value);
313+
314+
var loggedEvent = Assert.Single(eventListener.Events);
315+
var payload = loggedEvent.Payload!.Select(Assert.IsType<string>).ToArray();
316+
Assert.Equal(1, loggedEvent.EventId);
317+
Assert.Contains(payload, value => value.Contains(nameof(ThrowingOnStartTraceEnricher), StringComparison.Ordinal));
318+
Assert.Contains("OnStart", payload);
319+
Assert.Contains(payload, value => value.Contains("boom", StringComparison.Ordinal));
320+
321+
await host.StopAsync();
322+
}
323+
324+
private static void ThrowingEnrichmentAction(TraceEnrichmentBag bag)
325+
=> throw new InvalidOperationException("boom");
326+
327+
private static EventSource GetEnrichmentEventSource()
328+
{
329+
var eventSourceType = typeof(TraceEnricher).Assembly.GetType("OpenTelemetry.Extensions.Enrichment.EnrichmentEventSource", throwOnError: true)!;
330+
return (EventSource)eventSourceType
331+
.GetField("Log", BindingFlags.Public | BindingFlags.Static)!
332+
.GetValue(null)!;
333+
}
334+
335+
private sealed class ThrowingOnStartTraceEnricher : TraceEnricher
336+
{
337+
public override void Enrich(in TraceEnrichmentBag bag)
338+
{
339+
}
340+
341+
public override void EnrichOnActivityStart(in TraceEnrichmentBag bag)
342+
=> throw new InvalidOperationException("boom");
343+
}
344+
345+
private sealed class ThrowingOnEndTraceEnricher : TraceEnricher
346+
{
347+
public override void Enrich(in TraceEnrichmentBag bag)
348+
=> throw new InvalidOperationException("boom");
349+
}
350+
351+
private sealed class TrackingTraceEnricher : TraceEnricher
352+
{
353+
public const string StartKey = "tracking-start";
354+
public const string StartValue = "start";
355+
public const string EndKey = "tracking-end";
356+
public const string EndValue = "end";
357+
358+
public int StartCalls { get; private set; }
359+
360+
public int EndCalls { get; private set; }
361+
362+
public override void Enrich(in TraceEnrichmentBag bag)
363+
{
364+
this.EndCalls++;
365+
bag.Add(EndKey, EndValue);
366+
}
367+
368+
public override void EnrichOnActivityStart(in TraceEnrichmentBag bag)
369+
{
370+
this.StartCalls++;
371+
bag.Add(StartKey, StartValue);
372+
}
373+
}
186374
}

0 commit comments

Comments
 (0)