Skip to content

Add exception to IDiagnosticContext #56

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

Merged
merged 6 commits into from
Feb 27, 2022
Merged
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
Expand Up @@ -56,5 +56,12 @@ public void Set(string propertyName, object value, bool destructureObjects = fal
collector.AddOrUpdate(property);
}
}

/// <inheritdoc cref="IDiagnosticContext.SetException"/>
public void SetException(Exception exception)
{
var collector = AmbientDiagnosticContextCollector.Current;
collector?.SetException(exception);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ public sealed class DiagnosticContextCollector : IDisposable
{
readonly IDisposable _chainedDisposable;
readonly object _propertiesLock = new object();
Exception _exception;
Dictionary<string, LogEventProperty> _properties = new Dictionary<string, LogEventProperty>();

/// <summary>
Expand Down Expand Up @@ -38,19 +39,60 @@ public void AddOrUpdate(LogEventProperty property)
}
}

/// <summary>
/// Set the exception associated with the current diagnostic context.
/// </summary>
/// <example>
/// Passing an exception to the diagnostic context is useful when unhandled exceptions are handled before reaching Serilog's
/// RequestLoggingMiddleware. One example is using https://www.nuget.org/packages/Hellang.Middleware.ProblemDetails to transform
/// exceptions to ProblemDetails responses.
/// </example>
/// <remarks>
/// If an unhandled exception reaches Serilog's RequestLoggingMiddleware, then the unhandled exception takes precedence.<br/>
/// If <c>null</c> is given, it clears any previously assigned exception.
/// </remarks>
/// <param name="exception">The exception to log.</param>
public void SetException(Exception exception)
{
lock (_propertiesLock)
{
if (_properties == null) return;
_exception = exception;
}
}

/// <summary>
/// Complete the context and retrieve the properties added to it, if any. This will
/// stop collection and remove the collector from the original execution context and
/// any of its children.
/// </summary>
/// <param name="properties">The collected properties, or null if no collection is active.</param>
/// <returns>True if properties could be collected.</returns>
/// <seealso cref="IDiagnosticContext.Set"/>
[Obsolete("Replaced by TryComplete(out IEnumerable<LogEventProperty> properties, out Exception exception).")]
public bool TryComplete(out IEnumerable<LogEventProperty> properties)
{
return TryComplete(out properties, out _);
}

/// <summary>
/// Complete the context and retrieve the properties and exception added to it, if any. This will
/// stop collection and remove the collector from the original execution context and
/// any of its children.
/// </summary>
/// <param name="properties">The collected properties, or null if no collection is active.</param>
/// <param name="exception">The collected exception, or null if none has been collected or if no collection is active.</param>
/// <returns>True if properties could be collected.</returns>
/// <seealso cref="IDiagnosticContext.Set"/>
/// <seealso cref="Serilog.IDiagnosticContext.SetException"/>
public bool TryComplete(out IEnumerable<LogEventProperty> properties, out Exception exception)
{
lock (_propertiesLock)
{
properties = _properties?.Values;
exception = _exception;
_properties = null;
_exception = null;
Dispose();
return properties != null;
}
Expand Down
15 changes: 14 additions & 1 deletion src/Serilog.Extensions.Hosting/IDiagnosticContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
// See the License for the specific language governing permissions and
// limitations under the License.

using System;

namespace Serilog
{
/// <summary>
Expand All @@ -27,6 +29,17 @@ public interface IDiagnosticContext
/// <param name="value">The property value.</param>
/// <param name="destructureObjects">If true, the value will be serialized as structured
/// data if possible; if false, the object will be recorded as a scalar or simple array.</param>
void Set(string propertyName, object value, bool destructureObjects = false);
void Set(string propertyName, object value, bool destructureObjects = false);

/// <summary>
/// Set the specified exception on the current diagnostic context.
/// </summary>
/// <remarks>
/// This method is useful when unhandled exceptions do not reach <c>Serilog.AspNetCore.RequestLoggingMiddleware</c>,
/// such as when using <a href="https://www.nuget.org/packages/Hellang.Middleware.ProblemDetails">Hellang.Middleware.ProblemDetails</a>
/// to transform exceptions to ProblemDetails responses.
/// </remarks>
/// <param name="exception">The exception to log. If <c>null</c> is given, it clears any previously assigned exception.</param>
void SetException(Exception exception);
}
}
62 changes: 62 additions & 0 deletions test/Serilog.Extensions.Hosting.Tests/DiagnosticContextTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,13 @@ public void SetIsSafeWhenNoContextIsActive()
dc.Set(Some.String("name"), Some.Int32());
}

[Fact]
public void SetExceptionIsSafeWhenNoContextIsActive()
{
var dc = new DiagnosticContext(Some.Logger());
dc.SetException(new Exception("test"));
}

[Fact]
public async Task PropertiesAreCollectedInAnActiveContext()
{
Expand All @@ -39,6 +46,48 @@ public async Task PropertiesAreCollectedInAnActiveContext()
Assert.False(collector.TryComplete(out _));
}

[Fact]
public void ExceptionIsCollectedInAnActiveContext()
{
var dc = new DiagnosticContext(Some.Logger());
var collector = dc.BeginCollection();

var setException = new Exception("before collect");
dc.SetException(setException);

Assert.True(collector.TryComplete(out _, out var collectedException));
Assert.Same(setException, collectedException);
}

[Fact]
public void ExceptionIsNotCollectedAfterTryComplete()
{
var dc = new DiagnosticContext(Some.Logger());
var collector = dc.BeginCollection();
collector.TryComplete(out _, out _);
dc.SetException(new Exception(Some.String("after collect")));

var tryComplete2 = collector.TryComplete(out _, out var collectedException2);

Assert.False(tryComplete2);
Assert.Null(collectedException2);
}

[Fact]
public void ExceptionIsNotCollectedAfterDispose()
{
var dc = new DiagnosticContext(Some.Logger());
var collector = dc.BeginCollection();
collector.Dispose();

dc.SetException(new Exception("after dispose"));

var tryComplete = collector.TryComplete(out _, out var collectedException);

Assert.True(tryComplete);
Assert.Null(collectedException);
}

[Fact]
public void ExistingPropertiesCanBeUpdated()
{
Expand All @@ -53,5 +102,18 @@ public void ExistingPropertiesCanBeUpdated()
var scalar = Assert.IsType<ScalarValue>(prop.Value);
Assert.Equal(20, scalar.Value);
}

[Fact]
public void ExistingExceptionCanBeUpdated()
{
var dc = new DiagnosticContext(Some.Logger());
var collector = dc.BeginCollection();

dc.SetException(new Exception("ex1"));
dc.SetException(new Exception("ex2"));

Assert.True(collector.TryComplete(out _, out var collectedException));
Assert.Equal("ex2", collectedException.Message);
}
}
}