diff --git a/samples/Sample/Program.cs b/samples/Sample/Program.cs index 7f95bfa..03014b7 100644 --- a/samples/Sample/Program.cs +++ b/samples/Sample/Program.cs @@ -2,6 +2,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Serilog; +using Serilog.Extensions.Logging; namespace Sample { @@ -9,19 +10,33 @@ public class Program { public static void Main(string[] args) { + // Creating a `LoggerProviderCollection` lets Serilog optionally write + // events through other dynamically-added MEL ILoggerProviders. + var providers = new LoggerProviderCollection(); + Log.Logger = new LoggerConfiguration() .MinimumLevel.Debug() - .WriteTo.LiterateConsole() + .WriteTo.Console() + .WriteTo.Providers(providers) .CreateLogger(); - var services = new ServiceCollection() - .AddLogging(builder => - { - builder.AddSerilog(); - }); + var services = new ServiceCollection(); + + services.AddSingleton(providers); + services.AddSingleton(sc => + { + var providerCollection = sc.GetService(); + var factory = new SerilogLoggerFactory(null, true, providerCollection); + + foreach (var provider in sc.GetServices()) + factory.AddProvider(provider); + + return factory; + }); + + services.AddLogging(l => l.AddConsole()); var serviceProvider = services.BuildServiceProvider(); - // getting the logger using the class's name is conventional var logger = serviceProvider.GetRequiredService>(); var startTime = DateTimeOffset.UtcNow; @@ -57,6 +72,8 @@ public static void Main(string[] args) logger.LogInformation("{Result,-10:l}{StartTime,15:l}{EndTime,15:l}{Duration,15:l}", "RESULT", "START TIME", "END TIME", "DURATION(ms)"); logger.LogInformation("{Result,-10:l}{StartTime,15:l}{EndTime,15:l}{Duration,15:l}", "------", "----- ----", "--- ----", "------------"); logger.LogInformation("{Result,-10:l}{StartTime,15:mm:s tt}{EndTime,15:mm:s tt}{Duration,15}", "SUCCESS", startTime, endTime, (endTime - startTime).TotalMilliseconds); + + serviceProvider.Dispose(); } } } diff --git a/samples/Sample/Sample.csproj b/samples/Sample/Sample.csproj index 5c34526..c557b8a 100644 --- a/samples/Sample/Sample.csproj +++ b/samples/Sample/Sample.csproj @@ -1,7 +1,7 @@  - net461;netcoreapp2.0 + netcoreapp2.0 Sample Exe Sample @@ -14,7 +14,8 @@ - + + \ No newline at end of file diff --git a/serilog-extensions-logging.sln.DotSettings b/serilog-extensions-logging.sln.DotSettings new file mode 100644 index 0000000..c6bac2f --- /dev/null +++ b/serilog-extensions-logging.sln.DotSettings @@ -0,0 +1,7 @@ + + True + True + True + True + True + True \ No newline at end of file diff --git a/src/Serilog.Extensions.Logging/Extensions/Logging/LevelMapping.cs b/src/Serilog.Extensions.Logging/Extensions/Logging/LevelMapping.cs new file mode 100644 index 0000000..44b543d --- /dev/null +++ b/src/Serilog.Extensions.Logging/Extensions/Logging/LevelMapping.cs @@ -0,0 +1,64 @@ +// 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 Microsoft.Extensions.Logging; +using Serilog.Events; + +namespace Serilog.Extensions.Logging +{ + static class LevelMapping + { + public static LogEventLevel ToSerilogLevel(LogLevel logLevel) + { + switch (logLevel) + { + case LogLevel.Critical: + return LogEventLevel.Fatal; + case LogLevel.Error: + return LogEventLevel.Error; + case LogLevel.Warning: + return LogEventLevel.Warning; + case LogLevel.Information: + return LogEventLevel.Information; + case LogLevel.Debug: + return LogEventLevel.Debug; + // ReSharper disable once RedundantCaseLabel + case LogLevel.Trace: + default: + return LogEventLevel.Verbose; + } + } + + public static LogLevel ToExtensionsLevel(LogEventLevel logEventLevel) + { + switch (logEventLevel) + { + case LogEventLevel.Fatal: + return LogLevel.Critical; + case LogEventLevel.Error: + return LogLevel.Error; + case LogEventLevel.Warning: + return LogLevel.Warning; + case LogEventLevel.Information: + return LogLevel.Information; + case LogEventLevel.Debug: + return LogLevel.Debug; + // ReSharper disable once RedundantCaseLabel + case LogEventLevel.Verbose: + default: + return LogLevel.Trace; + } + } + } +} diff --git a/src/Serilog.Extensions.Logging/Extensions/Logging/LoggerProviderCollection.cs b/src/Serilog.Extensions.Logging/Extensions/Logging/LoggerProviderCollection.cs new file mode 100644 index 0000000..6ecb000 --- /dev/null +++ b/src/Serilog.Extensions.Logging/Extensions/Logging/LoggerProviderCollection.cs @@ -0,0 +1,67 @@ +// 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; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using Microsoft.Extensions.Logging; + +namespace Serilog.Extensions.Logging +{ + /// + /// A dynamically-modifiable collection of s. + /// + public class LoggerProviderCollection : IDisposable + { + volatile ILoggerProvider[] _providers = new ILoggerProvider[0]; + + /// + /// Add to the collection. + /// + /// A logger provider. + public void AddProvider(ILoggerProvider provider) + { + if (provider == null) throw new ArgumentNullException(nameof(provider)); + + var existing = _providers; + var added = existing.Concat(new[] {provider}).ToArray(); + +#pragma warning disable 420 // ref to a volatile field + while (Interlocked.CompareExchange(ref _providers, added, existing) != existing) +#pragma warning restore 420 + { + existing = _providers; + added = existing.Concat(new[] { provider }).ToArray(); + } + } + + /// + /// Get the currently-active providers. + /// + /// + /// If the collection has been disposed, we'll leave the individual + /// providers with the job of throwing . + /// + public IEnumerable Providers => _providers; + + /// + public void Dispose() + { + foreach (var provider in _providers) + provider.Dispose(); + } + } +} diff --git a/src/Serilog.Extensions.Logging/Extensions/Logging/LoggerProviderCollectionSink.cs b/src/Serilog.Extensions.Logging/Extensions/Logging/LoggerProviderCollectionSink.cs new file mode 100644 index 0000000..ba3a93b --- /dev/null +++ b/src/Serilog.Extensions.Logging/Extensions/Logging/LoggerProviderCollectionSink.cs @@ -0,0 +1,62 @@ +// 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; +using Serilog.Core; +using Serilog.Events; + +namespace Serilog.Extensions.Logging +{ + class LoggerProviderCollectionSink : ILogEventSink, IDisposable + { + readonly LoggerProviderCollection _providers; + + public LoggerProviderCollectionSink(LoggerProviderCollection providers) + { + _providers = providers ?? throw new ArgumentNullException(nameof(providers)); + } + + public void Emit(LogEvent logEvent) + { + string categoryName = null; + + if (logEvent.Properties.TryGetValue("SourceContext", out var sourceContextProperty) && + sourceContextProperty is ScalarValue sourceContextValue && + sourceContextValue.Value is string sourceContext) + { + categoryName = sourceContext; + } + + var level = LevelMapping.ToExtensionsLevel(logEvent.Level); + var slv = new SerilogLogValues(logEvent.MessageTemplate, logEvent.Properties); + + foreach (var provider in _providers.Providers) + { + var logger = provider.CreateLogger(categoryName); + + logger.Log( + level, + default, + slv, + logEvent.Exception, + (s, e) => s.ToString()); + } + } + + public void Dispose() + { + _providers.Dispose(); + } + } +} diff --git a/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLogValues.cs b/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLogValues.cs new file mode 100644 index 0000000..c6f8058 --- /dev/null +++ b/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLogValues.cs @@ -0,0 +1,62 @@ +// 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 Serilog.Events; +using System; +using System.Collections; +using System.Collections.Generic; + +namespace Serilog.Extensions.Logging +{ + readonly struct SerilogLogValues : IReadOnlyList> + { + // Note, this struct is only used in a very limited context internally, so we ignore + // the possibility of fields being null via the default struct initialization. + + private readonly MessageTemplate _messageTemplate; + private readonly IReadOnlyDictionary _properties; + private readonly KeyValuePair[] _values; + + public SerilogLogValues(MessageTemplate messageTemplate, IReadOnlyDictionary properties) + { + _messageTemplate = messageTemplate ?? throw new ArgumentNullException(nameof(messageTemplate)); + + // The dictionary is needed for rendering through the message template + _properties = properties ?? throw new ArgumentNullException(nameof(properties)); + + // The array is needed because the IReadOnlyList interface expects indexed access + _values = new KeyValuePair[_properties.Count + 1]; + var i = 0; + foreach (var p in properties) + { + _values[i] = new KeyValuePair(p.Key, (p.Value is ScalarValue sv) ? sv.Value : p.Value); + ++i; + } + _values[i] = new KeyValuePair("{OriginalFormat}", _messageTemplate.Text); + } + + public KeyValuePair this[int index] + { + get => _values[index]; + } + + public int Count => _properties.Count + 1; + + public IEnumerator> GetEnumerator() => ((IEnumerable>)_values).GetEnumerator(); + + public override string ToString() => _messageTemplate.Render(_properties); + + IEnumerator IEnumerable.GetEnumerator() => _values.GetEnumerator(); + } +} diff --git a/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLogger.cs b/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLogger.cs index 5dff16c..e81f79e 100644 --- a/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLogger.cs +++ b/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLogger.cs @@ -39,7 +39,7 @@ public SerilogLogger( public bool IsEnabled(LogLevel logLevel) { - return _logger.IsEnabled(ConvertLevel(logLevel)); + return _logger.IsEnabled(LevelMapping.ToSerilogLevel(logLevel)); } public IDisposable BeginScope(TState state) @@ -49,7 +49,7 @@ public IDisposable BeginScope(TState state) public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) { - var level = ConvertLevel(logLevel); + var level = LevelMapping.ToSerilogLevel(logLevel); if (!_logger.IsEnabled(level)) { return; @@ -133,27 +133,6 @@ static object AsLoggableValue(TState state, Func(2); diff --git a/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLoggerFactory.cs b/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLoggerFactory.cs new file mode 100644 index 0000000..cf25f06 --- /dev/null +++ b/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLoggerFactory.cs @@ -0,0 +1,76 @@ +// 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; +using Microsoft.Extensions.Logging; +using Serilog.Debugging; + +namespace Serilog.Extensions.Logging +{ + /// + /// A complete Serilog-backed implementation of the .NET Core logging infrastructure. + /// + public class SerilogLoggerFactory : ILoggerFactory + { + readonly LoggerProviderCollection _providerCollection; + readonly SerilogLoggerProvider _provider; + + /// + /// Initializes a new instance of the class. + /// + /// 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 , for use with WriteTo.Providers(). + public SerilogLoggerFactory(ILogger logger = null, bool dispose = false, LoggerProviderCollection providerCollection = null) + { + _provider = new SerilogLoggerProvider(logger, dispose); + _providerCollection = providerCollection; + } + + /// + /// Disposes the provider. + /// + public void Dispose() + { + _provider.Dispose(); + } + + /// + /// Creates a new instance. + /// + /// The category name for messages produced by the logger. + /// + /// The . + /// + public Microsoft.Extensions.Logging.ILogger CreateLogger(string categoryName) + { + return _provider.CreateLogger(categoryName); + } + + /// + /// Adds an to the logging system. + /// + /// The . + public void AddProvider(ILoggerProvider provider) + { + if (provider == null) throw new ArgumentNullException(nameof(provider)); + if (_providerCollection != null) + _providerCollection.AddProvider(provider); + else + SelfLog.WriteLine("Ignoring added logger provider {0}", provider); + } + } +} diff --git a/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLoggerProvider.cs b/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLoggerProvider.cs index 0a8910e..b4905ff 100644 --- a/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLoggerProvider.cs +++ b/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLoggerProvider.cs @@ -57,7 +57,7 @@ public FrameworkLogger CreateLogger(string name) return new SerilogLogger(this, _logger, name); } - /// + /// public IDisposable BeginScope(T state) { if (CurrentScope != null) @@ -66,7 +66,7 @@ public IDisposable BeginScope(T state) // The outermost scope pushes and pops the Serilog `LogContext` - once // this enricher is on the stack, the `CurrentScope` property takes care // of the rest of the `BeginScope()` stack. - var popSerilogContext = LogContext.PushProperties(this); + var popSerilogContext = LogContext.Push(this); return new SerilogLoggerScope(this, state, popSerilogContext); } diff --git a/src/Serilog.Extensions.Logging/LoggerSinkConfigurationExtensions.cs b/src/Serilog.Extensions.Logging/LoggerSinkConfigurationExtensions.cs new file mode 100644 index 0000000..85064db --- /dev/null +++ b/src/Serilog.Extensions.Logging/LoggerSinkConfigurationExtensions.cs @@ -0,0 +1,49 @@ +// 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; +using Serilog.Configuration; +using Serilog.Core; +using Serilog.Events; +using Serilog.Extensions.Logging; + +namespace Serilog +{ + /// + /// Extensions for . + /// + public static class LoggerSinkConfigurationExtensions + { + /// + /// Write Serilog events to the providers in . + /// + /// The `WriteTo` object. + /// A to write events to. + /// The minimum level for + /// events passed through the sink. Ignored when is specified. + /// A switch allowing the pass-through minimum level + /// to be changed at runtime. + /// A to allow method chaining. + public static LoggerConfiguration Providers( + this LoggerSinkConfiguration configuration, + LoggerProviderCollection providers, + LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum, + LoggingLevelSwitch levelSwitch = null) + { + if (configuration == null) throw new ArgumentNullException(nameof(configuration)); + if (providers == null) throw new ArgumentNullException(nameof(providers)); + return configuration.Sink(new LoggerProviderCollectionSink(providers), restrictedToMinimumLevel, levelSwitch); + } + } +} diff --git a/src/Serilog.Extensions.Logging/Serilog.Extensions.Logging.csproj b/src/Serilog.Extensions.Logging/Serilog.Extensions.Logging.csproj index dbf3073..eeaa59c 100644 --- a/src/Serilog.Extensions.Logging/Serilog.Extensions.Logging.csproj +++ b/src/Serilog.Extensions.Logging/Serilog.Extensions.Logging.csproj @@ -1,10 +1,10 @@ - + Low-level Serilog provider for Microsoft.Extensions.Logging - 2.0.5 + 3.0.0 Microsoft;Serilog Contributors - net45;net46;net461;netstandard1.3;netstandard2.0 + netstandard2.0 true true Serilog.Extensions.Logging @@ -20,32 +20,14 @@ git false Serilog + 7.3 - - - - - - - - + - - $(DefineConstants);ASYNCLOCAL - - - - $(DefineConstants);ASYNCLOCAL;LOGGING_BUILDER - - - - $(DefineConstants);ASYNCLOCAL - - $(DefineConstants);ASYNCLOCAL;LOGGING_BUILDER diff --git a/test/Serilog.Extensions.Logging.Tests/Serilog.Extensions.Logging.Tests.csproj b/test/Serilog.Extensions.Logging.Tests/Serilog.Extensions.Logging.Tests.csproj index 54eee14..7f8a2ff 100644 --- a/test/Serilog.Extensions.Logging.Tests/Serilog.Extensions.Logging.Tests.csproj +++ b/test/Serilog.Extensions.Logging.Tests/Serilog.Extensions.Logging.Tests.csproj @@ -1,15 +1,13 @@  - netcoreapp1.1;netcoreapp2.0;net46;net461 + netcoreapp2.0;net472 Serilog.Extensions.Logging.Tests ../../assets/Serilog.snk true true Serilog.Extensions.Logging.Tests true - $(PackageTargetFallback);dnxcore50;portable-net45+win8 - 1.0.4 @@ -22,9 +20,4 @@ - - - - - diff --git a/test/Serilog.Extensions.Logging.Tests/SerilogLogValuesTests.cs b/test/Serilog.Extensions.Logging.Tests/SerilogLogValuesTests.cs new file mode 100644 index 0000000..d5a777e --- /dev/null +++ b/test/Serilog.Extensions.Logging.Tests/SerilogLogValuesTests.cs @@ -0,0 +1,53 @@ +using Serilog.Events; +using Serilog.Parsing; +using System.Collections.Generic; +using System.Linq; +using Xunit; + +namespace Serilog.Extensions.Logging.Tests +{ + public class SerilogLogValuesTests + { + [Fact] + public void OriginalFormatIsExposed() + { + const string format = "Hello, {Name}!"; + var mt = new MessageTemplateParser().Parse(format); + var lv = new SerilogLogValues(mt, new Dictionary()); + var kvp = lv.Single(); + Assert.Equal("{OriginalFormat}", kvp.Key); + Assert.Equal(format, kvp.Value); + } + + [Fact] + public void ScalarPropertiesAreSimplified() + { + const string name = "Scalar"; + var scalar = 15; + var lv = new SerilogLogValues(MessageTemplate.Empty, new Dictionary { [name] = new ScalarValue(scalar) }); + var kvp = lv.Single(p => p.Key == name); + var sv = Assert.IsType(kvp.Value); + Assert.Equal(scalar, sv); + } + + [Fact] + public void NonscalarPropertiesAreWrapped() + { + const string name = "Sequence"; + var seq = new SequenceValue(Enumerable.Empty()); + var lv = new SerilogLogValues(MessageTemplate.Empty, new Dictionary { [name] = seq }); + var kvp = lv.Single(p => p.Key == name); + var sv = Assert.IsType(kvp.Value); + Assert.Equal(seq, sv); + } + + [Fact] + public void MessageTemplatesAreRendered() + { + const string format = "Hello, {Name}!"; + var mt = new MessageTemplateParser().Parse(format); + var lv = new SerilogLogValues(mt, new Dictionary { ["Name"] = new ScalarValue("World") }); + Assert.Equal("Hello, \"World\"!", lv.ToString()); + } + } +} diff --git a/test/Serilog.Extensions.Logging.Tests/SerilogLoggerTests.cs b/test/Serilog.Extensions.Logging.Tests/SerilogLoggerTests.cs index e57d4a8..a83ca0c 100644 --- a/test/Serilog.Extensions.Logging.Tests/SerilogLoggerTests.cs +++ b/test/Serilog.Extensions.Logging.Tests/SerilogLoggerTests.cs @@ -9,10 +9,10 @@ using System.IO; using System.Linq; using Serilog.Debugging; -using Serilog.Framework.Logging.Tests.Support; +using Serilog.Extensions.Logging.Tests.Support; using Xunit; -namespace Serilog.Extensions.Logging.Test +namespace Serilog.Extensions.Logging.Tests { public class SerilogLoggerTest { diff --git a/test/Serilog.Extensions.Logging.Tests/Support/DisposeTrackingLogger.cs b/test/Serilog.Extensions.Logging.Tests/Support/DisposeTrackingLogger.cs index a967fef..d833c0c 100644 --- a/test/Serilog.Extensions.Logging.Tests/Support/DisposeTrackingLogger.cs +++ b/test/Serilog.Extensions.Logging.Tests/Support/DisposeTrackingLogger.cs @@ -3,7 +3,7 @@ using Serilog.Core; using Serilog.Events; -namespace Serilog.Framework.Logging.Tests.Support +namespace Serilog.Extensions.Logging.Tests.Support { public class DisposeTrackingLogger : ILogger, IDisposable { diff --git a/test/Serilog.Extensions.Logging.Tests/Support/SerilogSink.cs b/test/Serilog.Extensions.Logging.Tests/Support/SerilogSink.cs index a0f6384..cd7d5d7 100644 --- a/test/Serilog.Extensions.Logging.Tests/Support/SerilogSink.cs +++ b/test/Serilog.Extensions.Logging.Tests/Support/SerilogSink.cs @@ -5,7 +5,7 @@ using Serilog.Core; using Serilog.Events; -namespace Serilog.Extensions.Logging.Test +namespace Serilog.Extensions.Logging.Tests.Support { public class SerilogSink : ILogEventSink {