diff --git a/Build.ps1 b/Build.ps1
index 41fbf5d..cce9432 100644
--- a/Build.ps1
+++ b/Build.ps1
@@ -11,7 +11,7 @@ if(Test-Path .\artifacts) {
$branch = @{ $true = $env:APPVEYOR_REPO_BRANCH; $false = $(git symbolic-ref --short -q HEAD) }[$env:APPVEYOR_REPO_BRANCH -ne $NULL];
$revision = @{ $true = "{0:00000}" -f [convert]::ToInt32("0" + $env:APPVEYOR_BUILD_NUMBER, 10); $false = "local" }[$env:APPVEYOR_BUILD_NUMBER -ne $NULL];
-$suffix = @{ $true = ""; $false = "$($branch.Substring(0, [math]::Min(10,$branch.Length)))-$revision"}[$branch -eq "master" -and $revision -ne "local"]
+$suffix = @{ $true = ""; $false = "$($branch.Substring(0, [math]::Min(10,$branch.Length)))-$revision"}[$branch -eq "main" -and $revision -ne "local"]
echo "build: Version suffix is $suffix"
diff --git a/appveyor.yml b/appveyor.yml
index d9cd93c..27bd2ea 100644
--- a/appveyor.yml
+++ b/appveyor.yml
@@ -1,6 +1,6 @@
version: '{build}'
skip_tags: true
-image: Visual Studio 2019
+image: Visual Studio 2022
test: off
build_script:
- ps: ./Build.ps1
@@ -15,11 +15,11 @@ deploy:
api_key:
secure: Q65rY+zaFWOhs8a9IVSaX4Go5HNcIlEXjEFWMB83Y325WE9aXzi0xzDDc0/fJDzk
on:
- branch: /^(master|dev)$/
+ branch: /^(main|dev)$/
- provider: GitHub
auth_token:
secure: p4LpVhBKxGS5WqucHxFQ5c7C8cP74kbNB0Z8k9Oxx/PMaDQ1+ibmoexNqVU5ZlmX
artifact: /Serilog.*\.nupkg/
tag: v$(appveyor_build_version)
on:
- branch: master
+ branch: main
diff --git a/global.json b/global.json
index 14a6148..af5a328 100644
--- a/global.json
+++ b/global.json
@@ -1,7 +1,7 @@
{
"sdk": {
"allowPrerelease": false,
- "version": "5.0.102",
+ "version": "6.0.300",
"rollForward": "latestFeature"
}
}
diff --git a/src/Serilog.Extensions.Hosting/Extensions/Hosting/DiagnosticContext.cs b/src/Serilog.Extensions.Hosting/Extensions/Hosting/DiagnosticContext.cs
index 7c12c19..44462ac 100644
--- a/src/Serilog.Extensions.Hosting/Extensions/Hosting/DiagnosticContext.cs
+++ b/src/Serilog.Extensions.Hosting/Extensions/Hosting/DiagnosticContext.cs
@@ -56,5 +56,12 @@ public void Set(string propertyName, object value, bool destructureObjects = fal
collector.AddOrUpdate(property);
}
}
+
+ ///
+ public void SetException(Exception exception)
+ {
+ var collector = AmbientDiagnosticContextCollector.Current;
+ collector?.SetException(exception);
+ }
}
}
diff --git a/src/Serilog.Extensions.Hosting/Extensions/Hosting/DiagnosticContextCollector.cs b/src/Serilog.Extensions.Hosting/Extensions/Hosting/DiagnosticContextCollector.cs
index d1aba65..63c6339 100644
--- a/src/Serilog.Extensions.Hosting/Extensions/Hosting/DiagnosticContextCollector.cs
+++ b/src/Serilog.Extensions.Hosting/Extensions/Hosting/DiagnosticContextCollector.cs
@@ -11,6 +11,7 @@ public sealed class DiagnosticContextCollector : IDisposable
{
readonly IDisposable _chainedDisposable;
readonly object _propertiesLock = new object();
+ Exception _exception;
Dictionary _properties = new Dictionary();
///
@@ -38,6 +39,28 @@ public void AddOrUpdate(LogEventProperty property)
}
}
+ ///
+ /// Set the exception associated with the current diagnostic context.
+ ///
+ ///
+ /// 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.
+ ///
+ ///
+ /// If an unhandled exception reaches Serilog's RequestLoggingMiddleware, then the unhandled exception takes precedence.
+ /// If null is given, it clears any previously assigned exception.
+ ///
+ /// The exception to log.
+ public void SetException(Exception exception)
+ {
+ lock (_propertiesLock)
+ {
+ if (_properties == null) return;
+ _exception = exception;
+ }
+ }
+
///
/// 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
@@ -45,12 +68,31 @@ public void AddOrUpdate(LogEventProperty property)
///
/// The collected properties, or null if no collection is active.
/// True if properties could be collected.
+ ///
+ [Obsolete("Replaced by TryComplete(out IEnumerable properties, out Exception exception).")]
public bool TryComplete(out IEnumerable properties)
+ {
+ return TryComplete(out properties, out _);
+ }
+
+ ///
+ /// 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.
+ ///
+ /// The collected properties, or null if no collection is active.
+ /// The collected exception, or null if none has been collected or if no collection is active.
+ /// True if properties could be collected.
+ ///
+ ///
+ public bool TryComplete(out IEnumerable properties, out Exception exception)
{
lock (_propertiesLock)
{
properties = _properties?.Values;
+ exception = _exception;
_properties = null;
+ _exception = null;
Dispose();
return properties != null;
}
diff --git a/src/Serilog.Extensions.Hosting/Extensions/Hosting/ReloadableLogger.cs b/src/Serilog.Extensions.Hosting/Extensions/Hosting/ReloadableLogger.cs
index 5c95a55..6ec1d37 100644
--- a/src/Serilog.Extensions.Hosting/Extensions/Hosting/ReloadableLogger.cs
+++ b/src/Serilog.Extensions.Hosting/Extensions/Hosting/ReloadableLogger.cs
@@ -368,9 +368,13 @@ public bool BindProperty(string propertyName, object value, bool destructureObje
[MethodImpl(MethodImplOptions.AggressiveInlining)]
(ILogger, bool) UpdateForCaller(ILogger root, ILogger cached, IReloadableLogger caller, out ILogger newRoot, out ILogger newCached, out bool frozen)
{
+ // Synchronization on `_sync` is not required in this method; it will be called without a lock
+ // if `_frozen` and under a lock if not.
+
if (_frozen)
{
- // If we're frozen, then the caller hasn't observed this yet and should update.
+ // If we're frozen, then the caller hasn't observed this yet and should update. We could optimize a little here
+ // and only signal an update if the cached logger is stale (as per the next condition below).
newRoot = _logger;
newCached = caller.ReloadLogger();
frozen = true;
@@ -384,7 +388,7 @@ public bool BindProperty(string propertyName, object value, bool destructureObje
frozen = false;
return (cached, false);
}
-
+
newRoot = _logger;
newCached = caller.ReloadLogger();
frozen = false;
diff --git a/src/Serilog.Extensions.Hosting/IDiagnosticContext.cs b/src/Serilog.Extensions.Hosting/IDiagnosticContext.cs
index 468fa84..79da237 100644
--- a/src/Serilog.Extensions.Hosting/IDiagnosticContext.cs
+++ b/src/Serilog.Extensions.Hosting/IDiagnosticContext.cs
@@ -12,6 +12,8 @@
// See the License for the specific language governing permissions and
// limitations under the License.
+using System;
+
namespace Serilog
{
///
@@ -27,6 +29,17 @@ public interface IDiagnosticContext
/// The property value.
/// 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.
- void Set(string propertyName, object value, bool destructureObjects = false);
+ void Set(string propertyName, object value, bool destructureObjects = false);
+
+ ///
+ /// Set the specified exception on the current diagnostic context.
+ ///
+ ///
+ /// This method is useful when unhandled exceptions do not reach Serilog.AspNetCore.RequestLoggingMiddleware,
+ /// such as when using Hellang.Middleware.ProblemDetails
+ /// to transform exceptions to ProblemDetails responses.
+ ///
+ /// The exception to log. If null is given, it clears any previously assigned exception.
+ void SetException(Exception exception);
}
}
diff --git a/src/Serilog.Extensions.Hosting/Serilog.Extensions.Hosting.csproj b/src/Serilog.Extensions.Hosting/Serilog.Extensions.Hosting.csproj
index 375fc60..a1c9cd3 100644
--- a/src/Serilog.Extensions.Hosting/Serilog.Extensions.Hosting.csproj
+++ b/src/Serilog.Extensions.Hosting/Serilog.Extensions.Hosting.csproj
@@ -2,7 +2,7 @@
Serilog support for .NET Core logging in hosted services
- 4.2.0
+ 5.0.0
Microsoft;Serilog Contributors
netstandard2.0;netstandard2.1
8
diff --git a/src/Serilog.Extensions.Hosting/SerilogHostBuilderExtensions.cs b/src/Serilog.Extensions.Hosting/SerilogHostBuilderExtensions.cs
index 7764a2b..086f810 100644
--- a/src/Serilog.Extensions.Hosting/SerilogHostBuilderExtensions.cs
+++ b/src/Serilog.Extensions.Hosting/SerilogHostBuilderExtensions.cs
@@ -80,7 +80,13 @@ public static IHostBuilder UseSerilog(
collection.AddSingleton(services => new SerilogLoggerFactory(logger, dispose));
}
- ConfigureServices(collection, logger);
+ if (logger != null)
+ {
+ // This won't (and shouldn't) take ownership of the logger.
+ collection.AddSingleton(logger);
+ }
+ bool useRegisteredLogger = logger != null;
+ ConfigureDiagnosticContext(collection, useRegisteredLogger);
});
return builder;
@@ -221,32 +227,26 @@ public static IHostBuilder UseSerilog(
return factory;
});
-
- // Null is passed here because we've already (lazily) registered `ILogger`
- ConfigureServices(collection, null);
+
+ ConfigureDiagnosticContext(collection, preserveStaticLogger);
});
return builder;
}
- static void ConfigureServices(IServiceCollection collection, ILogger logger)
+ static void ConfigureDiagnosticContext(IServiceCollection collection, bool useRegisteredLogger)
{
if (collection == null) throw new ArgumentNullException(nameof(collection));
- if (logger != null)
- {
- // This won't (and shouldn't) take ownership of the logger.
- collection.AddSingleton(logger);
- }
-
- // Registered to provide two services...
- var diagnosticContext = new DiagnosticContext(logger);
-
+ // Registered to provide two services...
// Consumed by e.g. middleware
- collection.AddSingleton(diagnosticContext);
-
+ collection.AddSingleton(services =>
+ {
+ ILogger logger = useRegisteredLogger ? services.GetRequiredService().Logger : null;
+ return new DiagnosticContext(logger);
+ });
// Consumed by user code
- collection.AddSingleton(diagnosticContext);
+ collection.AddSingleton(services => services.GetRequiredService());
}
}
}
diff --git a/test/Serilog.Extensions.Hosting.Tests/DiagnosticContextTests.cs b/test/Serilog.Extensions.Hosting.Tests/DiagnosticContextTests.cs
index c32f9ea..c3fbb1d 100644
--- a/test/Serilog.Extensions.Hosting.Tests/DiagnosticContextTests.cs
+++ b/test/Serilog.Extensions.Hosting.Tests/DiagnosticContextTests.cs
@@ -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()
{
@@ -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()
{
@@ -53,5 +102,18 @@ public void ExistingPropertiesCanBeUpdated()
var scalar = Assert.IsType(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);
+ }
}
}
diff --git a/test/Serilog.Extensions.Hosting.Tests/Serilog.Extensions.Hosting.Tests.csproj b/test/Serilog.Extensions.Hosting.Tests/Serilog.Extensions.Hosting.Tests.csproj
index 1601189..c964995 100644
--- a/test/Serilog.Extensions.Hosting.Tests/Serilog.Extensions.Hosting.Tests.csproj
+++ b/test/Serilog.Extensions.Hosting.Tests/Serilog.Extensions.Hosting.Tests.csproj
@@ -1,7 +1,7 @@
- netcoreapp3.1;net5.0;net4.8
+ netcoreapp3.1;net6.0;net4.8
Serilog.Extensions.Hosting.Tests
../../assets/Serilog.snk
true