diff --git a/global.json b/global.json index 6b51ddf..0a37afd 100644 --- a/global.json +++ b/global.json @@ -1,5 +1,5 @@ { "sdk": { - "version": "2.1.300-*" + "version": "2.2.105" } } diff --git a/samples/SimpleServiceSample/PrintTimeService.cs b/samples/SimpleServiceSample/PrintTimeService.cs index aa59fd0..513cace 100644 --- a/samples/SimpleServiceSample/PrintTimeService.cs +++ b/samples/SimpleServiceSample/PrintTimeService.cs @@ -4,7 +4,7 @@ using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -namespace SimpleWebSample +namespace SimpleServiceSample { public class PrintTimeService : IHostedService, IDisposable { diff --git a/samples/SimpleServiceSample/Program.cs b/samples/SimpleServiceSample/Program.cs index b357b10..4ff6e7e 100644 --- a/samples/SimpleServiceSample/Program.cs +++ b/samples/SimpleServiceSample/Program.cs @@ -1,11 +1,11 @@ using System; using System.IO; -using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Configuration; -using Serilog; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Serilog; -namespace SimpleWebSample +namespace SimpleServiceSample { public class Program { diff --git a/serilog-extensions-hosting.sln.DotSettings b/serilog-extensions-hosting.sln.DotSettings new file mode 100644 index 0000000..87d9b7b --- /dev/null +++ b/serilog-extensions-hosting.sln.DotSettings @@ -0,0 +1,5 @@ + + True + True + True + True \ No newline at end of file diff --git a/src/Serilog.Extensions.Hosting/Extensions/Hosting/AmbientDiagnosticContextCollector.cs b/src/Serilog.Extensions.Hosting/Extensions/Hosting/AmbientDiagnosticContextCollector.cs new file mode 100644 index 0000000..acc1ccd --- /dev/null +++ b/src/Serilog.Extensions.Hosting/Extensions/Hosting/AmbientDiagnosticContextCollector.cs @@ -0,0 +1,32 @@ +using System; +using System.Threading; + +namespace Serilog.Extensions.Hosting +{ + class AmbientDiagnosticContextCollector : IDisposable + { + static readonly AsyncLocal AmbientCollector = + new AsyncLocal(); + + // The indirection here ensures that completing collection cleans up the collector in all + // execution contexts. Via @benaadams' addition to `HttpContextAccessor` :-) + DiagnosticContextCollector _collector; + + public static DiagnosticContextCollector Current => AmbientCollector.Value?._collector; + + public static DiagnosticContextCollector Begin() + { + var value = new AmbientDiagnosticContextCollector(); + value._collector = new DiagnosticContextCollector(value); + AmbientCollector.Value = value; + return value._collector; + } + + public void Dispose() + { + _collector = null; + if (AmbientCollector.Value == this) + AmbientCollector.Value = null; + } + } +} diff --git a/src/Serilog.Extensions.Hosting/Extensions/Hosting/DiagnosticContext.cs b/src/Serilog.Extensions.Hosting/Extensions/Hosting/DiagnosticContext.cs new file mode 100644 index 0000000..0e5e602 --- /dev/null +++ b/src/Serilog.Extensions.Hosting/Extensions/Hosting/DiagnosticContext.cs @@ -0,0 +1,59 @@ +// Copyright 2019 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; + +namespace Serilog.Extensions.Hosting +{ + /// + /// Implements an ambient/async-local diagnostic context. Consumers should + /// use in preference to this concrete type. + /// + public class DiagnosticContext : IDiagnosticContext + { + readonly ILogger _logger; + + /// + /// Construct a . + /// + /// A logger for binding properties in the context, or null to use . + public DiagnosticContext(ILogger logger) + { + _logger = logger; + } + + /// + /// Start collecting properties to associate with the current async context. This will replace + /// the active collector, if any. + /// + /// A collector that will receive events added in the current async context. + public DiagnosticContextCollector BeginCollection() + { + return AmbientDiagnosticContextCollector.Begin(); + } + + /// + public void Add(string propertyName, object value, bool destructureObjects = false) + { + if (propertyName == null) throw new ArgumentNullException(nameof(propertyName)); + + var collector = AmbientDiagnosticContextCollector.Current; + if (collector != null && + (_logger ?? Log.Logger).BindProperty(propertyName, value, destructureObjects, out var property)) + { + collector.Add(property); + } + } + } +} diff --git a/src/Serilog.Extensions.Hosting/Extensions/Hosting/DiagnosticContextCollector.cs b/src/Serilog.Extensions.Hosting/Extensions/Hosting/DiagnosticContextCollector.cs new file mode 100644 index 0000000..dd92af7 --- /dev/null +++ b/src/Serilog.Extensions.Hosting/Extensions/Hosting/DiagnosticContextCollector.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.Generic; +using Serilog.Events; + +namespace Serilog.Extensions.Hosting +{ + /// + /// A container that receives properties added to a diagnostic context. + /// + public sealed class DiagnosticContextCollector : IDisposable + { + readonly AmbientDiagnosticContextCollector _ambientCollector; + List _properties = new List(); + + internal DiagnosticContextCollector(AmbientDiagnosticContextCollector ambientCollector) + { + _ambientCollector = ambientCollector ?? throw new ArgumentNullException(nameof(ambientCollector)); + } + + /// + /// Add the property to the context. + /// + /// The property to add. + public void Add(LogEventProperty property) + { + if (property == null) throw new ArgumentNullException(nameof(property)); + _properties?.Add(property); + } + + /// + /// 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. + /// + /// The collected properties, or null if no collection is active. + /// True if properties could be collected. + public bool TryComplete(out List properties) + { + properties = _properties; + _properties = null; + Dispose(); + return properties != null; + } + + /// + public void Dispose() + { + _ambientCollector.Dispose(); + } + } +} diff --git a/src/Serilog.Extensions.Hosting/Hosting/SerilogLoggerFactory.cs b/src/Serilog.Extensions.Hosting/Extensions/Hosting/SerilogLoggerFactory.cs similarity index 87% rename from src/Serilog.Extensions.Hosting/Hosting/SerilogLoggerFactory.cs rename to src/Serilog.Extensions.Hosting/Extensions/Hosting/SerilogLoggerFactory.cs index 1c91166..1b100b9 100644 --- a/src/Serilog.Extensions.Hosting/Hosting/SerilogLoggerFactory.cs +++ b/src/Serilog.Extensions.Hosting/Extensions/Hosting/SerilogLoggerFactory.cs @@ -12,18 +12,25 @@ // See the License for the specific language governing permissions and // limitations under the License. +using System; +using System.ComponentModel; using Microsoft.Extensions.Logging; using Serilog.Debugging; using Serilog.Extensions.Logging; +// To line up with the convention used elsewhere in the *.Extensions libraries, this +// should have been Serilog.Extensions.Hosting. +// ReSharper disable once CheckNamespace namespace Serilog.Hosting { /// /// Implements so that we can inject Serilog Logger. /// + [Obsolete("Replaced with Serilog.Extensions.Logging.SerilogLoggerFactory")] + [EditorBrowsable(EditorBrowsableState.Never)] public class SerilogLoggerFactory : ILoggerFactory { - private readonly SerilogLoggerProvider _provider; + readonly SerilogLoggerProvider _provider; /// /// Initializes a new instance of the class. diff --git a/src/Serilog.Extensions.Hosting/IDiagnosticContext.cs b/src/Serilog.Extensions.Hosting/IDiagnosticContext.cs new file mode 100644 index 0000000..5b472c4 --- /dev/null +++ b/src/Serilog.Extensions.Hosting/IDiagnosticContext.cs @@ -0,0 +1,31 @@ +// Copyright 2019 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. + +namespace Serilog +{ + /// + /// Collects diagnostic information for packaging into wide events. + /// + public interface IDiagnosticContext + { + /// + /// Add the specified property to the request's diagnostic payload. + /// + /// The name of the property. Must be non-empty. + /// 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 Add(string propertyName, object value, bool destructureObjects = false); + } +} diff --git a/src/Serilog.Extensions.Hosting/Serilog.Extensions.Hosting.csproj b/src/Serilog.Extensions.Hosting/Serilog.Extensions.Hosting.csproj index c343413..5a96a0a 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 - 2.0.1 + 3.0.0 Microsoft;Serilog Contributors netstandard2.0 true @@ -23,8 +23,8 @@ - - + + diff --git a/src/Serilog.Extensions.Hosting/SerilogHostBuilderExtensions.cs b/src/Serilog.Extensions.Hosting/SerilogHostBuilderExtensions.cs index 804b8c3..306eadd 100644 --- a/src/Serilog.Extensions.Hosting/SerilogHostBuilderExtensions.cs +++ b/src/Serilog.Extensions.Hosting/SerilogHostBuilderExtensions.cs @@ -1,4 +1,4 @@ -// Copyright 2018 Serilog Contributors +// Copyright 2019 Serilog Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -15,8 +15,9 @@ using System; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -using Serilog.Hosting; using Microsoft.Extensions.DependencyInjection; +using Serilog.Extensions.Hosting; +using Serilog.Extensions.Logging; namespace Serilog { @@ -33,48 +34,126 @@ public static class SerilogHostBuilderExtensions /// 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. - /// The (generic) host builder. - public static IHostBuilder UseSerilog(this IHostBuilder builder, Serilog.ILogger logger = null, bool dispose = false) + /// 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 host builder. + public static IHostBuilder UseSerilog( + this IHostBuilder builder, + ILogger logger = null, + bool dispose = false, + LoggerProviderCollection providers = null) { if (builder == null) throw new ArgumentNullException(nameof(builder)); - builder.ConfigureServices((context, collection) => - collection.AddSingleton(services => new SerilogLoggerFactory(logger, dispose))); + + 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)); + } + + ConfigureServices(collection, logger); + }); + return builder; } - /// - /// Sets Serilog as the logging provider. - /// + /// Sets Serilog as the logging provider. /// /// A is supplied so that configuration and hosting information can be used. /// The logger will be shut down when application services are disposed. /// /// The host builder to configure. - /// The delegate for configuring the that will be used to construct a . + /// The delegate for configuring the that will be used to construct a . /// Indicates whether to preserve the value of . - /// The (generic) host builder. - public static IHostBuilder UseSerilog(this IHostBuilder builder, Action configureLogger, bool preserveStaticLogger = false) + /// 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 host builder. + public static IHostBuilder UseSerilog( + this IHostBuilder builder, + Action configureLogger, + bool preserveStaticLogger = false, + bool writeToProviders = false) { if (builder == null) throw new ArgumentNullException(nameof(builder)); if (configureLogger == null) throw new ArgumentNullException(nameof(configureLogger)); + builder.ConfigureServices((context, collection) => { var loggerConfiguration = new LoggerConfiguration(); + + LoggerProviderCollection loggerProviders = null; + if (writeToProviders) + { + loggerProviders = new LoggerProviderCollection(); + loggerConfiguration.WriteTo.Providers(loggerProviders); + } + configureLogger(context, loggerConfiguration); var logger = loggerConfiguration.CreateLogger(); + + ILogger registeredLogger = null; if (preserveStaticLogger) { - collection.AddSingleton(services => new SerilogLoggerFactory(logger, true)); + registeredLogger = logger; } - else + 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; - collection.AddSingleton(services => new SerilogLoggerFactory(null, true)); } + + collection.AddSingleton(services => + { + var factory = new SerilogLoggerFactory(registeredLogger, true, loggerProviders); + + if (writeToProviders) + { + foreach (var provider in services.GetServices()) + factory.AddProvider(provider); + } + + return factory; + }); + + ConfigureServices(collection, logger); }); return builder; } + + static void ConfigureServices(IServiceCollection collection, ILogger logger) + { + 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); + + // Consumed by e.g. middleware + collection.AddSingleton(diagnosticContext); + + // Consumed by user code + collection.AddSingleton(diagnosticContext); + } } } diff --git a/test/Serilog.Extensions.Hosting.Tests/DiagnosticContextTests.cs b/test/Serilog.Extensions.Hosting.Tests/DiagnosticContextTests.cs new file mode 100644 index 0000000..834492c --- /dev/null +++ b/test/Serilog.Extensions.Hosting.Tests/DiagnosticContextTests.cs @@ -0,0 +1,38 @@ +using System; +using System.Threading.Tasks; +using Serilog.Extensions.Hosting.Tests.Support; +using Xunit; + +namespace Serilog.Extensions.Hosting.Tests +{ + public class DiagnosticContextTests + { + [Fact] + public void AddIsSafeWhenNoContextIsActive() + { + var dc = new DiagnosticContext(Some.Logger()); + dc.Add(Some.String("name"), Some.Int32()); + } + + [Fact] + public async Task PropertiesAreCollectedInAnActiveContext() + { + var dc = new DiagnosticContext(Some.Logger()); + + var collector = dc.BeginCollection(); + dc.Add(Some.String("name"), Some.Int32()); + await Task.Delay(TimeSpan.FromMilliseconds(10)); + dc.Add(Some.String("name"), Some.Int32()); + + Assert.True(collector.TryComplete(out var properties)); + Assert.Equal(2, properties.Count); + + Assert.False(collector.TryComplete(out _)); + + collector.Dispose(); + + dc.Add(Some.String("name"), Some.Int32()); + Assert.False(collector.TryComplete(out _)); + } + } +} 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 bdbdbcc..f8952eb 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 @@  - netcoreapp2.1 + netcoreapp2.2 Serilog.Extensions.Hosting.Tests ../../assets/Serilog.snk true @@ -14,9 +14,12 @@ - - - + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + diff --git a/test/Serilog.Extensions.Hosting.Tests/SerilogHostBuilderExtensionsTests.cs b/test/Serilog.Extensions.Hosting.Tests/SerilogHostBuilderExtensionsTests.cs deleted file mode 100644 index 3341f1d..0000000 --- a/test/Serilog.Extensions.Hosting.Tests/SerilogHostBuilderExtensionsTests.cs +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using Xunit; - -namespace Serilog.Extensions.Hosting.Tests -{ - public class SerilogHostBuilderExtensionsTests - { - [Fact] - public void Todo() - { - - } - } -} \ No newline at end of file diff --git a/test/Serilog.Extensions.Hosting.Tests/Support/Some.cs b/test/Serilog.Extensions.Hosting.Tests/Support/Some.cs new file mode 100644 index 0000000..5137cef --- /dev/null +++ b/test/Serilog.Extensions.Hosting.Tests/Support/Some.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading; +using Serilog.Events; + +namespace Serilog.Extensions.Hosting.Tests.Support +{ + static class Some + { + static int _next; + + public static int Int32() => Interlocked.Increment(ref _next); + + public static string String(string tag = null) => $"s_{tag}{Int32()}"; + + public static LogEventProperty LogEventProperty() => new LogEventProperty(String("name"), new ScalarValue(Int32())); + + public static ILogger Logger() => new LoggerConfiguration().CreateLogger(); + } +}