diff --git a/README.md b/README.md index 986828c3..1dcba722 100644 --- a/README.md +++ b/README.md @@ -147,6 +147,48 @@ Add the `DbUpdateExceptionDestructurer` during setup: .WithDestructurers(new[] { new DbUpdateExceptionDestructurer() })) ``` +### Serilog.Exceptions.Refit + +[![Serilog.Exceptions.Refit NuGet Package](https://img.shields.io/nuget/v/Serilog.Exceptions.Refit.svg)](https://www.nuget.org/packages/Serilog.Exceptions.Refit/) +[![Serilog.Exceptions.Refit package in serilog-exceptions feed in Azure Artifacts](https://feeds.dev.azure.com/serilog-exceptions/_apis/public/Packaging/Feeds/8479813c-da6b-4677-b40d-78df8725dc9c/Packages/dce98084-312a-4939-b879-07bc25734572/Badge)](https://dev.azure.com/serilog-exceptions/Serilog.Exceptions/_packaging?_a=package&feed=8479813c-da6b-4677-b40d-78df8725dc9c&package=dce98084-312a-4939-b879-07bc25734572&preferRelease=true) [![Serilog.Exceptions.Refit NuGet Package Downloads](https://img.shields.io/nuget/dt/Serilog.Exceptions.Refit)](https://www.nuget.org/packages/Serilog.Exceptions.Refit) + +Add the [Serilog.Exceptions.Refit](https://www.nuget.org/packages/Serilog.Exceptions.Refit/) NuGet package to your project to provide detailed logging for the `ApiException` when using [Refit](https://www.nuget.org/packages/Refit/): + +``` +Install-Package Serilog.Exceptions.Refit +``` + +Add the `ApiExceptionDestructurer` during setup: +```csharp +.Enrich.WithExceptionDetails(new DestructuringOptionsBuilder() + .WithDefaultDestructurers() + .WithDestructurers(new[] { new ApiExceptionDestructurer() })) +``` + +Depending on your Serilog setup, common `System.Exception` properties may already be logged. To omit the logging of these properties, use the overloaded +constructor as follows: + +```csharp +.Enrich.WithExceptionDetails(new DestructuringOptionsBuilder() + .WithDefaultDestructurers() + .WithDestructurers(new[] { new ApiExceptionDestructurer(destructureCommonExceptionProperties: false) })) +``` + +The default configuration logs the following properties of an `ApiException`: + +- `Uri` +- `StatusCode` + +In addition, the `ApiException.Content` property can be logged with the following setup: + +```csharp +.Enrich.WithExceptionDetails(new DestructuringOptionsBuilder() + .WithDefaultDestructurers() + .WithDestructurers(new[] { new ApiExceptionDestructurer(destructureHttpContent: true) })) +``` + +Be careful with this option as the HTTP body could be very large and/or contain sensitive information. + ## Custom Exception Destructurers You may want to add support for destructuring your own exceptions without relying on reflection. To do this, create your own destructuring class implementing `ExceptionDestructurer` (You can take a look at [this](https://github.com/RehanSaeed/Serilog.Exceptions/blob/main/Source/Serilog.Exceptions/Destructurers/ArgumentExceptionDestructurer.cs) for `ArgumentException`), then simply add it like so: diff --git a/Serilog.Exceptions.sln b/Serilog.Exceptions.sln index 83d1537e..c6d4b515 100644 --- a/Serilog.Exceptions.sln +++ b/Serilog.Exceptions.sln @@ -90,6 +90,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{ EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Serilog.Exceptions.MsSqlServer", "Source\Serilog.Exceptions.MsSqlServer\Serilog.Exceptions.MsSqlServer.csproj", "{0A21D2AD-024B-4F3D-95F4-BAEFEEE95945}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Serilog.Exceptions.Refit", "Source\Serilog.Exceptions.Refit\Serilog.Exceptions.Refit.csproj", "{0EABF22F-F070-4F8D-B165-DD4C4AB62820}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -124,6 +126,10 @@ Global {0A21D2AD-024B-4F3D-95F4-BAEFEEE95945}.Debug|Any CPU.Build.0 = Debug|Any CPU {0A21D2AD-024B-4F3D-95F4-BAEFEEE95945}.Release|Any CPU.ActiveCfg = Release|Any CPU {0A21D2AD-024B-4F3D-95F4-BAEFEEE95945}.Release|Any CPU.Build.0 = Release|Any CPU + {0EABF22F-F070-4F8D-B165-DD4C4AB62820}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0EABF22F-F070-4F8D-B165-DD4C4AB62820}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0EABF22F-F070-4F8D-B165-DD4C4AB62820}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0EABF22F-F070-4F8D-B165-DD4C4AB62820}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -140,6 +146,7 @@ Global {2C245036-D7F6-4F7C-9BB6-5AFBCCE480F7} = {76FBEEA2-0F88-487E-99C3-5D865CBE79B6} {4F089B23-3121-4935-B24E-7A9A497BD9FE} = {2C245036-D7F6-4F7C-9BB6-5AFBCCE480F7} {0A21D2AD-024B-4F3D-95F4-BAEFEEE95945} = {C5508012-7216-4ABE-AB2F-B166ED5FF94F} + {0EABF22F-F070-4F8D-B165-DD4C4AB62820} = {C5508012-7216-4ABE-AB2F-B166ED5FF94F} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {BE74AFAC-AC6F-4B80-860F-15C22BEE1A38} diff --git a/Source/Serilog.Exceptions.Refit/Destructurers/ApiExceptionDestructurer.cs b/Source/Serilog.Exceptions.Refit/Destructurers/ApiExceptionDestructurer.cs new file mode 100644 index 00000000..6d8b718d --- /dev/null +++ b/Source/Serilog.Exceptions.Refit/Destructurers/ApiExceptionDestructurer.cs @@ -0,0 +1,71 @@ +namespace Serilog.Exceptions.Refit.Destructurers +{ + using System; + using System.Collections.Generic; + using global::Refit; + using Serilog.Exceptions.Core; + using Serilog.Exceptions.Destructurers; + + /// + /// A destructurer for the Refit . + /// + /// + public class ApiExceptionDestructurer : ExceptionDestructurer + { + private readonly bool destructureCommonExceptionProperties; + private readonly bool destructureHttpContent; + + /// + /// Initializes a new instance of the class. + /// + /// Destructure common public Exception properties or not. + /// Destructure the HTTP body. This is left optional due to possible security and log size concerns. + public ApiExceptionDestructurer(bool destructureCommonExceptionProperties = true, bool destructureHttpContent = false) + { + this.destructureCommonExceptionProperties = destructureCommonExceptionProperties; + this.destructureHttpContent = destructureHttpContent; + } + + /// + public override Type[] TargetTypes => new[] { typeof(ApiException) }; + + /// + public override void Destructure(Exception exception, IExceptionPropertiesBag propertiesBag, Func?> destructureException) + { + if (this.destructureCommonExceptionProperties) + { + base.Destructure(exception, propertiesBag, destructureException); + } + else + { + // Argument checks are usually done in + // but as we didn't call this method we need to do the checks here. + if (exception is null) + { + throw new ArgumentNullException(nameof(propertiesBag)); + } + + if (propertiesBag is null) + { + throw new ArgumentNullException(nameof(propertiesBag)); + } + + if (destructureException is null) + { + throw new ArgumentNullException(nameof(destructureException)); + } + } + +#pragma warning disable CA1062 // Validate arguments of public methods + var apiException = (ApiException)exception; + if (this.destructureHttpContent) + { + propertiesBag.AddProperty(nameof(ApiException.Content), apiException.Content); + } + + propertiesBag.AddProperty(nameof(ApiException.Uri), apiException.Uri); + propertiesBag.AddProperty(nameof(ApiException.StatusCode), apiException.StatusCode); +#pragma warning restore CA1062 // Validate arguments of public methods + } + } +} diff --git a/Source/Serilog.Exceptions.Refit/Properties/AssemblyInfo.cs b/Source/Serilog.Exceptions.Refit/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..0270020d --- /dev/null +++ b/Source/Serilog.Exceptions.Refit/Properties/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using System; + +[assembly: CLSCompliant(true)] diff --git a/Source/Serilog.Exceptions.Refit/Serilog.Exceptions.Refit.csproj b/Source/Serilog.Exceptions.Refit/Serilog.Exceptions.Refit.csproj new file mode 100644 index 00000000..cd336190 --- /dev/null +++ b/Source/Serilog.Exceptions.Refit/Serilog.Exceptions.Refit.csproj @@ -0,0 +1,21 @@ + + + + net5.0;netstandard2.0 + + + + Serilog Exceptions Refit + Log exception details and custom properties that are not output in Exception.ToString(). Contains custom destructurers for Refit exceptions. + Serilog;Exception;Log;Logging;Detail;Details;Refit + + + + + + + + + + + diff --git a/Tests/Serilog.Exceptions.Test/Destructurers/ApiExceptionDestructurerTest.cs b/Tests/Serilog.Exceptions.Test/Destructurers/ApiExceptionDestructurerTest.cs new file mode 100644 index 00000000..4c374af8 --- /dev/null +++ b/Tests/Serilog.Exceptions.Test/Destructurers/ApiExceptionDestructurerTest.cs @@ -0,0 +1,100 @@ +namespace Serilog.Exceptions.Test.Destructurers +{ + using System; + using System.Net; + using System.Net.Http; + using System.Net.Http.Json; + using System.Threading.Tasks; + using global::Refit; + using Serilog.Exceptions.Core; + using Serilog.Exceptions.Refit.Destructurers; + using Xunit; + using static LogJsonOutputUtils; + + public class ApiExceptionDestructurerTest + { + [Fact] + public async Task ApiException_HttpStatusCodeIsLoggedAsPropertyAsync() + { + using var message = new HttpRequestMessage(HttpMethod.Get, new Uri("https://foobar.com")); + using var response = new HttpResponseMessage(HttpStatusCode.InternalServerError); + var options = new DestructuringOptionsBuilder().WithDestructurers(new[] { new ApiExceptionDestructurer() }); + var apiException = await ApiException.Create(message, HttpMethod.Get, response, new RefitSettings()).ConfigureAwait(false); + + Test_LoggedExceptionContainsProperty(apiException, nameof(ApiException.StatusCode), nameof(HttpStatusCode.InternalServerError), options); + } + + [Fact] + public async Task ApiException_UriIsLoggedAsPropertyAsync() + { + var requestUri = new Uri("https://foobar.com"); + using var message = new HttpRequestMessage(HttpMethod.Get, requestUri); + using var response = new HttpResponseMessage(HttpStatusCode.InternalServerError); + var options = new DestructuringOptionsBuilder().WithDestructurers(new[] { new ApiExceptionDestructurer() }); + var apiException = await ApiException.Create(message, HttpMethod.Get, response, new RefitSettings()).ConfigureAwait(false); + + Test_LoggedExceptionContainsProperty(apiException, nameof(ApiException.Uri), requestUri.ToString(), options); + } + + [Fact] + public async Task ApiException_ByDefaultContentIsNotLoggedAsPropertyAsync() + { + var requestUri = new Uri("https://foobar.com"); + using var message = new HttpRequestMessage(HttpMethod.Get, requestUri); + using var response = new HttpResponseMessage(HttpStatusCode.InternalServerError); + var options = new DestructuringOptionsBuilder().WithDestructurers(new[] { new ApiExceptionDestructurer() }); + response.Content = JsonContent.Create("hello"); + + var apiException = await ApiException.Create(message, HttpMethod.Get, response, new RefitSettings()).ConfigureAwait(false); + + Test_LoggedExceptionDoesNotContainProperty(apiException, nameof(ApiException.Content), options); + } + + [Fact] + public async Task ApiException_WhenSpecifiedContentIsLoggedAsPropertyAsync() + { + var requestUri = new Uri("https://foobar.com"); + using var message = new HttpRequestMessage(HttpMethod.Get, requestUri); + using var response = new HttpResponseMessage(HttpStatusCode.InternalServerError); + var options = new DestructuringOptionsBuilder().WithDestructurers(new[] { new ApiExceptionDestructurer(destructureHttpContent: true) }); + response.Content = JsonContent.Create("hello"); + + var apiException = await ApiException.Create(message, HttpMethod.Get, response, new RefitSettings()).ConfigureAwait(false); + + Test_LoggedExceptionContainsProperty(apiException, nameof(ApiException.Content), "\"hello\"", options); + } + + [Fact] + public async Task ApiException_ByDefaultCommonPropertiesLoggedAsPropertiesAsync() + { + var requestUri = new Uri("https://foobar.com"); + using var message = new HttpRequestMessage(HttpMethod.Get, requestUri); + using var response = new HttpResponseMessage(HttpStatusCode.InternalServerError); + var options = new DestructuringOptionsBuilder().WithDestructurers(new[] { new ApiExceptionDestructurer() }); + var apiException = await ApiException.Create(message, HttpMethod.Get, response, new RefitSettings()).ConfigureAwait(false); + + // No need to test all properties, just a handful is sufficient + Test_LoggedExceptionContainsProperty(apiException, nameof(Exception.StackTrace), apiException.StackTrace, options); + Test_LoggedExceptionContainsProperty(apiException, nameof(Exception.Message), apiException.Message, options); + Test_LoggedExceptionContainsProperty(apiException, nameof(Type), apiException.GetType().ToString(), options); + } + + [Fact] + public async Task ApiException_WhenSpecifiedCommonPropertiesNotLoggedAsPropertiesAsync() + { + var requestUri = new Uri("https://foobar.com"); + using var message = new HttpRequestMessage(HttpMethod.Get, requestUri); + using var response = new HttpResponseMessage(HttpStatusCode.InternalServerError); + var options = new DestructuringOptionsBuilder().WithDestructurers(new[] { new ApiExceptionDestructurer(destructureCommonExceptionProperties: false) }); + var apiException = await ApiException.Create(message, HttpMethod.Get, response, new RefitSettings()).ConfigureAwait(false); + + Test_LoggedExceptionDoesNotContainProperty(apiException, nameof(Exception.StackTrace), options); + Test_LoggedExceptionDoesNotContainProperty(apiException, nameof(Exception.Message), options); + Test_LoggedExceptionDoesNotContainProperty(apiException, nameof(Exception.InnerException), options); + Test_LoggedExceptionDoesNotContainProperty(apiException, nameof(Exception.HelpLink), options); + Test_LoggedExceptionDoesNotContainProperty(apiException, nameof(Exception.Data), options); + Test_LoggedExceptionDoesNotContainProperty(apiException, nameof(Exception.HResult), options); + Test_LoggedExceptionDoesNotContainProperty(apiException, nameof(Type), options); + } + } +} diff --git a/Tests/Serilog.Exceptions.Test/Destructurers/LogJsonOutputUtils.cs b/Tests/Serilog.Exceptions.Test/Destructurers/LogJsonOutputUtils.cs index 172818a2..4cb8de6a 100644 --- a/Tests/Serilog.Exceptions.Test/Destructurers/LogJsonOutputUtils.cs +++ b/Tests/Serilog.Exceptions.Test/Destructurers/LogJsonOutputUtils.cs @@ -36,15 +36,15 @@ public static JObject LogAndDestructureException( return rootObject; } - public static void Test_LoggedExceptionContainsProperty(Exception exception, string propertyKey, string? propertyValue) + public static void Test_LoggedExceptionContainsProperty(Exception exception, string propertyKey, string? propertyValue, IDestructuringOptions? destructuringOptions = null) { - var rootObject = LogAndDestructureException(exception); + var rootObject = LogAndDestructureException(exception, destructuringOptions); Assert_JObjectContainsPropertiesExceptionDetailsWithProperty(rootObject, propertyKey, propertyValue); } - public static void Test_LoggedExceptionDoesNotContainProperty(Exception exception, string propertyKey) + public static void Test_LoggedExceptionDoesNotContainProperty(Exception exception, string propertyKey, IDestructuringOptions? destructuringOptions = null) { - var rootObject = LogAndDestructureException(exception); + var rootObject = LogAndDestructureException(exception, destructuringOptions); Assert_JObjectContainsPropertiesExceptionDetailsWithoutProperty(rootObject, propertyKey); } diff --git a/Tests/Serilog.Exceptions.Test/Serilog.Exceptions.Test.csproj b/Tests/Serilog.Exceptions.Test/Serilog.Exceptions.Test.csproj index 8921f2d9..918a57b1 100644 --- a/Tests/Serilog.Exceptions.Test/Serilog.Exceptions.Test.csproj +++ b/Tests/Serilog.Exceptions.Test/Serilog.Exceptions.Test.csproj @@ -11,6 +11,7 @@ +