Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ApiExceptionDestructurer for Refit.ApiException (Refit NuGet) #398

Merged
merged 8 commits into from
Sep 28, 2021
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,33 @@ 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) }))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just asking, shouldn't it be destructureCommonExceptionProperties: false

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Absolutely, well spotted. Now corrected, also added docmentation for the other optional constructor parameter.

```

## 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:
Expand Down
7 changes: 7 additions & 0 deletions Serilog.Exceptions.sln
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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}
Expand Down
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// A destructurer for the Refit <see cref="ApiException"/>.
/// </summary>
/// <seealso cref="ExceptionDestructurer" />
public class ApiExceptionDestructurer : ExceptionDestructurer
{
private readonly bool destructureCommonExceptionProperties = true;
RehanSaeed marked this conversation as resolved.
Show resolved Hide resolved
private readonly bool destructureHttpContent;

/// <summary>
/// Initializes a new instance of the <see cref="ApiExceptionDestructurer"/> class.
/// </summary>
/// <param name="destructureCommonExceptionProperties">Destructure common public Exception properties or not.</param>
/// <param name="destructureHttpContent">Destructure the HTTP body. This is left optional due to possible security and log size concerns.</param>
public ApiExceptionDestructurer(bool destructureCommonExceptionProperties = true, bool destructureHttpContent = false)
{
this.destructureCommonExceptionProperties = destructureCommonExceptionProperties;
this.destructureHttpContent = destructureHttpContent;
}

/// <inheritdoc cref="IExceptionDestructurer.TargetTypes"/>
public override Type[] TargetTypes => new[] { typeof(ApiException) };

/// <inheritdoc />
public override void Destructure(Exception exception, IExceptionPropertiesBag propertiesBag, Func<Exception, IReadOnlyDictionary<string, object?>?> destructureException)
{
if (this.destructureCommonExceptionProperties)
{
base.Destructure(exception, propertiesBag, destructureException);
}
else
{
// Argument checks are usually done in <see cref="ExceptionDestructurer.Destructure"/>
// 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
}
}
}
3 changes: 3 additions & 0 deletions Source/Serilog.Exceptions.Refit/Properties/AssemblyInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
using System;

[assembly: CLSCompliant(true)]
21 changes: 21 additions & 0 deletions Source/Serilog.Exceptions.Refit/Serilog.Exceptions.Refit.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup Label="Build">
<TargetFrameworks>net5.0;netstandard2.0</TargetFrameworks>
</PropertyGroup>

<PropertyGroup Label="Package">
<Product>Serilog Exceptions Refit</Product>
<Description>Log exception details and custom properties that are not output in Exception.ToString(). Contains custom destructurers for Refit exceptions.</Description>
<PackageTags>Serilog;Exception;Log;Logging;Detail;Details;Refit</PackageTags>
</PropertyGroup>

<ItemGroup Label="Project References">
<ProjectReference Include="..\..\Source\Serilog.Exceptions\Serilog.Exceptions.csproj" />
</ItemGroup>

<ItemGroup Label="Package References">
<PackageReference Include="Refit" Version="6.0.94" />
RehanSaeed marked this conversation as resolved.
Show resolved Hide resolved
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

<ItemGroup Label="Project References">
<ProjectReference Include="..\..\Source\Serilog.Exceptions.EntityFrameworkCore\Serilog.Exceptions.EntityFrameworkCore.csproj" />
<ProjectReference Include="..\..\Source\Serilog.Exceptions.Refit\Serilog.Exceptions.Refit.csproj" />
<ProjectReference Include="..\..\Source\Serilog.Exceptions\Serilog.Exceptions.csproj" />
</ItemGroup>

Expand Down