diff --git a/README.md b/README.md index f35ae30..096f513 100644 --- a/README.md +++ b/README.md @@ -9,8 +9,8 @@ Serilog logging for _Microsoft.Extensions.Hosting_. This package routes framewor **First**, install the _Serilog.Extensions.Hosting_ [NuGet package](https://www.nuget.org/packages/Serilog.Extensions.Hosting) into your app. You will need a way to view the log messages - _Serilog.Sinks.Console_ writes these to the console; there are [many more sinks available](https://www.nuget.org/packages?q=Tags%3A%22serilog%22) on NuGet. ```powershell -Install-Package Serilog.Extensions.Hosting -DependencyVersion Highest -Install-Package Serilog.Sinks.Console +dotnet add package Serilog.Extensions.Hosting +dotnet add package Serilog.Sinks.Console ``` **Next**, in your application's _Program.cs_ file, configure Serilog first. A `try`/`catch` block will ensure any configuration issues are appropriately logged: @@ -88,3 +88,7 @@ You can alternatively configure Serilog using a delegate as shown below: This has the advantage of making the `hostingContext`'s `Configuration` object available for configuration of the logger, but at the expense of ignoring `Exception`s raised earlier in program startup. If this method is used, `Log.Logger` is assigned implicitly, and closed when the app is shut down. + +### Versioning + +This package tracks the versioning and target framework support of its [_Microsoft.Extensions.Hosting_](https://nuget.org/packages/Microsoft.Extensions.Hosting) dependency. diff --git a/appveyor.yml b/appveyor.yml index 27bd2ea..78df5a5 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -13,7 +13,7 @@ deploy: - provider: NuGet skip_symbols: true api_key: - secure: Q65rY+zaFWOhs8a9IVSaX4Go5HNcIlEXjEFWMB83Y325WE9aXzi0xzDDc0/fJDzk + secure: EN9f+XXE3fW+ebL4wxrIbafdtbNvRfddBN8UUixvctYh4qMBHzr1JdnM83QsM1zo on: branch: /^(main|dev)$/ - provider: GitHub diff --git a/global.json b/global.json index af5a328..2c23bfe 100644 --- a/global.json +++ b/global.json @@ -1,7 +1,7 @@ { "sdk": { "allowPrerelease": false, - "version": "6.0.300", + "version": "7.0.100", "rollForward": "latestFeature" } } diff --git a/samples/SimpleServiceSample/SimpleServiceSample.csproj b/samples/SimpleServiceSample/SimpleServiceSample.csproj index 9d323e4..c0736fa 100644 --- a/samples/SimpleServiceSample/SimpleServiceSample.csproj +++ b/samples/SimpleServiceSample/SimpleServiceSample.csproj @@ -1,7 +1,7 @@  - netcoreapp3.1 + net7.0 @@ -9,8 +9,8 @@ - - + + diff --git a/samples/WebApplicationSample/WebApplicationSample.csproj b/samples/WebApplicationSample/WebApplicationSample.csproj index 9f85c41..587f02c 100644 --- a/samples/WebApplicationSample/WebApplicationSample.csproj +++ b/samples/WebApplicationSample/WebApplicationSample.csproj @@ -1,7 +1,7 @@ - netcoreapp3.1 + net7.0 diff --git a/src/Serilog.Extensions.Hosting/Properties/AssemblyInfo.cs b/src/Serilog.Extensions.Hosting/Properties/AssemblyInfo.cs index 6890343..8ec980f 100644 --- a/src/Serilog.Extensions.Hosting/Properties/AssemblyInfo.cs +++ b/src/Serilog.Extensions.Hosting/Properties/AssemblyInfo.cs @@ -1,7 +1,7 @@ using System.Reflection; using System.Runtime.CompilerServices; -[assembly: AssemblyVersion("2.0.0.0")] +[assembly: AssemblyVersion("7.0.0.0")] [assembly: InternalsVisibleTo("Serilog.Extensions.Hosting.Tests, PublicKey=" + "0024000004800000940000000602000000240000525341310004000001000100fb8d13fd344a1c" + diff --git a/src/Serilog.Extensions.Hosting/Serilog.Extensions.Hosting.csproj b/src/Serilog.Extensions.Hosting/Serilog.Extensions.Hosting.csproj index 36c62ea..c914b3c 100644 --- a/src/Serilog.Extensions.Hosting/Serilog.Extensions.Hosting.csproj +++ b/src/Serilog.Extensions.Hosting/Serilog.Extensions.Hosting.csproj @@ -2,17 +2,18 @@ Serilog support for .NET Core logging in hosted services - 5.0.1 + + 7.0.0 Microsoft;Serilog Contributors - netstandard2.0;netstandard2.1 + + net462;netstandard2.0;netstandard2.1;net6.0;net7.0 8 true true - Serilog.Extensions.Hosting ../../assets/Serilog.snk true true - Serilog.Extensions.Hosting serilog;aspnet;aspnetcore;hosting icon.png https://github.com/serilog/serilog-extensions-hosting @@ -23,20 +24,21 @@ Serilog - + $(DefineConstants);NO_RELOADABLE_LOGGER - - - - - + + + - - + + + + + diff --git a/src/Serilog.Extensions.Hosting/SerilogHostBuilderExtensions.cs b/src/Serilog.Extensions.Hosting/SerilogHostBuilderExtensions.cs index d588000..7f0ab0d 100644 --- a/src/Serilog.Extensions.Hosting/SerilogHostBuilderExtensions.cs +++ b/src/Serilog.Extensions.Hosting/SerilogHostBuilderExtensions.cs @@ -13,10 +13,8 @@ // limitations under the License. using System; -using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -using Serilog.Extensions.Hosting; using Serilog.Extensions.Logging; // ReSharper disable MemberCanBePrivate.Global @@ -27,20 +25,6 @@ namespace Serilog /// public static class SerilogHostBuilderExtensions { - // Used internally to pass information through the container. We need to do this because if `logger` is the - // root logger, registering it as a singleton may lead to disposal along with the container by MEDI. This isn't - // always desirable, i.e. we may be handed a logger and `dispose: false`, so wrapping it keeps us in control - // of when the logger is disposed. - class RegisteredLogger - { - public RegisteredLogger(ILogger logger) - { - Logger = logger; - } - - public ILogger Logger { get; } - } - /// /// Sets Serilog as the logging provider. /// @@ -63,33 +47,7 @@ public static IHostBuilder UseSerilog( builder.ConfigureServices((_, collection) => { - if (providers != null) - { - collection.AddSingleton(services => - { - var factory = new SerilogLoggerFactory(logger, dispose, providers); - - foreach (var provider in services.GetServices()) - factory.AddProvider(provider); - - return factory; - }); - } - else - { - collection.AddSingleton(services => new SerilogLoggerFactory(logger, dispose)); - } - - if (logger != null) - { - // This won't (and shouldn't) take ownership of the logger. - collection.AddSingleton(logger); - - // Still need to use RegisteredLogger as it is used by ConfigureDiagnosticContext. - collection.AddSingleton(new RegisteredLogger(logger)); - } - bool useRegisteredLogger = logger != null; - ConfigureDiagnosticContext(collection, useRegisteredLogger); + collection.AddSerilog(logger, dispose, providers); }); return builder; @@ -148,108 +106,16 @@ public static IHostBuilder UseSerilog( if (builder == null) throw new ArgumentNullException(nameof(builder)); if (configureLogger == null) throw new ArgumentNullException(nameof(configureLogger)); - // This check is eager; replacing the bootstrap logger after calling this method is not supported. -#if !NO_RELOADABLE_LOGGER - var reloadable = Log.Logger as ReloadableLogger; - var useReload = reloadable != null && !preserveStaticLogger; -#else - const bool useReload = false; -#endif - builder.ConfigureServices((context, collection) => { - LoggerProviderCollection loggerProviders = null; - if (writeToProviders) - { - loggerProviders = new LoggerProviderCollection(); - } - - collection.AddSingleton(services => - { - ILogger logger; -#if !NO_RELOADABLE_LOGGER - if (useReload) - { - reloadable!.Reload(cfg => - { - if (loggerProviders != null) - cfg.WriteTo.Providers(loggerProviders); - - configureLogger(context, services, cfg); - return cfg; - }); - - logger = reloadable.Freeze(); - } - else -#endif - { - var loggerConfiguration = new LoggerConfiguration(); - - if (loggerProviders != null) - loggerConfiguration.WriteTo.Providers(loggerProviders); - - configureLogger(context, services, loggerConfiguration); - logger = loggerConfiguration.CreateLogger(); - } - - return new RegisteredLogger(logger); - }); - - collection.AddSingleton(services => - { - // How can we register the logger, here, but not have MEDI dispose it? - // Using the `NullEnricher` hack to prevent disposal. - var logger = services.GetRequiredService().Logger; - return logger.ForContext(new NullEnricher()); - }); - - collection.AddSingleton(services => - { - var logger = services.GetRequiredService().Logger; - - ILogger registeredLogger = null; - if (preserveStaticLogger) - { - registeredLogger = logger; - } - else - { - // Passing a `null` logger to `SerilogLoggerFactory` results in disposal via - // `Log.CloseAndFlush()`, which additionally replaces the static logger with a no-op. - Log.Logger = logger; - } - - var factory = new SerilogLoggerFactory(registeredLogger, !useReload, loggerProviders); - - if (writeToProviders) - { - foreach (var provider in services.GetServices()) - factory.AddProvider(provider); - } - - return factory; - }); - - ConfigureDiagnosticContext(collection, preserveStaticLogger); + collection.AddSerilog( + (services, loggerConfiguration) => + configureLogger(context, services, loggerConfiguration), + preserveStaticLogger: preserveStaticLogger, + writeToProviders: writeToProviders); }); return builder; } - - static void ConfigureDiagnosticContext(IServiceCollection collection, bool useRegisteredLogger) - { - if (collection == null) throw new ArgumentNullException(nameof(collection)); - - // Registered to provide two services... - // Consumed by e.g. middleware - collection.AddSingleton(services => - { - ILogger logger = useRegisteredLogger ? services.GetRequiredService().Logger : null; - return new DiagnosticContext(logger); - }); - // Consumed by user code - collection.AddSingleton(services => services.GetRequiredService()); - } } } diff --git a/src/Serilog.Extensions.Hosting/SerilogServiceCollectionExtensions.cs b/src/Serilog.Extensions.Hosting/SerilogServiceCollectionExtensions.cs new file mode 100644 index 0000000..e6055c0 --- /dev/null +++ b/src/Serilog.Extensions.Hosting/SerilogServiceCollectionExtensions.cs @@ -0,0 +1,241 @@ +// Copyright 2020 Serilog Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Serilog.Extensions.Hosting; +using Serilog.Extensions.Logging; +// ReSharper disable MemberCanBePrivate.Global + +namespace Serilog +{ + /// + /// Extends with Serilog configuration methods. + /// + public static class SerilogServiceCollectionExtensions + { + // Used internally to pass information through the container. We need to do this because if `logger` is the + // root logger, registering it as a singleton may lead to disposal along with the container by MEDI. This isn't + // always desirable, i.e. we may be handed a logger and `dispose: false`, so wrapping it keeps us in control + // of when the logger is disposed. + class RegisteredLogger + { + public RegisteredLogger(ILogger logger) + { + Logger = logger; + } + + public ILogger Logger { get; } + } + + /// + /// Sets Serilog as the logging provider. + /// + /// The service collection to use. + /// The Serilog logger; if not supplied, the static will be used. + /// When true, dispose when the framework disposes the provider. If the + /// logger is not specified but is true, the method will be + /// called on the static class instead. + /// A registered in the Serilog pipeline using the + /// WriteTo.Providers() configuration method, enabling other s to receive events. By + /// default, only Serilog sinks will receive events. + /// The service collection. + public static IServiceCollection AddSerilog( + this IServiceCollection collection, + ILogger logger = null, + bool dispose = false, + LoggerProviderCollection providers = null) + { + if (collection == null) throw new ArgumentNullException(nameof(collection)); + + if (providers != null) + { + collection.AddSingleton(services => + { + var factory = new SerilogLoggerFactory(logger, dispose, providers); + + foreach (var provider in services.GetServices()) + factory.AddProvider(provider); + + return factory; + }); + } + else + { + collection.AddSingleton(services => new SerilogLoggerFactory(logger, dispose)); + } + + if (logger != null) + { + // This won't (and shouldn't) take ownership of the logger. + collection.AddSingleton(logger); + + // Still need to use RegisteredLogger as it is used by ConfigureDiagnosticContext. + collection.AddSingleton(new RegisteredLogger(logger)); + } + bool useRegisteredLogger = logger != null; + ConfigureDiagnosticContext(collection, useRegisteredLogger); + + return collection; + } + + /// Sets Serilog as the logging provider. + /// The service collection to use. + /// The delegate for configuring the that will be used to construct a . + /// Indicates whether to preserve the value of . + /// By default, Serilog does not write events to s registered through + /// the Microsoft.Extensions.Logging API. Normally, equivalent Serilog sinks are used in place of providers. Specify + /// true to write events to all providers. + /// The service collection. + public static IServiceCollection AddSerilog( + this IServiceCollection collection, + Action configureLogger, + bool preserveStaticLogger = false, + bool writeToProviders = false) + { + if (collection == null) throw new ArgumentNullException(nameof(collection)); + if (configureLogger == null) throw new ArgumentNullException(nameof(configureLogger)); + return AddSerilog( + collection, + (services, loggerConfiguration) => + configureLogger(loggerConfiguration), + preserveStaticLogger: preserveStaticLogger, + writeToProviders: writeToProviders); + } + + /// Sets Serilog as the logging provider. + /// The service collection to use. + /// The delegate for configuring the that will be used to construct a . + /// Indicates whether to preserve the value of . + /// By default, Serilog does not write events to s registered through + /// the Microsoft.Extensions.Logging API. Normally, equivalent Serilog sinks are used in place of providers. Specify + /// true to write events to all providers. + /// If the static is a bootstrap logger (see + /// LoggerConfigurationExtensions.CreateBootstrapLogger()), and is + /// not specified, the the bootstrap logger will be reconfigured through the supplied delegate, rather than being + /// replaced entirely or ignored. + /// The service collection. + public static IServiceCollection AddSerilog( + this IServiceCollection collection, + Action configureLogger, + bool preserveStaticLogger = false, + bool writeToProviders = false) + { + if (collection == null) throw new ArgumentNullException(nameof(collection)); + if (configureLogger == null) throw new ArgumentNullException(nameof(configureLogger)); + + // This check is eager; replacing the bootstrap logger after calling this method is not supported. +#if !NO_RELOADABLE_LOGGER + var reloadable = Log.Logger as ReloadableLogger; + var useReload = reloadable != null && !preserveStaticLogger; +#else + const bool useReload = false; +#endif + + LoggerProviderCollection loggerProviders = null; + if (writeToProviders) + { + loggerProviders = new LoggerProviderCollection(); + } + + collection.AddSingleton(services => + { + ILogger logger; +#if !NO_RELOADABLE_LOGGER + if (useReload) + { + reloadable!.Reload(cfg => + { + if (loggerProviders != null) + cfg.WriteTo.Providers(loggerProviders); + + configureLogger(services, cfg); + return cfg; + }); + + logger = reloadable.Freeze(); + } + else +#endif + { + var loggerConfiguration = new LoggerConfiguration(); + + if (loggerProviders != null) + loggerConfiguration.WriteTo.Providers(loggerProviders); + + configureLogger(services, loggerConfiguration); + logger = loggerConfiguration.CreateLogger(); + } + + return new RegisteredLogger(logger); + }); + + collection.AddSingleton(services => + { + // How can we register the logger, here, but not have MEDI dispose it? + // Using the `NullEnricher` hack to prevent disposal. + var logger = services.GetRequiredService().Logger; + return logger.ForContext(new NullEnricher()); + }); + + collection.AddSingleton(services => + { + var logger = services.GetRequiredService().Logger; + + ILogger registeredLogger = null; + if (preserveStaticLogger) + { + registeredLogger = logger; + } + else + { + // Passing a `null` logger to `SerilogLoggerFactory` results in disposal via + // `Log.CloseAndFlush()`, which additionally replaces the static logger with a no-op. + Log.Logger = logger; + } + + var factory = new SerilogLoggerFactory(registeredLogger, !useReload, loggerProviders); + + if (writeToProviders) + { + foreach (var provider in services.GetServices()) + factory.AddProvider(provider); + } + + return factory; + }); + + ConfigureDiagnosticContext(collection, preserveStaticLogger); + + return collection; + } + + static void ConfigureDiagnosticContext(IServiceCollection collection, bool useRegisteredLogger) + { + if (collection == null) throw new ArgumentNullException(nameof(collection)); + + // Registered to provide two services... + // Consumed by e.g. middleware + collection.AddSingleton(services => + { + ILogger logger = useRegisteredLogger ? services.GetRequiredService().Logger : null; + return new DiagnosticContext(logger); + }); + // Consumed by user code + collection.AddSingleton(services => services.GetRequiredService()); + } + } +} diff --git a/test/Serilog.Extensions.Hosting.Tests/DiagnosticContextTests.cs b/test/Serilog.Extensions.Hosting.Tests/DiagnosticContextTests.cs index c3fbb1d..3281065 100644 --- a/test/Serilog.Extensions.Hosting.Tests/DiagnosticContextTests.cs +++ b/test/Serilog.Extensions.Hosting.Tests/DiagnosticContextTests.cs @@ -35,15 +35,16 @@ public async Task PropertiesAreCollectedInAnActiveContext() await Task.Delay(TimeSpan.FromMilliseconds(10)); dc.Set(Some.String("second"), Some.Int32()); - Assert.True(collector.TryComplete(out var properties)); + Assert.True(collector.TryComplete(out var properties, out var exception)); Assert.Equal(2, properties.Count()); + Assert.Null(exception); - Assert.False(collector.TryComplete(out _)); + Assert.False(collector.TryComplete(out _, out _)); collector.Dispose(); dc.Set(Some.String("third"), Some.Int32()); - Assert.False(collector.TryComplete(out _)); + Assert.False(collector.TryComplete(out _, out _)); } [Fact] @@ -97,10 +98,11 @@ public void ExistingPropertiesCanBeUpdated() dc.Set("name", 10); dc.Set("name", 20); - Assert.True(collector.TryComplete(out var properties)); + Assert.True(collector.TryComplete(out var properties, out var exception)); var prop = Assert.Single(properties); var scalar = Assert.IsType(prop.Value); Assert.Equal(20, scalar.Value); + Assert.Null(exception); } [Fact] 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 c964995..e83da0d 100644 --- a/test/Serilog.Extensions.Hosting.Tests/Serilog.Extensions.Hosting.Tests.csproj +++ b/test/Serilog.Extensions.Hosting.Tests/Serilog.Extensions.Hosting.Tests.csproj @@ -1,12 +1,10 @@  - netcoreapp3.1;net6.0;net4.8 - Serilog.Extensions.Hosting.Tests + net6.0;net7.0;net4.8 ../../assets/Serilog.snk true true - true latest @@ -19,13 +17,10 @@ - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - + + + + diff --git a/test/Serilog.Extensions.Hosting.Tests/SerilogServiceCollectionExtensionsTests.cs b/test/Serilog.Extensions.Hosting.Tests/SerilogServiceCollectionExtensionsTests.cs new file mode 100644 index 0000000..7fd9752 --- /dev/null +++ b/test/Serilog.Extensions.Hosting.Tests/SerilogServiceCollectionExtensionsTests.cs @@ -0,0 +1,58 @@ +using System; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Xunit; + +namespace Serilog.Extensions.Hosting.Tests +{ + public class SerilogServiceCollectionExtensionsTests + { + [Fact] + public void ServicesAreRegisteredWhenCallingAddSerilog() + { + // Arrange + var collection = new ServiceCollection(); + + // Act + collection.AddSerilog(); + + // Assert + using var provider = collection.BuildServiceProvider(); + provider.GetRequiredService(); + provider.GetRequiredService(); + } + + [Fact] + public void ServicesAreRegisteredWhenCallingAddSerilogWithLogger() + { + // Arrange + var collection = new ServiceCollection(); + ILogger logger = new LoggerConfiguration().CreateLogger(); + + // Act + collection.AddSerilog(logger); + + // Assert + using var provider = collection.BuildServiceProvider(); + provider.GetRequiredService(); + provider.GetRequiredService(); + provider.GetRequiredService(); + } + + [Fact] + public void ServicesAreRegisteredWhenCallingAddSerilogWithConfigureDelegate() + { + // Arrange + var collection = new ServiceCollection(); + + // Act + collection.AddSerilog(_ => { }); + + // Assert + using var provider = collection.BuildServiceProvider(); + provider.GetRequiredService(); + provider.GetRequiredService(); + provider.GetRequiredService(); + } + } +}