From 2d8204716abe1790b8c657f68802a5b5aa1fb23c Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Wed, 28 Apr 2021 14:05:56 +1000 Subject: [PATCH] Implement Error exception cause This commit implements the cause property on the exception of an APM error. Cause can capture chained exceptions, which are the chain of inner exceptions for .NET. Closes #1267 --- src/Elastic.Apm/Api/CapturedException.cs | 25 +++++++++++++ .../Model/ExecutionSegmentCommon.cs | 29 +++++++++++++-- test/Elastic.Apm.Tests/ErrorTests.cs | 35 +++++++++++++++++++ 3 files changed, 86 insertions(+), 3 deletions(-) diff --git a/src/Elastic.Apm/Api/CapturedException.cs b/src/Elastic.Apm/Api/CapturedException.cs index a3422db0a..e4cf5080a 100644 --- a/src/Elastic.Apm/Api/CapturedException.cs +++ b/src/Elastic.Apm/Api/CapturedException.cs @@ -9,18 +9,43 @@ namespace Elastic.Apm.Api { + /// + /// Information about the original error + /// public class CapturedException { + /// + /// A collection of error exceptions representing chained exceptions. + /// The chain starts with the outermost exception, followed by its cause, and so on. + /// + public List Cause { get; set; } + + /// + /// Code that is set when the error happened, e.g. database error code. + /// [MaxLength] public string Code { get; set; } + /// + /// Indicates whether the error was caught in the code or not. + /// + // TODO: makes this nullable in 2.x public bool Handled { get; set; } + /// + /// The originally captured error message. + /// public string Message { get; set; } + /// + /// Stacktrace information of the captured exception. + /// [JsonProperty("stacktrace")] public List StackTrace { get; set; } + /// + /// The type of the exception + /// [MaxLength] public string Type { get; set; } diff --git a/src/Elastic.Apm/Model/ExecutionSegmentCommon.cs b/src/Elastic.Apm/Model/ExecutionSegmentCommon.cs index 6cf1f2634..2951c0500 100644 --- a/src/Elastic.Apm/Model/ExecutionSegmentCommon.cs +++ b/src/Elastic.Apm/Model/ExecutionSegmentCommon.cs @@ -166,11 +166,34 @@ public static void CaptureException( executionSegment.Outcome = Outcome.Failure; var capturedCulprit = string.IsNullOrEmpty(culprit) ? GetCulprit(exception, configurationReader) : culprit; + var debugMessage = $"{nameof(ExecutionSegmentCommon)}.{nameof(CaptureException)}"; - var capturedException = new CapturedException { Message = exception.Message, Type = exception.GetType().FullName, Handled = isHandled }; + var capturedException = new CapturedException + { + Message = exception.Message, + Type = exception.GetType().FullName, + Handled = isHandled, + StackTrace = StacktraceHelper.GenerateApmStackTrace(exception, logger, + debugMessage, configurationReader, apmServerInfo) + }; + + var innerException = exception.InnerException; + if (innerException != null) + { + capturedException.Cause = new List(); + while (innerException != null) + { + capturedException.Cause.Add(new CapturedException + { + Message = innerException.Message, + Type = innerException.GetType().FullName, + StackTrace = StacktraceHelper.GenerateApmStackTrace(innerException, logger, + debugMessage, configurationReader, apmServerInfo) + }); - capturedException.StackTrace = StacktraceHelper.GenerateApmStackTrace(exception, logger, - $"{nameof(Transaction)}.{nameof(CaptureException)}", configurationReader, apmServerInfo); + innerException = innerException.InnerException; + } + } payloadSender.QueueError(new Error(capturedException, transaction, parentId ?? executionSegment?.Id, logger, labels) { diff --git a/test/Elastic.Apm.Tests/ErrorTests.cs b/test/Elastic.Apm.Tests/ErrorTests.cs index c11df928a..487305b1f 100644 --- a/test/Elastic.Apm.Tests/ErrorTests.cs +++ b/test/Elastic.Apm.Tests/ErrorTests.cs @@ -3,6 +3,7 @@ // Elasticsearch B.V licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information +using System; using System.Collections.Generic; using System.Diagnostics; using Elastic.Apm.Api; @@ -128,5 +129,39 @@ public void ErrorOnTransactionWithEmptyHeaders() mockPayloadSender.FirstError.Context.Response.Headers.Should().BeNull(); mockPayloadSender.FirstError.Context.InternalLabels.Value.InnerDictionary.Should().BeEmpty(); } + + [Fact] + public void Includes_Cause_When_Exception_Has_InnerException() + { + var mockPayloadSender = new MockPayloadSender(); + using var agent = new ApmAgent(new TestAgentComponents(payloadSender: mockPayloadSender)); + + agent.Tracer.CaptureTransaction("Test", "Test", t => + { + var exception = new Exception( + "Outer exception", + new Exception("Inner exception", new Exception("Inner inner exception"))); + + t.CaptureException(exception); + }); + + mockPayloadSender.WaitForErrors(); + var error = mockPayloadSender.FirstError; + + error.Should().NotBeNull(); + var capturedException = error.Exception; + + capturedException.Should().NotBeNull(); + capturedException.Message.Should().Be("Outer exception"); + capturedException.Cause.Should().NotBeNull().And.HaveCount(2); + + var firstCause = capturedException.Cause[0]; + firstCause.Message.Should().Be("Inner exception"); + firstCause.Type.Should().Be("System.Exception"); + + var secondCause = capturedException.Cause[1]; + secondCause.Message.Should().Be("Inner inner exception"); + secondCause.Type.Should().Be("System.Exception"); + } } }