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 @@
+