Skip to content

Commit 6ddcd9a

Browse files
[Instrumentation.Wcf] Allow WCF services to record exceptions if RecordException is set (#2880)
Co-authored-by: Matt Hensley <130569+matt-hensley@users.noreply.github.com>
1 parent b1767d6 commit 6ddcd9a

10 files changed

Lines changed: 200 additions & 2 deletions

src/OpenTelemetry.Instrumentation.Wcf/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

33
## Unreleased
44

5+
* Added server instrumentation support for `RecordException` option.
6+
([#2880](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/2880))
7+
58
## 1.12.0-beta.1
69

710
Released 2025-May-06

src/OpenTelemetry.Instrumentation.Wcf/Implementation/TelemetryDispatchMessageInspector.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,11 @@ internal TelemetryDispatchMessageInspector(IDictionary<string, ActionMetadata> a
104104
}
105105
}
106106

107+
if (WcfInstrumentationActivitySource.Options.RecordException)
108+
{
109+
OperationContext.Current?.Extensions.Add(new WcfOperationContext(activity));
110+
}
111+
107112
if (textMapPropagator is not TraceContextPropagator)
108113
{
109114
Baggage.Current = ctx.Baggage;
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
// Copyright The OpenTelemetry Authors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
#if NETFRAMEWORK
5+
using System.Diagnostics;
6+
using System.ServiceModel;
7+
using System.ServiceModel.Channels;
8+
using System.ServiceModel.Dispatcher;
9+
10+
namespace OpenTelemetry.Instrumentation.Wcf.Implementation;
11+
12+
internal class TracingErrorHandler : IErrorHandler
13+
{
14+
public bool HandleError(Exception error)
15+
{
16+
return false;
17+
}
18+
19+
public void ProvideFault(Exception error, MessageVersion version, ref Message fault)
20+
{
21+
// By rights this should be in `HandleError` instead, which would keep it from
22+
// interfering with the response to the client.
23+
// However, by the time `HandleError` fires, the Activity has already been stopped
24+
// so the error appears after the Activity has completed. Additionally, sometimes
25+
// the context has already been disposed or otherwise lost, preventing association
26+
// at all.
27+
// Also it becomes very difficult to unit-test, because there is no easy `ErrorsHandled`
28+
// event to listen for before checking to see whether the error was logged.
29+
30+
if (!WcfInstrumentationActivitySource.Options?.RecordException ?? false)
31+
{
32+
return;
33+
}
34+
35+
// OperationContext.Current *should* be reliable even in async calls at .NET 4.6.2+.
36+
// In older versions it may not be.
37+
var context = OperationContext.Current?.Extensions.Find<WcfOperationContext>();
38+
var activity = context?.Activity ?? WcfInstrumentationActivitySource.ActivitySource.StartActivity(WcfInstrumentationActivitySource.UnassociatedExceptionActivityName, ActivityKind.Internal);
39+
40+
activity?.AddException(error);
41+
42+
if (activity != context?.Activity)
43+
{
44+
activity?.Stop();
45+
}
46+
}
47+
}
48+
#endif

src/OpenTelemetry.Instrumentation.Wcf/Implementation/WcfInstrumentationActivitySource.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ internal static class WcfInstrumentationActivitySource
1919
internal static readonly string ActivitySourceName = AssemblyName.Name!;
2020
internal static readonly string IncomingRequestActivityName = ActivitySourceName + ".IncomingRequest";
2121
internal static readonly string OutgoingRequestActivityName = ActivitySourceName + ".OutgoingRequest";
22+
internal static readonly string UnassociatedExceptionActivityName = ActivitySourceName + ".Exception";
2223

2324
public static ActivitySource ActivitySource { get; } = new(ActivitySourceName, Assembly.GetPackageVersion());
2425

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// Copyright The OpenTelemetry Authors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
#if NETFRAMEWORK
5+
using System.Diagnostics;
6+
using System.ServiceModel;
7+
8+
namespace OpenTelemetry.Instrumentation.Wcf.Implementation;
9+
10+
internal class WcfOperationContext : IExtension<OperationContext>
11+
{
12+
public WcfOperationContext(Activity activity)
13+
{
14+
this.Activity = activity;
15+
}
16+
17+
public Activity Activity { get; }
18+
19+
public void Attach(OperationContext owner)
20+
{
21+
}
22+
23+
public void Detach(OperationContext owner)
24+
{
25+
}
26+
}
27+
#endif

src/OpenTelemetry.Instrumentation.Wcf/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -233,8 +233,8 @@ This instrumentation can be configured to change the default behavior by using
233233

234234
This instrumentation automatically sets Activity Status to Error if an unhandled
235235
exception is thrown. Additionally, `RecordException` feature may be turned on,
236-
to store the exception to the Activity itself as ActivityEvent. `RecordException`
237-
is available only on the client side.
236+
to store the exception to the Activity itself as ActivityEvent. This feature
237+
applies both on instrumented servers and clients.
238238

239239
## References
240240

src/OpenTelemetry.Instrumentation.Wcf/TelemetryContractBehaviorAttribute.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ public void ApplyDispatchBehavior(ContractDescription contractDescription, Servi
4545
{
4646
#if NETFRAMEWORK
4747
Guard.ThrowIfNull(dispatchRuntime);
48+
dispatchRuntime.ChannelDispatcher.ErrorHandlers.Add(new TracingErrorHandler());
4849
TelemetryEndpointBehavior.ApplyDispatchBehaviorToEndpoint(dispatchRuntime.EndpointDispatcher);
4950
#endif
5051
}

src/OpenTelemetry.Instrumentation.Wcf/TelemetryEndpointBehavior.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ public void ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher e
4141
{
4242
#if NETFRAMEWORK
4343
Guard.ThrowIfNull(endpointDispatcher);
44+
endpointDispatcher.ChannelDispatcher.ErrorHandlers.Add(new TracingErrorHandler());
4445
ApplyDispatchBehaviorToEndpoint(endpointDispatcher);
4546
#endif
4647
}

src/OpenTelemetry.Instrumentation.Wcf/TelemetryServiceBehavior.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.ServiceModel;
66
using System.ServiceModel.Description;
77
using System.ServiceModel.Dispatcher;
8+
using OpenTelemetry.Instrumentation.Wcf.Implementation;
89
using OpenTelemetry.Internal;
910

1011
namespace OpenTelemetry.Instrumentation.Wcf;
@@ -27,6 +28,9 @@ public void ApplyDispatchBehavior(ServiceDescription serviceDescription, Service
2728
foreach (var channelDispatcherBase in serviceHostBase.ChannelDispatchers)
2829
{
2930
var channelDispatcher = (ChannelDispatcher)channelDispatcherBase;
31+
32+
channelDispatcher.ErrorHandlers.Add(new TracingErrorHandler());
33+
3034
foreach (var endpointDispatcher in channelDispatcher.Endpoints)
3135
{
3236
TelemetryEndpointBehavior.ApplyDispatchBehaviorToEndpoint(endpointDispatcher);

test/OpenTelemetry.Instrumentation.Wcf.Tests/TelemetryDispatchMessageInspectorTests.netfx.cs

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,114 @@ await client.ExecuteAsync(
205205
Assert.Empty(stoppedActivities);
206206
}
207207
}
208+
209+
[Theory]
210+
[InlineData(true, true, true)]
211+
[InlineData(true, true, false)]
212+
[InlineData(true, false, true)]
213+
[InlineData(false, true, true)]
214+
[InlineData(true, false, false)]
215+
[InlineData(false, false, true)]
216+
[InlineData(false, true, false)]
217+
[InlineData(false, false, false)]
218+
public async Task RecordExceptionTest(
219+
bool recordException,
220+
bool triggerException,
221+
bool runAsync)
222+
{
223+
List<Activity> stoppedActivities = [];
224+
List<Activity> startedActivities = [];
225+
226+
List<Exception> recordedExceptions = [];
227+
using var activityListener = new ActivityListener
228+
{
229+
ShouldListenTo = activitySource => true,
230+
ActivityStarted = startedActivities.Add,
231+
ActivityStopped = stoppedActivities.Add,
232+
};
233+
234+
activityListener.ExceptionRecorder += (Activity activity, Exception ex, ref TagList tags) =>
235+
{
236+
recordedExceptions.Add(ex);
237+
};
238+
239+
ActivitySource.AddActivityListener(activityListener);
240+
241+
var tracerProvider = Sdk.CreateTracerProviderBuilder()
242+
.AddWcfInstrumentation(options =>
243+
{
244+
options.RecordException = recordException;
245+
})
246+
.Build();
247+
248+
var client = new ServiceClient(
249+
new NetTcpBinding(),
250+
new EndpointAddress(new Uri(this.serviceBaseUri, "/Service")));
251+
try
252+
{
253+
if (triggerException)
254+
{
255+
if (runAsync)
256+
{
257+
await client.ErrorAsync();
258+
}
259+
else
260+
{
261+
client.ErrorSynchronous();
262+
}
263+
}
264+
else
265+
{
266+
if (runAsync)
267+
{
268+
await client.ExecuteAsync(
269+
new ServiceRequest(
270+
payload: "Hello Open Telemetry!"));
271+
}
272+
else
273+
{
274+
client.ExecuteSynchronous(
275+
new ServiceRequest(
276+
payload: "Hello Open Telemetry!"));
277+
}
278+
}
279+
}
280+
catch (Exception)
281+
{
282+
}
283+
finally
284+
{
285+
startedActivities[0].AddTag(nameof(recordException), recordException);
286+
startedActivities[0].AddTag(nameof(triggerException), triggerException);
287+
startedActivities[0].AddTag(nameof(runAsync), runAsync);
288+
289+
if (client.State == CommunicationState.Faulted)
290+
{
291+
client.Abort();
292+
}
293+
else
294+
{
295+
client.Close();
296+
}
297+
298+
tracerProvider?.Shutdown();
299+
tracerProvider?.Dispose();
300+
301+
WcfInstrumentationActivitySource.Options = null;
302+
}
303+
304+
Assert.NotEmpty(stoppedActivities);
305+
var activity = Assert.Single(stoppedActivities);
306+
307+
if (recordException && triggerException)
308+
{
309+
Assert.Collection(recordedExceptions, e => Assert.IsType<Exception>(e));
310+
}
311+
else
312+
{
313+
Assert.Empty(recordedExceptions);
314+
}
315+
}
208316
}
209317

210318
#endif

0 commit comments

Comments
 (0)