From 1b6c39d879cbde4ffe46163572de46b19ade08fa Mon Sep 17 00:00:00 2001 From: Eddy Nakamura Date: Fri, 21 Aug 2020 11:07:44 -0300 Subject: [PATCH] Implementation of RecordException in TelemetrySpan (#1116) * Add RecordException method to TelemetrySpan * reusing ToInvariantString already implemented * updating changelog * checking for null * reusing method * Adding activity extension to record exception * adding aggressiveinlining Co-authored-by: Cijo Thomas --- src/OpenTelemetry.Api/CHANGELOG.md | 2 + .../Trace/ActivityExtensions.cs | 28 ++++++++ .../Trace/SemanticConventions.cs | 8 ++- src/OpenTelemetry.Api/Trace/TelemetrySpan.cs | 51 +++++++++++++ .../Trace/ActivityExtensionsTest.cs | 15 ++++ .../Trace/TelemetrySpanTest.cs | 72 +++++++++++++++++++ 6 files changed, 175 insertions(+), 1 deletion(-) create mode 100644 test/OpenTelemetry.Tests/Trace/TelemetrySpanTest.cs diff --git a/src/OpenTelemetry.Api/CHANGELOG.md b/src/OpenTelemetry.Api/CHANGELOG.md index 1d9374526..2a2508082 100644 --- a/src/OpenTelemetry.Api/CHANGELOG.md +++ b/src/OpenTelemetry.Api/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +* Added `RecordException` in `TelemetrySpan` + ([#1116](https://github.com/open-telemetry/opentelemetry-dotnet/pull/1116)) * `PropagationContext` is now used instead of `ActivityContext` in the `ITextFormat` API ([#1048](https://github.com/open-telemetry/opentelemetry-dotnet/pull/1048)) diff --git a/src/OpenTelemetry.Api/Trace/ActivityExtensions.cs b/src/OpenTelemetry.Api/Trace/ActivityExtensions.cs index 70e1a1ed4..0e9a82512 100644 --- a/src/OpenTelemetry.Api/Trace/ActivityExtensions.cs +++ b/src/OpenTelemetry.Api/Trace/ActivityExtensions.cs @@ -19,6 +19,7 @@ using System.Diagnostics; using System.Linq; using System.Linq.Expressions; using System.Runtime.CompilerServices; +using OpenTelemetry.Internal; namespace OpenTelemetry.Trace { @@ -84,6 +85,33 @@ namespace OpenTelemetry.Trace SetKindProperty(activity, kind); } + /// + /// Record Exception. + /// + /// Activity instance. + /// Exception to be recorded. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void RecordException(this Activity activity, Exception ex) + { + if (ex == null) + { + return; + } + + var tagsCollection = new ActivityTagsCollection + { + { SemanticConventions.AttributeExceptionType, ex.GetType().Name }, + { SemanticConventions.AttributeExceptionStacktrace, ex.ToInvariantString() }, + }; + + if (!string.IsNullOrWhiteSpace(ex.Message)) + { + tagsCollection.Add(SemanticConventions.AttributeExceptionMessage, ex.Message); + } + + activity?.AddEvent(new ActivityEvent(SemanticConventions.AttributeExceptionEventName, default, tagsCollection)); + } + #pragma warning disable SA1201 // Elements should appear in the correct order private static readonly Action SetKindProperty = CreateActivityKindSetter(); #pragma warning restore SA1201 // Elements should appear in the correct order diff --git a/src/OpenTelemetry.Api/Trace/SemanticConventions.cs b/src/OpenTelemetry.Api/Trace/SemanticConventions.cs index 75a11adae..b29cdd7dd 100644 --- a/src/OpenTelemetry.Api/Trace/SemanticConventions.cs +++ b/src/OpenTelemetry.Api/Trace/SemanticConventions.cs @@ -23,7 +23,8 @@ namespace OpenTelemetry.Trace internal static class SemanticConventions { // The set of constants matches the specification as of this commit. - // https://github.com/open-telemetry/opentelemetry-specification/tree/709293fe132709705f0e0dd4252992e87a6ec899/specification/trace/semantic_conventions + // https://github.com/open-telemetry/opentelemetry-specification/tree/master/specification/trace/semantic_conventions + // https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/trace/semantic_conventions/exceptions.md #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member public const string AttributeServiceName = "service.name"; public const string AttributeServiceNamespace = "service.namespace"; @@ -147,6 +148,11 @@ namespace OpenTelemetry.Trace public const string AttributeMessagingPayloadSize = "messaging.message_payload_size_bytes"; public const string AttributeMessagingPayloadCompressedSize = "messaging.message_payload_compressed_size_bytes"; public const string AttributeMessagingOperation = "messaging.operation"; + + public const string AttributeExceptionEventName = "exception"; + public const string AttributeExceptionType = "exception.type"; + public const string AttributeExceptionMessage = "exception.message"; + public const string AttributeExceptionStacktrace = "exception.stacktrace"; #pragma warning restore CS1591 // Missing XML comment for publicly visible type or member } } diff --git a/src/OpenTelemetry.Api/Trace/TelemetrySpan.cs b/src/OpenTelemetry.Api/Trace/TelemetrySpan.cs index 11e97a8e2..1d9e1f659 100644 --- a/src/OpenTelemetry.Api/Trace/TelemetrySpan.cs +++ b/src/OpenTelemetry.Api/Trace/TelemetrySpan.cs @@ -18,6 +18,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.Runtime.CompilerServices; +using OpenTelemetry.Internal; namespace OpenTelemetry.Trace { @@ -303,6 +304,56 @@ namespace OpenTelemetry.Trace return this; } + /// + /// Record Exception. + /// + /// Exception to be recorded. + /// The instance for chaining. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public TelemetrySpan RecordException(Exception ex) + { + if (ex == null) + { + return this; + } + + return this.RecordException(ex.GetType().Name, ex.Message, ex.ToInvariantString()); + } + + /// + /// Record Exception. + /// + /// Type of the exception to be recorded. + /// Message of the exception to be recorded. + /// Stacktrace of the exception to be recorded. + /// The instance for chaining. + public TelemetrySpan RecordException(string type, string message, string stacktrace) + { + Dictionary attributes = new Dictionary(); + + if (!string.IsNullOrWhiteSpace(type)) + { + attributes.Add(SemanticConventions.AttributeExceptionType, type); + } + + if (!string.IsNullOrWhiteSpace(stacktrace)) + { + attributes.Add(SemanticConventions.AttributeExceptionStacktrace, stacktrace); + } + + if (!string.IsNullOrWhiteSpace(message)) + { + attributes.Add(SemanticConventions.AttributeExceptionMessage, message); + } + + if (attributes.Count != 0) + { + this.AddEvent(SemanticConventions.AttributeExceptionEventName, attributes); + } + + return this; + } + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Dispose() diff --git a/test/OpenTelemetry.Tests/Trace/ActivityExtensionsTest.cs b/test/OpenTelemetry.Tests/Trace/ActivityExtensionsTest.cs index 1a6a71745..eff405f09 100644 --- a/test/OpenTelemetry.Tests/Trace/ActivityExtensionsTest.cs +++ b/test/OpenTelemetry.Tests/Trace/ActivityExtensionsTest.cs @@ -14,7 +14,9 @@ // limitations under the License. // +using System; using System.Diagnostics; +using System.Linq; using Xunit; namespace OpenTelemetry.Trace.Tests @@ -114,5 +116,18 @@ namespace OpenTelemetry.Trace.Tests Assert.Equal(inputOutput, activity.Kind); } + + [Fact] + public void CheckRecordException() + { + var message = "message"; + var exception = new ArgumentNullException(message, new Exception(message)); + var activity = new Activity("test-activity"); + activity.RecordException(exception); + + var @event = activity.Events.FirstOrDefault(e => e.Name == SemanticConventions.AttributeExceptionEventName); + Assert.Equal(message, @event.Tags.FirstOrDefault(t => t.Key == SemanticConventions.AttributeExceptionMessage).Value); + Assert.Equal(exception.GetType().Name, @event.Tags.FirstOrDefault(t => t.Key == SemanticConventions.AttributeExceptionType).Value); + } } } diff --git a/test/OpenTelemetry.Tests/Trace/TelemetrySpanTest.cs b/test/OpenTelemetry.Tests/Trace/TelemetrySpanTest.cs new file mode 100644 index 000000000..44d2f57e3 --- /dev/null +++ b/test/OpenTelemetry.Tests/Trace/TelemetrySpanTest.cs @@ -0,0 +1,72 @@ +// +// 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; +using OpenTelemetry.Trace; +using Xunit; + +namespace OpenTelemetry.Tests.Trace +{ + public class TelemetrySpanTest + { + [Fact] + public void CheckRecordExceptionData() + { + string message = "message"; + + using Activity activity = new Activity("exception-test"); + using TelemetrySpan telemetrySpan = new TelemetrySpan(activity); + telemetrySpan.RecordException(new ArgumentNullException(message, new Exception("new-exception"))); + Assert.Single(activity.Events); + + var @event = telemetrySpan.Activity.Events.FirstOrDefault(q => q.Name == SemanticConventions.AttributeExceptionEventName); + Assert.Equal(message, @event.Tags.FirstOrDefault(t => t.Key == SemanticConventions.AttributeExceptionMessage).Value); + Assert.Equal(typeof(ArgumentNullException).Name, @event.Tags.FirstOrDefault(t => t.Key == SemanticConventions.AttributeExceptionType).Value); + } + + [Fact] + public void CheckRecordExceptionData2() + { + string type = "ArgumentNullException"; + string message = "message"; + string stack = "stack"; + + using Activity activity = new Activity("exception-test"); + using TelemetrySpan telemetrySpan = new TelemetrySpan(activity); + telemetrySpan.RecordException(type, message, stack); + Assert.Single(activity.Events); + + var @event = telemetrySpan.Activity.Events.FirstOrDefault(q => q.Name == SemanticConventions.AttributeExceptionEventName); + Assert.Equal(message, @event.Tags.FirstOrDefault(t => t.Key == SemanticConventions.AttributeExceptionMessage).Value); + Assert.Equal(type, @event.Tags.FirstOrDefault(t => t.Key == SemanticConventions.AttributeExceptionType).Value); + Assert.Equal(stack, @event.Tags.FirstOrDefault(t => t.Key == SemanticConventions.AttributeExceptionStacktrace).Value); + } + + [Fact] + public void CheckRecordExceptionEmpty() + { + using Activity activity = new Activity("exception-test"); + using TelemetrySpan telemetrySpan = new TelemetrySpan(activity); + telemetrySpan.RecordException(string.Empty, string.Empty, string.Empty); + Assert.Empty(activity.Events); + + telemetrySpan.RecordException(null); + Assert.Empty(activity.Events); + } + } +}