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