From 9ec98ab12006751b74c99be761d1e54a8ead25cd Mon Sep 17 00:00:00 2001 From: Reiley Yang Date: Sat, 6 Mar 2021 07:19:49 -0800 Subject: [PATCH] Add SetErrorStatusOnException option to TracerProviderSdk (#1858) * add SetErrorStatusOnUnhandledException option to TracerProviderSdk * changelog * add doc * markdown lint * fix nits * markdown lint * update public api file * fix nits in the changelog * add test cases * improve example * improve doc * rename * change the wording in the doc * use RecordException in the example * tweak the doc * address review comment from Tom * adjust wording * Revert "adjust wording" This reverts commit 9bf74a336dfad203b5d78e632b28b9c70a32073a. * better exception message * update example/doc * more test cases * address corner case in test Co-authored-by: Cijo Thomas --- OpenTelemetry.sln | 7 + docs/Directory.Build.props | 2 +- docs/trace/exception-handling/Program.cs | 53 ++++++++ docs/trace/exception-handling/README.md | 115 +++++++++++++++++ .../exception-handling.csproj | 8 ++ .../.publicApi/net452/PublicAPI.Unshipped.txt | 5 + .../.publicApi/net46/PublicAPI.Unshipped.txt | 5 + .../.publicApi/net461/PublicAPI.Unshipped.txt | 5 + .../netstandard2.0/PublicAPI.Unshipped.txt | 5 + src/OpenTelemetry/CHANGELOG.md | 9 +- src/OpenTelemetry/Sdk.cs | 18 ++- src/OpenTelemetry/Trace/ExceptionProcessor.cs | 80 ++++++++++++ .../Trace/TracerProviderBuilderSdk.cs | 16 ++- .../Trace/TracerProviderOptions.cs | 27 ++++ .../Trace/ExceptionProcessorTest.cs | 120 ++++++++++++++++++ .../Trace/TracerProviderOptionsTest.cs | 81 ++++++++++++ 16 files changed, 547 insertions(+), 9 deletions(-) create mode 100644 docs/trace/exception-handling/Program.cs create mode 100644 docs/trace/exception-handling/README.md create mode 100644 docs/trace/exception-handling/exception-handling.csproj create mode 100644 src/OpenTelemetry/Trace/ExceptionProcessor.cs create mode 100644 src/OpenTelemetry/Trace/TracerProviderOptions.cs create mode 100644 test/OpenTelemetry.Tests/Trace/ExceptionProcessorTest.cs create mode 100644 test/OpenTelemetry.Tests/Trace/TracerProviderOptionsTest.cs diff --git a/OpenTelemetry.sln b/OpenTelemetry.sln index d3d368ae3..c4cd9ca81 100644 --- a/OpenTelemetry.sln +++ b/OpenTelemetry.sln @@ -197,6 +197,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenTelemetry.Shared", "src EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestApp.AspNetCore.5.0", "test\TestApp.AspNetCore.5.0\TestApp.AspNetCore.5.0.csproj", "{972396A8-E35B-499C-9BA1-765E9B8822E1}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "exception-handling", "docs\trace\exception-handling\exception-handling.csproj", "{08D29501-F0A3-468F-B18D-BD1821A72383}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -387,6 +389,10 @@ Global {972396A8-E35B-499C-9BA1-765E9B8822E1}.Debug|Any CPU.Build.0 = Debug|Any CPU {972396A8-E35B-499C-9BA1-765E9B8822E1}.Release|Any CPU.ActiveCfg = Release|Any CPU {972396A8-E35B-499C-9BA1-765E9B8822E1}.Release|Any CPU.Build.0 = Release|Any CPU + {08D29501-F0A3-468F-B18D-BD1821A72383}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {08D29501-F0A3-468F-B18D-BD1821A72383}.Debug|Any CPU.Build.0 = Debug|Any CPU + {08D29501-F0A3-468F-B18D-BD1821A72383}.Release|Any CPU.ActiveCfg = Release|Any CPU + {08D29501-F0A3-468F-B18D-BD1821A72383}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -416,6 +422,7 @@ Global {B3F03725-23A0-4582-9526-F6A7E38F35CC} = {3862190B-E2C5-418E-AFDC-DB281FB5C705} {13C10C9A-07E8-43EB-91F5-C2B116FBE0FC} = {3862190B-E2C5-418E-AFDC-DB281FB5C705} {972396A8-E35B-499C-9BA1-765E9B8822E1} = {77C7929A-2EED-4AA6-8705-B5C443C8AA0F} + {08D29501-F0A3-468F-B18D-BD1821A72383} = {5B7FB835-3FFF-4BC2-99C5-A5B5FAE3C818} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {55639B5C-0770-4A22-AB56-859604650521} diff --git a/docs/Directory.Build.props b/docs/Directory.Build.props index 3b7db6739..96d086bdd 100644 --- a/docs/Directory.Build.props +++ b/docs/Directory.Build.props @@ -4,7 +4,7 @@ Exe - netcoreapp2.1;netcoreapp3.1 + netcoreapp2.1;netcoreapp3.1;net5.0 $(TargetFrameworks);net461;net462;net47;net471;net472;net48 diff --git a/docs/trace/exception-handling/Program.cs b/docs/trace/exception-handling/Program.cs new file mode 100644 index 000000000..1148dee2e --- /dev/null +++ b/docs/trace/exception-handling/Program.cs @@ -0,0 +1,53 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using System.Diagnostics; +using OpenTelemetry; +using OpenTelemetry.Trace; + +public class Program +{ + private static readonly ActivitySource MyActivitySource = new ActivitySource( + "MyCompany.MyProduct.MyLibrary"); + + public static void Main() + { + using var tracerProvider = Sdk.CreateTracerProviderBuilder(options => + { + options.SetErrorStatusOnException = true; + }) + .AddSource("MyCompany.MyProduct.MyLibrary") + .SetSampler(new AlwaysOnSampler()) + .AddConsoleExporter() + .Build(); + + try + { + using (MyActivitySource.StartActivity("Foo")) + { + using (MyActivitySource.StartActivity("Bar")) + { + throw new Exception("Oops!"); + } + } + } + catch (Exception) + { + // swallow the exception + } + } +} diff --git a/docs/trace/exception-handling/README.md b/docs/trace/exception-handling/README.md new file mode 100644 index 000000000..15fd107a5 --- /dev/null +++ b/docs/trace/exception-handling/README.md @@ -0,0 +1,115 @@ +# Exception Handling + +## User-handled Exception + +The term `User-handled Exception` is used to describe exceptions that are +handled by the application. + +While using `Activity` API, the common pattern would be: + +```csharp +using (var activity = MyActivitySource.StartActivity("Foo")) +{ + try + { + Func(); + } + catch (SomeException ex) + { + activity?.SetStatus(Status.Error); + DoSomething(); + } + catch (Exception) + { + activity?.SetStatus(Status.Error); + throw; + } +} +``` + +The above approach could become hard to manage if there are deeply nested +`Activity` objects, or there are activities created in a 3rd party library. + +The following configuration will automatically detect exception and set the +activity status to `Error`: + +```csharp +Sdk.CreateTracerProviderBuilder(options => { + options.SetErrorStatusOnException = true; +}); +``` + +A complete example can be found [here](./Program.cs). + +Note: this feature is platform dependent as it relies on +[System.Runtime.InteropServices.Marshal.GetExceptionPointers](https://docs.microsoft.com/dotnet/api/system.runtime.interopservices.marshal.getexceptionpointers). + +## Unhandled Exception + +The term `Unhandled Exception` is used to describe exceptions that are not +handled by the application. When an unhandled exception happened, the behavior +will depend on the presence of a debugger: + +* If there is no debugger, the exception will normally crash the process or + terminate the thread. +* If a debugger is attached, the debugger will be notified that an unhandled + exception happened. +* In case a postmortem debugger is configured, the postmortem debugger will be + activited and normally it will collect a crash dump. + +It might be useful to automatically capture the unhandled exceptions, travel +through the unfinished activities and export them for troubleshooting. Here goes +one possible way of doing this: + +**WARNING:** Use `AppDomain.UnhandledException` with caution. A throw in the +handler puts the process into an unrecoverable state. + + +```csharp +using System; +using System.Diagnostics; +using OpenTelemetry; +using OpenTelemetry.Trace; + +public class Program +{ + private static readonly ActivitySource MyActivitySource = new ActivitySource("MyCompany.MyProduct.MyLibrary"); + + public static void Main() + { + AppDomain.CurrentDomain.UnhandledException += UnhandledExceptionHandler; + + using var tracerProvider = Sdk.CreateTracerProviderBuilder(options => + { + options.SetErrorStatusOnException = true; + }) + .AddSource("MyCompany.MyProduct.MyLibrary") + .SetSampler(new AlwaysOnSampler()) + .AddConsoleExporter() + .Build(); + + using (MyActivitySource.StartActivity("Foo")) + { + using (MyActivitySource.StartActivity("Bar")) + { + throw new Exception("Oops!"); + } + } + } + + private static void UnhandledExceptionHandler(object source, UnhandledExceptionEventArgs args) + { + var ex = (Exception)args.ExceptionObject; + + var activity = Activity.Current; + + while (activity != null) + { + activity.RecordException(ex); + activity.Dispose(); + activity = activity.Parent; + } + } +} +``` + diff --git a/docs/trace/exception-handling/exception-handling.csproj b/docs/trace/exception-handling/exception-handling.csproj new file mode 100644 index 000000000..afb37b73c --- /dev/null +++ b/docs/trace/exception-handling/exception-handling.csproj @@ -0,0 +1,8 @@ + + + + + + diff --git a/src/OpenTelemetry/.publicApi/net452/PublicAPI.Unshipped.txt b/src/OpenTelemetry/.publicApi/net452/PublicAPI.Unshipped.txt index f0dee03ad..3b7ac888e 100644 --- a/src/OpenTelemetry/.publicApi/net452/PublicAPI.Unshipped.txt +++ b/src/OpenTelemetry/.publicApi/net452/PublicAPI.Unshipped.txt @@ -1,3 +1,8 @@ OpenTelemetry.Trace.ParentBasedSampler.ParentBasedSampler(OpenTelemetry.Trace.Sampler rootSampler, OpenTelemetry.Trace.Sampler remoteParentSampled = null, OpenTelemetry.Trace.Sampler remoteParentNotSampled = null, OpenTelemetry.Trace.Sampler localParentSampled = null, OpenTelemetry.Trace.Sampler localParentNotSampled = null) -> void +OpenTelemetry.Trace.TracerProviderOptions +OpenTelemetry.Trace.TracerProviderOptions.SetErrorStatusOnException.get -> bool +OpenTelemetry.Trace.TracerProviderOptions.SetErrorStatusOnException.set -> void +OpenTelemetry.Trace.TracerProviderOptions.TracerProviderOptions() -> void +static OpenTelemetry.Sdk.CreateTracerProviderBuilder(System.Action configure = null) -> OpenTelemetry.Trace.TracerProviderBuilder static OpenTelemetry.Trace.TracerProviderBuilderExtensions.AddLegacyActivity(this OpenTelemetry.Trace.TracerProviderBuilder tracerProviderBuilder, string operationName) -> OpenTelemetry.Trace.TracerProviderBuilder static OpenTelemetry.Trace.TracerProviderExtensions.ForceFlush(this OpenTelemetry.Trace.TracerProvider provider, int timeoutMilliseconds = -1) -> bool diff --git a/src/OpenTelemetry/.publicApi/net46/PublicAPI.Unshipped.txt b/src/OpenTelemetry/.publicApi/net46/PublicAPI.Unshipped.txt index f0dee03ad..3b7ac888e 100644 --- a/src/OpenTelemetry/.publicApi/net46/PublicAPI.Unshipped.txt +++ b/src/OpenTelemetry/.publicApi/net46/PublicAPI.Unshipped.txt @@ -1,3 +1,8 @@ OpenTelemetry.Trace.ParentBasedSampler.ParentBasedSampler(OpenTelemetry.Trace.Sampler rootSampler, OpenTelemetry.Trace.Sampler remoteParentSampled = null, OpenTelemetry.Trace.Sampler remoteParentNotSampled = null, OpenTelemetry.Trace.Sampler localParentSampled = null, OpenTelemetry.Trace.Sampler localParentNotSampled = null) -> void +OpenTelemetry.Trace.TracerProviderOptions +OpenTelemetry.Trace.TracerProviderOptions.SetErrorStatusOnException.get -> bool +OpenTelemetry.Trace.TracerProviderOptions.SetErrorStatusOnException.set -> void +OpenTelemetry.Trace.TracerProviderOptions.TracerProviderOptions() -> void +static OpenTelemetry.Sdk.CreateTracerProviderBuilder(System.Action configure = null) -> OpenTelemetry.Trace.TracerProviderBuilder static OpenTelemetry.Trace.TracerProviderBuilderExtensions.AddLegacyActivity(this OpenTelemetry.Trace.TracerProviderBuilder tracerProviderBuilder, string operationName) -> OpenTelemetry.Trace.TracerProviderBuilder static OpenTelemetry.Trace.TracerProviderExtensions.ForceFlush(this OpenTelemetry.Trace.TracerProvider provider, int timeoutMilliseconds = -1) -> bool diff --git a/src/OpenTelemetry/.publicApi/net461/PublicAPI.Unshipped.txt b/src/OpenTelemetry/.publicApi/net461/PublicAPI.Unshipped.txt index f0dee03ad..3b7ac888e 100644 --- a/src/OpenTelemetry/.publicApi/net461/PublicAPI.Unshipped.txt +++ b/src/OpenTelemetry/.publicApi/net461/PublicAPI.Unshipped.txt @@ -1,3 +1,8 @@ OpenTelemetry.Trace.ParentBasedSampler.ParentBasedSampler(OpenTelemetry.Trace.Sampler rootSampler, OpenTelemetry.Trace.Sampler remoteParentSampled = null, OpenTelemetry.Trace.Sampler remoteParentNotSampled = null, OpenTelemetry.Trace.Sampler localParentSampled = null, OpenTelemetry.Trace.Sampler localParentNotSampled = null) -> void +OpenTelemetry.Trace.TracerProviderOptions +OpenTelemetry.Trace.TracerProviderOptions.SetErrorStatusOnException.get -> bool +OpenTelemetry.Trace.TracerProviderOptions.SetErrorStatusOnException.set -> void +OpenTelemetry.Trace.TracerProviderOptions.TracerProviderOptions() -> void +static OpenTelemetry.Sdk.CreateTracerProviderBuilder(System.Action configure = null) -> OpenTelemetry.Trace.TracerProviderBuilder static OpenTelemetry.Trace.TracerProviderBuilderExtensions.AddLegacyActivity(this OpenTelemetry.Trace.TracerProviderBuilder tracerProviderBuilder, string operationName) -> OpenTelemetry.Trace.TracerProviderBuilder static OpenTelemetry.Trace.TracerProviderExtensions.ForceFlush(this OpenTelemetry.Trace.TracerProvider provider, int timeoutMilliseconds = -1) -> bool diff --git a/src/OpenTelemetry/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt b/src/OpenTelemetry/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt index f0dee03ad..3b7ac888e 100644 --- a/src/OpenTelemetry/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt +++ b/src/OpenTelemetry/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt @@ -1,3 +1,8 @@ OpenTelemetry.Trace.ParentBasedSampler.ParentBasedSampler(OpenTelemetry.Trace.Sampler rootSampler, OpenTelemetry.Trace.Sampler remoteParentSampled = null, OpenTelemetry.Trace.Sampler remoteParentNotSampled = null, OpenTelemetry.Trace.Sampler localParentSampled = null, OpenTelemetry.Trace.Sampler localParentNotSampled = null) -> void +OpenTelemetry.Trace.TracerProviderOptions +OpenTelemetry.Trace.TracerProviderOptions.SetErrorStatusOnException.get -> bool +OpenTelemetry.Trace.TracerProviderOptions.SetErrorStatusOnException.set -> void +OpenTelemetry.Trace.TracerProviderOptions.TracerProviderOptions() -> void +static OpenTelemetry.Sdk.CreateTracerProviderBuilder(System.Action configure = null) -> OpenTelemetry.Trace.TracerProviderBuilder static OpenTelemetry.Trace.TracerProviderBuilderExtensions.AddLegacyActivity(this OpenTelemetry.Trace.TracerProviderBuilder tracerProviderBuilder, string operationName) -> OpenTelemetry.Trace.TracerProviderBuilder static OpenTelemetry.Trace.TracerProviderExtensions.ForceFlush(this OpenTelemetry.Trace.TracerProvider provider, int timeoutMilliseconds = -1) -> bool diff --git a/src/OpenTelemetry/CHANGELOG.md b/src/OpenTelemetry/CHANGELOG.md index 74af3d738..4fe5e34da 100644 --- a/src/OpenTelemetry/CHANGELOG.md +++ b/src/OpenTelemetry/CHANGELOG.md @@ -9,14 +9,15 @@ please check the latest changes ## Unreleased -* Added `ForceFlush` to `TracerProvider`. ([#1837](https://github.com/open-telemetry/opentelemetry-dotnet/pull/1837)) - +* Added `TracerProviderOptions` and `SetErrorStatusOnException`. + ([#1858](https://github.com/open-telemetry/opentelemetry-dotnet/pull/1858)) +* Added `ForceFlush` to `TracerProvider`. + ([#1837](https://github.com/open-telemetry/opentelemetry-dotnet/pull/1837)) * Added a TracerProvierBuilder extension method called `AddLegacyActivityOperationName` which is used by instrumentation libraries that use DiagnosticSource to get activities processed without ActivitySourceAdapter. - [#1836](https://github.com/open-telemetry/opentelemetry-dotnet/pull/1836) - + ([#1836](https://github.com/open-telemetry/opentelemetry-dotnet/pull/1836)) * Added new constructor with optional parameters to allow customization of `ParentBasedSampler` behavior. ([#1727](https://github.com/open-telemetry/opentelemetry-dotnet/pull/1727)) diff --git a/src/OpenTelemetry/Sdk.cs b/src/OpenTelemetry/Sdk.cs index 276650df2..ea1899e21 100644 --- a/src/OpenTelemetry/Sdk.cs +++ b/src/OpenTelemetry/Sdk.cs @@ -14,6 +14,7 @@ // limitations under the License. // +using System; using System.Diagnostics; using OpenTelemetry.Context.Propagation; using OpenTelemetry.Internal; @@ -54,13 +55,24 @@ namespace OpenTelemetry } /// - /// Creates TracerProviderBuilder which should be used to build - /// TracerProvider. + /// Creates TracerProviderBuilder which should be used to build TracerProvider. /// /// TracerProviderBuilder instance, which should be used to build TracerProvider. public static TracerProviderBuilder CreateTracerProviderBuilder() { - return new TracerProviderBuilderSdk(); + return CreateTracerProviderBuilder(null); + } + + /// + /// Creates TracerProviderBuilder which should be used to build TracerProvider. + /// + /// TracerProvider configuration options. + /// TracerProviderBuilder instance, which should be used to build TracerProvider. + public static TracerProviderBuilder CreateTracerProviderBuilder(Action configure = null) + { + var options = new TracerProviderOptions(); + configure?.Invoke(options); + return new TracerProviderBuilderSdk(options); } } } diff --git a/src/OpenTelemetry/Trace/ExceptionProcessor.cs b/src/OpenTelemetry/Trace/ExceptionProcessor.cs new file mode 100644 index 000000000..16ebcfa14 --- /dev/null +++ b/src/OpenTelemetry/Trace/ExceptionProcessor.cs @@ -0,0 +1,80 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using System.Diagnostics; +using System.Linq.Expressions; +using System.Reflection; +using System.Runtime.InteropServices; + +namespace OpenTelemetry.Trace +{ + internal class ExceptionProcessor : BaseProcessor + { + private const string ExceptionPointersKey = "otel.exception_pointers"; + + private readonly Func fnGetExceptionPointers; + + public ExceptionProcessor() + { + try + { + var flags = BindingFlags.Static | BindingFlags.Public; + var method = typeof(Marshal).GetMethod("GetExceptionPointers", flags, null, new Type[] { }, null); + var lambda = Expression.Lambda>(Expression.Call(method)); + this.fnGetExceptionPointers = lambda.Compile(); + } + catch (Exception ex) + { + throw new NotSupportedException("System.Runtime.InteropServices.Marshal.GetExceptionPointers is not supported.", ex); + } + } + + /// + public override void OnStart(Activity activity) + { + var pointers = this.fnGetExceptionPointers(); + + if (pointers != IntPtr.Zero) + { + activity.SetTag(ExceptionPointersKey, pointers); + } + } + + /// + public override void OnEnd(Activity activity) + { + var pointers = this.fnGetExceptionPointers(); + + if (pointers == IntPtr.Zero) + { + return; + } + + var snapshot = activity.GetTagValue(ExceptionPointersKey) as IntPtr?; + + if (snapshot != null) + { + activity.SetTag(ExceptionPointersKey, null); + } + + if (snapshot != pointers) + { + activity.SetStatus(Status.Error); + } + } + } +} diff --git a/src/OpenTelemetry/Trace/TracerProviderBuilderSdk.cs b/src/OpenTelemetry/Trace/TracerProviderBuilderSdk.cs index cfa21e5d8..6f0585342 100644 --- a/src/OpenTelemetry/Trace/TracerProviderBuilderSdk.cs +++ b/src/OpenTelemetry/Trace/TracerProviderBuilderSdk.cs @@ -28,14 +28,28 @@ namespace OpenTelemetry.Trace { private readonly List instrumentationFactories = new List(); + private readonly TracerProviderOptions options; private readonly List> processors = new List>(); private readonly List sources = new List(); private readonly Dictionary legacyActivityOperationNames = new Dictionary(StringComparer.OrdinalIgnoreCase); private ResourceBuilder resourceBuilder = ResourceBuilder.CreateDefault(); private Sampler sampler = new ParentBasedSampler(new AlwaysOnSampler()); - internal TracerProviderBuilderSdk() + internal TracerProviderBuilderSdk(TracerProviderOptions options) { + this.options = options ?? new TracerProviderOptions(); + + if (options.SetErrorStatusOnException) + { + try + { + this.AddProcessor(new ExceptionProcessor()); + } + catch (Exception ex) + { + throw new NotSupportedException($"{nameof(options.SetErrorStatusOnException)} is not supported on this platform.", ex); + } + } } /// diff --git a/src/OpenTelemetry/Trace/TracerProviderOptions.cs b/src/OpenTelemetry/Trace/TracerProviderOptions.cs new file mode 100644 index 000000000..7ec4b852b --- /dev/null +++ b/src/OpenTelemetry/Trace/TracerProviderOptions.cs @@ -0,0 +1,27 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace OpenTelemetry.Trace +{ + public class TracerProviderOptions + { + /// + /// Gets or sets a value indicating whether the status of + /// should be set to Status.Error when it ended abnormally due to an unhandled exception. + /// + public bool SetErrorStatusOnException { get; set; } = false; + } +} diff --git a/test/OpenTelemetry.Tests/Trace/ExceptionProcessorTest.cs b/test/OpenTelemetry.Tests/Trace/ExceptionProcessorTest.cs new file mode 100644 index 000000000..97e4eeaec --- /dev/null +++ b/test/OpenTelemetry.Tests/Trace/ExceptionProcessorTest.cs @@ -0,0 +1,120 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using System.Diagnostics; +using OpenTelemetry.Tests; +using Xunit; + +namespace OpenTelemetry.Trace.Tests +{ + public class ExceptionProcessorTest + { + private const string ActivitySourceName = "ExceptionProcessorTest"; + + [Fact] + public void ActivityStatusSetToErrorWhenExceptionProcessorEnabled() + { + using var activitySource = new ActivitySource(ActivitySourceName); + using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddSource(ActivitySourceName) + .SetSampler(new AlwaysOnSampler()) + .AddProcessor(new ExceptionProcessor()) + .Build(); + + Activity activity1 = null; + Activity activity2 = null; + Activity activity3 = null; + Activity activity4 = null; + Activity activity5 = null; + + try + { + using (activity1 = activitySource.StartActivity("Activity1")) + { + using (activity2 = activitySource.StartActivity("Activity2")) + { + throw new Exception("Oops!"); + } + } + } + catch (Exception) + { + using (activity3 = activitySource.StartActivity("Activity3")) + { + } + } + finally + { + using (activity4 = activitySource.StartActivity("Activity4")) + { + } + } + + try + { + throw new Exception("Oops!"); + } + catch (Exception) + { + activity5 = activitySource.StartActivity("Activity5"); + } + finally + { + activity5.Dispose(); + } + + Assert.Equal(StatusCode.Error, activity1.GetStatus().StatusCode); + Assert.Null(activity1.GetTagValue("otel.exception_pointers")); + Assert.Equal(StatusCode.Error, activity2.GetStatus().StatusCode); + Assert.Null(activity2.GetTagValue("otel.exception_pointers")); + Assert.Equal(StatusCode.Unset, activity3.GetStatus().StatusCode); + Assert.Null(activity3.GetTagValue("otel.exception_pointers")); + Assert.Equal(StatusCode.Unset, activity4.GetStatus().StatusCode); + Assert.Null(activity4.GetTagValue("otel.exception_pointers")); + Assert.Equal(StatusCode.Unset, activity5.GetStatus().StatusCode); +#if !NETFRAMEWORK + // In this rare case, the following Activity tag will not get cleaned up due to perf consideration. + Assert.NotNull(activity5.GetTagValue("otel.exception_pointers")); +#endif + } + + [Fact] + public void ActivityStatusNotSetWhenExceptionProcessorNotEnabled() + { + using var activitySource = new ActivitySource(ActivitySourceName); + using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddSource(ActivitySourceName) + .SetSampler(new AlwaysOnSampler()) + .Build(); + + Activity activity = null; + + try + { + using (activity = activitySource.StartActivity("Activity")) + { + throw new Exception("Oops!"); + } + } + catch (Exception) + { + } + + Assert.Equal(StatusCode.Unset, activity.GetStatus().StatusCode); + } + } +} diff --git a/test/OpenTelemetry.Tests/Trace/TracerProviderOptionsTest.cs b/test/OpenTelemetry.Tests/Trace/TracerProviderOptionsTest.cs new file mode 100644 index 000000000..7e4b456d9 --- /dev/null +++ b/test/OpenTelemetry.Tests/Trace/TracerProviderOptionsTest.cs @@ -0,0 +1,81 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using System.Diagnostics; +using OpenTelemetry.Tests; +using Xunit; + +namespace OpenTelemetry.Trace.Tests +{ + public class TracerProviderOptionsTest + { + private const string ActivitySourceName = "TracerProviderOptionsTest"; + + [Fact] + public void SetErrorStatusOnExceptionEnabled() + { + using var activitySource = new ActivitySource(ActivitySourceName); + using var tracerProvider = Sdk.CreateTracerProviderBuilder(options => + { + options.SetErrorStatusOnException = true; + }) + .AddSource(ActivitySourceName) + .SetSampler(new AlwaysOnSampler()) + .Build(); + + var activity = activitySource.StartActivity("Activity"); + + try + { + using (activity) + { + throw new Exception("Oops!"); + } + } + catch (Exception) + { + } + + Assert.Equal(StatusCode.Error, activity.GetStatus().StatusCode); + } + + [Fact] + public void SetErrorStatusOnExceptionDefault() + { + using var activitySource = new ActivitySource(ActivitySourceName); + using var tracerProvider = Sdk.CreateTracerProviderBuilder(options => { }) + .AddSource(ActivitySourceName) + .SetSampler(new AlwaysOnSampler()) + .Build(); + + var activity = activitySource.StartActivity("Activity"); + + try + { + using (activity) + { + throw new Exception("Oops!"); + } + } + catch (Exception) + { + } + + Assert.Equal(StatusCode.Unset, activity.GetStatus().StatusCode); + } + } +}