diff --git a/NLog.Extensions.Logging.sln b/NLog.Extensions.Logging.sln index cf14af65..631b3ddf 100644 --- a/NLog.Extensions.Logging.sln +++ b/NLog.Extensions.Logging.sln @@ -8,6 +8,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution ProjectSection(SolutionItems) = preProject appveyor.yml = appveyor.yml build.ps1 = build.ps1 + CHANGELOG.MD = CHANGELOG.MD README.md = README.md EndProjectSection EndProject diff --git a/build.ps1 b/build.ps1 index 90751251..b9abb9e0 100644 --- a/build.ps1 +++ b/build.ps1 @@ -2,8 +2,8 @@ # creates NuGet package at \artifacts dotnet --version -$versionPrefix = "1.4.0" -$versionSuffix = "" +$versionPrefix = "1.5.0" +$versionSuffix = "rc1" $versionFile = $versionPrefix + "." + ${env:APPVEYOR_BUILD_NUMBER} $versionProduct = $versionPrefix; if (-Not $versionSuffix.Equals("")) diff --git a/examples/NetCore2/ConsoleExample/ConsoleExample.csproj b/examples/NetCore2/ConsoleExample/ConsoleExample.csproj index b86064c9..c5eefde4 100644 --- a/examples/NetCore2/ConsoleExample/ConsoleExample.csproj +++ b/examples/NetCore2/ConsoleExample/ConsoleExample.csproj @@ -11,7 +11,6 @@ - diff --git a/examples/NetCore2/ConsoleExample/Program.cs b/examples/NetCore2/ConsoleExample/Program.cs index 8eda3269..931557a2 100644 --- a/examples/NetCore2/ConsoleExample/Program.cs +++ b/examples/NetCore2/ConsoleExample/Program.cs @@ -12,6 +12,7 @@ internal class Program private static void Main() { var logger = LogManager.GetCurrentClassLogger(); + try { var config = new ConfigurationBuilder() @@ -19,6 +20,8 @@ private static void Main() .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) .Build(); + LogManager.Configuration = new NLogLoggingConfiguration(config.GetSection("NLog")); + var servicesProvider = BuildDi(config); using (servicesProvider as IDisposable) { diff --git a/examples/NetCore2/ConsoleExample/appsettings.json b/examples/NetCore2/ConsoleExample/appsettings.json index daabc235..352cad2f 100644 --- a/examples/NetCore2/ConsoleExample/appsettings.json +++ b/examples/NetCore2/ConsoleExample/appsettings.json @@ -5,5 +5,43 @@ "ParseMessageTemplates": true, "CaptureMessageProperties": true } + }, + "NLog": { + "autoreload": true, + "internalLogLevel": "Info", + "internalLogFile": "c:/temp/console-example-internal.log", + "throwConfigExceptions": true, + "targets": { + "console": { + "type": "Console", + "layout": "${date}|${level:uppercase=true}|${message} ${exception:format=tostring}|${logger}|${all-event-properties}" + }, + "file": { + "type": "AsyncWrapper", + "target": { + "wrappedFile": { + "type": "File", + "fileName": "c:/temp/console-example.log", + "layout": { + "type": "JsonLayout", + "Attributes": [ + { "name": "timestamp", "layout": "${date:format=o}" }, + { "name": "level", "layout": "${level}" }, + { "name": "logger", "layout": "${logger}" }, + { "name": "message", "layout": "${message:raw=true}" }, + { "name": "properties", "encode": false, "layout": { "type": "JsonLayout", "includeallproperties": "true" } } + ] + } + } + } + } + }, + "rules": [ + { + "logger": "*", + "minLevel": "Trace", + "writeTo": "File,Console" + } + ] } } \ No newline at end of file diff --git a/src/NLog.Extensions.Logging/Config/NLogLoggingConfiguration.cs b/src/NLog.Extensions.Logging/Config/NLogLoggingConfiguration.cs new file mode 100644 index 00000000..8f461467 --- /dev/null +++ b/src/NLog.Extensions.Logging/Config/NLogLoggingConfiguration.cs @@ -0,0 +1,108 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Extensions.Configuration; +using NLog.Config; + +namespace NLog.Extensions.Logging +{ + /// + /// Configures NLog through Microsoft Extension Configuration section (Ex from appsettings.json) + /// + public class NLogLoggingConfiguration : LoggingConfigurationParser + { + private readonly Action _reloadConfiguration; + + private class LoggingConfigurationElement : ILoggingConfigurationElement + { + readonly IConfigurationSection _configurationSection; + readonly string _nameOverride; + + public bool AutoReload { get; private set; } + + public LoggingConfigurationElement(IConfigurationSection configurationSection, bool topElement, string nameOverride = null) + { + _configurationSection = configurationSection; + _nameOverride = nameOverride; + if (topElement && bool.TryParse(configurationSection["autoreload"], out bool autoreload)) + { + AutoReload = autoreload; + } + } + + public string Name => _nameOverride ?? _configurationSection.Key; + + public IEnumerable> Values + { + get + { + var children = _configurationSection.GetChildren(); + foreach (var child in children) + { + if (!child.GetChildren().Any()) + yield return new KeyValuePair(child.Key, child.Value); + } + if (_nameOverride != null) + yield return new KeyValuePair("name", _configurationSection.Key); + } + } + + public IEnumerable Children + { + get + { + var children = _configurationSection.GetChildren(); + foreach (var child in children) + { + var firstChildValue = child?.GetChildren()?.FirstOrDefault(); + if (firstChildValue != null) + { + if (_nameOverride == "target" && string.Equals(child.Key, "target", StringComparison.OrdinalIgnoreCase) && child.GetChildren().Count() == 1) + { + yield return new LoggingConfigurationElement(firstChildValue, false, "target"); + } + else + { + string nameOverride = null; + if (string.Equals(_configurationSection.Key, "targets", StringComparison.OrdinalIgnoreCase)) + nameOverride = "target"; + yield return new LoggingConfigurationElement(child, false, nameOverride); + } + } + } + } + } + } + + /// + /// Initializes a new instance of the class. + /// + /// Configuration section to be read + public NLogLoggingConfiguration(IConfigurationSection nlogConfig) + : this(nlogConfig, NLog.LogManager.LogFactory) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// Configuration section to be read + /// The to which to apply any applicable configuration values. + public NLogLoggingConfiguration(IConfigurationSection nlogConfig, LogFactory logFactory) + : base(logFactory) + { + _reloadConfiguration = (state) => LoadConfigurationSection((IConfigurationSection)state, true); + LoadConfigurationSection(nlogConfig, null); + } + + private void LoadConfigurationSection(IConfigurationSection nlogConfig, bool? autoReload) + { + var configElement = new LoggingConfigurationElement(nlogConfig, true); + LoadConfig(configElement, null); + if (autoReload ?? configElement.AutoReload) + { + nlogConfig.GetReloadToken().RegisterChangeCallback(_reloadConfiguration, nlogConfig); + } + } + } +} diff --git a/src/NLog.Extensions.Logging/Logging/NLogBeginScopeParser.cs b/src/NLog.Extensions.Logging/Logging/NLogBeginScopeParser.cs index 78407347..c57d1ee7 100644 --- a/src/NLog.Extensions.Logging/Logging/NLogBeginScopeParser.cs +++ b/src/NLog.Extensions.Logging/Logging/NLogBeginScopeParser.cs @@ -48,15 +48,7 @@ public static IDisposable CreateDiagnosticLogicalContext(T state) { try { -#if NETSTANDARD return NestedDiagnosticsLogicalContext.Push(state); // AsyncLocal has no requirement to be Serializable -#else - // TODO Add support for Net46 in NLog (AsyncLocal), then we only have to do this check for legacy Net451 (CallContext) - if (state?.GetType().IsSerializable ?? true) - return NestedDiagnosticsLogicalContext.Push(state); - else - return NestedDiagnosticsLogicalContext.Push(state.ToString()); // Support HostingLogScope, ActionLogScope, FormattedLogValues and others -#endif } catch (Exception ex) { @@ -67,39 +59,47 @@ public static IDisposable CreateDiagnosticLogicalContext(T state) private class ScopeProperties : IDisposable { - Stack _properties; - - /// - /// Properties, never null and lazy init - /// - Stack Properties => _properties ?? (_properties = new Stack()); + private readonly IDisposable _ndlcScope; + private readonly IDisposable _mldcScope; - public ScopeProperties(int initialCapacity = 0) + public ScopeProperties(IDisposable ndlcScope, IDisposable mldcScope) { - if (initialCapacity > 0) - _properties = new Stack(initialCapacity); + _ndlcScope = ndlcScope; + _mldcScope = mldcScope; } - public static ScopeProperties CaptureScopeProperties(IReadOnlyList> scopePropertyList) + private static IDisposable CreateScopeProperties(object scopeObject, IReadOnlyList> propertyList) { - ScopeProperties scope = new ScopeProperties(scopePropertyList.Count + 1); + if (propertyList?.Count > 0) + return new ScopeProperties(CreateDiagnosticLogicalContext(scopeObject), MappedDiagnosticsLogicalContext.SetScoped(propertyList)); + else + return CreateDiagnosticLogicalContext(scopeObject); + } - for (int i = 0; i < scopePropertyList.Count; ++i) + public static IDisposable CaptureScopeProperties(IReadOnlyList> scopePropertyList) + { + if (scopePropertyList.Count == 0 || scopePropertyList[scopePropertyList.Count - 1].Key != NLogLogger.OriginalFormatPropertyName) { - var property = scopePropertyList[i]; - if (i == scopePropertyList.Count - 1 && i > 0 && property.Key == NLogLogger.OriginalFormatPropertyName) - continue; // Handle BeginScope("Hello {World}", "Earth") - - scope.AddProperty(property.Key, property.Value); + return CreateScopeProperties(scopePropertyList, scopePropertyList); } + else + { + List> propertyList = new List>(scopePropertyList.Count - 1); + for (int i = 0; i < scopePropertyList.Count; ++i) + { + var property = scopePropertyList[i]; + if (i == scopePropertyList.Count - 1 && i > 0 && property.Key == NLogLogger.OriginalFormatPropertyName) + continue; // Handle BeginScope("Hello {World}", "Earth") - scope.AddDispose(CreateDiagnosticLogicalContext(scopePropertyList)); - return scope; + propertyList.Add(property); + } + return CreateScopeProperties(scopePropertyList, propertyList); + } } - public static ScopeProperties CaptureScopeProperties(System.Collections.IEnumerable scopePropertyCollection, ConcurrentDictionary, Func>> stateExractor) + public static IDisposable CaptureScopeProperties(System.Collections.IEnumerable scopePropertyCollection, ConcurrentDictionary, Func>> stateExractor) { - ScopeProperties scope = new ScopeProperties(); + List> propertyList = null; var keyValueExtractor = default(KeyValuePair, Func>); foreach (var property in scopePropertyCollection) @@ -113,11 +113,15 @@ public static ScopeProperties CaptureScopeProperties(System.Collections.IEnumera break; } - AddKeyValueProperty(scope, keyValueExtractor, property); + var propertyValue = TryParseKeyValueProperty(keyValueExtractor, property); + if (!propertyValue.HasValue) + continue; + + propertyList = propertyList ?? new List>(); + propertyList.Add(propertyValue.Value); } - scope.AddDispose(CreateDiagnosticLogicalContext(scopePropertyCollection)); - return scope; + return CreateScopeProperties(scopePropertyCollection, propertyList); } public static IDisposable CaptureScopeProperty(TState scopeProperty, ConcurrentDictionary, Func>> stateExractor) @@ -125,23 +129,28 @@ public static IDisposable CaptureScopeProperty(TState scopeProperty, Con if (!TryLookupExtractor(stateExractor, scopeProperty.GetType(), out var keyValueExtractor)) return CreateDiagnosticLogicalContext(scopeProperty); - var scope = new ScopeProperties(); - AddKeyValueProperty(scope, keyValueExtractor, scopeProperty); - scope.AddDispose(CreateDiagnosticLogicalContext(scopeProperty)); - return scope; + var propertyValue = TryParseKeyValueProperty(keyValueExtractor, scopeProperty); + if (!propertyValue.HasValue) + return CreateDiagnosticLogicalContext(scopeProperty); + + return new ScopeProperties(CreateDiagnosticLogicalContext(scopeProperty), MappedDiagnosticsLogicalContext.SetScoped(propertyValue.Value.Key, propertyValue.Value.Value)); } - private static void AddKeyValueProperty(ScopeProperties scope, KeyValuePair, Func> keyValueExtractor, object property) + private static KeyValuePair? TryParseKeyValueProperty(KeyValuePair, Func> keyValueExtractor, object property) { + string propertyName = null; + try { var propertyKey = keyValueExtractor.Key.Invoke(property); + propertyName = propertyKey?.ToString() ?? string.Empty; var propertyValue = keyValueExtractor.Value.Invoke(property); - scope.AddProperty(propertyKey?.ToString() ?? string.Empty, propertyValue); + return new KeyValuePair(propertyName, propertyValue); } catch (Exception ex) { - InternalLogger.Debug(ex, "Exception in BeginScope add property"); + InternalLogger.Debug(ex, "Exception in BeginScope add property {0}", propertyName); + return null; } } @@ -193,41 +202,29 @@ private static bool TryBuildExtractor(Type propertyType, out KeyValuePair 0) - { - try - { - property = properties.Pop(); - property.Dispose(); - } - catch (Exception ex) - { - InternalLogger.Debug(ex, "Exception in BeginScope dispose property {0}", property); - } - } + _mldcScope?.Dispose(); + } + catch (Exception ex) + { + InternalLogger.Debug(ex, "Exception in BeginScope dispose MappedDiagnosticsLogicalContext"); + } + try + { + _ndlcScope?.Dispose(); + } + catch (Exception ex) + { + InternalLogger.Debug(ex, "Exception in BeginScope dispose NestedDiagnosticsLogicalContext"); } } public override string ToString() { - return (_properties?.Count > 0 ? _properties.Peek()?.ToString() : null) ?? base.ToString(); + return _ndlcScope?.ToString() ?? base.ToString(); } } } diff --git a/src/NLog.Extensions.Logging/Logging/NLogLogger.cs b/src/NLog.Extensions.Logging/Logging/NLogLogger.cs index 46b9b986..5bc588b9 100644 --- a/src/NLog.Extensions.Logging/Logging/NLogLogger.cs +++ b/src/NLog.Extensions.Logging/Logging/NLogLogger.cs @@ -441,7 +441,7 @@ public IDisposable BeginScope(TState state) return NullScope.Instance; } - if (_beginScopeParser != null) + if (_options.CaptureMessageProperties) { return _beginScopeParser.ParseBeginScope(state) ?? NullScope.Instance; } diff --git a/src/NLog.Extensions.Logging/Logging/NLogLoggerProvider.cs b/src/NLog.Extensions.Logging/Logging/NLogLoggerProvider.cs index 86b92ca6..5e36d1ce 100644 --- a/src/NLog.Extensions.Logging/Logging/NLogLoggerProvider.cs +++ b/src/NLog.Extensions.Logging/Logging/NLogLoggerProvider.cs @@ -15,7 +15,7 @@ namespace NLog.Extensions.Logging #endif public class NLogLoggerProvider : Microsoft.Extensions.Logging.ILoggerProvider { - private NLogBeginScopeParser _beginScopeParser; + private readonly NLogBeginScopeParser _beginScopeParser; /// /// NLog options @@ -64,10 +64,7 @@ public NLogLoggerProvider(NLogProviderOptions options, LogFactory logFactory) /// New Logger public Microsoft.Extensions.Logging.ILogger CreateLogger(string name) { - var beginScopeParser = ((Options?.CaptureMessageProperties ?? true) && (Options?.IncludeScopes ?? true)) - ? (_beginScopeParser ?? System.Threading.Interlocked.CompareExchange(ref _beginScopeParser, new NLogBeginScopeParser(Options), null)) - : null; - return new NLogLogger(LogFactory.GetLogger(name), Options, beginScopeParser); + return new NLogLogger(LogFactory.GetLogger(name), Options, _beginScopeParser); } /// @@ -98,6 +95,7 @@ private static void RegisterHiddenAssembliesForCallSite() { InternalLogger.Debug("Hide assemblies for callsite"); LogManager.AddHiddenAssembly(typeof(NLogLoggerProvider).GetTypeInfo().Assembly); + NLog.Config.ConfigurationItemFactory.Default.RegisterItemsFromAssembly(typeof(NLogLoggerProvider).GetTypeInfo().Assembly); #if !NETCORE1_0 var allAssemblies = AppDomain.CurrentDomain.GetAssemblies(); diff --git a/src/NLog.Extensions.Logging/NLog.Extensions.Logging.csproj b/src/NLog.Extensions.Logging/NLog.Extensions.Logging.csproj index 3978eb4b..32f6a49b 100644 --- a/src/NLog.Extensions.Logging/NLog.Extensions.Logging.csproj +++ b/src/NLog.Extensions.Logging/NLog.Extensions.Logging.csproj @@ -18,13 +18,13 @@ For ASP.NET Core, use NLog.Web.AspNetCore: https://www.nuget.org/packages/NLog.W NLog;Microsoft.Extensions.Logging;log;logfiles;netcore +- Load NLog configuration from appsettings.json using ExtensionLoggingConfiguration - Updated to Microsoft.Extensions.Logging ver. 2.1.0 - ${configsetting} layout renderer now depends on AddNLog-call to provide IConfiguration-interface - NLogLoggerProvider options now binds to IConfiguration (Using section Logging:NLog) - Now builds snupkg packages Full changelog: https://github.com/NLog/NLog.Extensions.Logging/blob/master/CHANGELOG.MD - https://github.com/NLog/NLog.Extensions.Logging https://github.com/NLog/NLog.Extensions.Logging/blob/master/LICENSE @@ -62,7 +62,7 @@ Full changelog: https://github.com/NLog/NLog.Extensions.Logging/blob/master/CHAN $(DefineConstants);NETSTANDARD - + diff --git a/test/NLog.Extensions.Logging.Tests/ConfigSettingLayoutRendererTests.cs b/test/NLog.Extensions.Logging.Tests/ConfigSettingLayoutRendererTests.cs index cfe3972f..9e0a0f8e 100644 --- a/test/NLog.Extensions.Logging.Tests/ConfigSettingLayoutRendererTests.cs +++ b/test/NLog.Extensions.Logging.Tests/ConfigSettingLayoutRendererTests.cs @@ -15,7 +15,7 @@ public void ConfigSettingFallbackDefaultLookup() Assert.Equal("MyTableName", result); } -#if NETSTANDARD2_0 || NET461 +#if !NETCORE1_0 [Fact] public void ConfigSettingGlobalConfigLookup() { diff --git a/test/NLog.Extensions.Logging.Tests/NLog.Extensions.Logging.Tests.csproj b/test/NLog.Extensions.Logging.Tests/NLog.Extensions.Logging.Tests.csproj index 29ae213d..5f520c7b 100644 --- a/test/NLog.Extensions.Logging.Tests/NLog.Extensions.Logging.Tests.csproj +++ b/test/NLog.Extensions.Logging.Tests/NLog.Extensions.Logging.Tests.csproj @@ -14,6 +14,10 @@ true + + $(DefineConstants);NETCORE1_0 + + diff --git a/test/NLog.Extensions.Logging.Tests/NLogLoggingConfigurationTests.cs b/test/NLog.Extensions.Logging.Tests/NLogLoggingConfigurationTests.cs new file mode 100644 index 00000000..2d5419f1 --- /dev/null +++ b/test/NLog.Extensions.Logging.Tests/NLogLoggingConfigurationTests.cs @@ -0,0 +1,56 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.Extensions.Configuration; +using NLog.Targets; +using NLog.Targets.Wrappers; +using Xunit; + +namespace NLog.Extensions.Logging.Tests +{ + public class NLogLoggingConfigurationTests + { +#if !NETCORE1_0 + [Fact] + public void LoadSimpleConfig() + { + var memoryConfig = new Dictionary(); + memoryConfig["NLog:Targets:file:type"] = "File"; + memoryConfig["NLog:Targets:file:fileName"] = "hello.txt"; + memoryConfig["NLog:Targets:console:type"] = "Console"; + memoryConfig["NLog:Rules:0:logger"] = "*"; + memoryConfig["NLog:Rules:0:minLevel"] = "Trace"; + memoryConfig["NLog:Rules:0:writeTo"] = "File,Console"; + var configuration = new ConfigurationBuilder().AddInMemoryCollection(memoryConfig).Build(); + var logFactory = new LogFactory(); + var logConfig = new NLogLoggingConfiguration(configuration.GetSection("NLog"), logFactory); + Assert.Single(logConfig.LoggingRules); + Assert.Equal(2, logConfig.LoggingRules[0].Targets.Count); + Assert.Equal(2, logConfig.AllTargets.Count); + Assert.Single(logConfig.AllTargets.Where(t => t is FileTarget)); + Assert.Single(logConfig.AllTargets.Where(t => t is ConsoleTarget)); + } + + [Fact] + public void LoadWrapperConfig() + { + var memoryConfig = new Dictionary(); + memoryConfig["NLog:Targets:file:type"] = "AsyncWrapper"; + memoryConfig["NLog:Targets:file:target:wrappedFile:type"] = "File"; + memoryConfig["NLog:Targets:file:target:wrappedFile:fileName"] = "hello.txt"; + memoryConfig["NLog:Targets:console:type"] = "Console"; + memoryConfig["NLog:Rules:0:logger"] = "*"; + memoryConfig["NLog:Rules:0:minLevel"] = "Trace"; + memoryConfig["NLog:Rules:0:writeTo"] = "File,Console"; + var configuration = new ConfigurationBuilder().AddInMemoryCollection(memoryConfig).Build(); + var logFactory = new LogFactory(); + var logConfig = new NLogLoggingConfiguration(configuration.GetSection("NLog"), logFactory); + Assert.Single(logConfig.LoggingRules); + Assert.Equal(2, logConfig.LoggingRules[0].Targets.Count); + Assert.Equal(3, logConfig.AllTargets.Count); + Assert.Single(logConfig.AllTargets.Where(t => t is AsyncTargetWrapper)); + Assert.Single(logConfig.AllTargets.Where(t => t is FileTarget)); + Assert.Single(logConfig.AllTargets.Where(t => t is ConsoleTarget)); + } +#endif + } +}