diff --git a/src/NLog.Extensions.Logging/NLog.Extensions.Logging.csproj b/src/NLog.Extensions.Logging/NLog.Extensions.Logging.csproj index 12325d29..9983108e 100644 --- a/src/NLog.Extensions.Logging/NLog.Extensions.Logging.csproj +++ b/src/NLog.Extensions.Logging/NLog.Extensions.Logging.csproj @@ -56,7 +56,7 @@ For ASP.NET Core, use NLog.Web.AspNetCore: https://www.nuget.org/packages/NLog.W - + \ No newline at end of file diff --git a/src/NLog.Extensions.Logging/NLogLogger.cs b/src/NLog.Extensions.Logging/NLogLogger.cs index a38f192e..77fc5588 100644 --- a/src/NLog.Extensions.Logging/NLogLogger.cs +++ b/src/NLog.Extensions.Logging/NLogLogger.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using Microsoft.Extensions.Logging; namespace NLog.Extensions.Logging @@ -11,6 +12,7 @@ internal class NLogLogger : Microsoft.Extensions.Logging.ILogger private readonly Logger _logger; private readonly NLogProviderOptions _options; + internal const string OriginalFormatPropertyName = "{OriginalFormat}"; private static readonly object EmptyEventId = default(EventId); // Cache boxing of empty EventId-struct private static readonly object ZeroEventId = default(EventId).Id; // Cache boxing of zero EventId-Value private Tuple _eventIdPropertyNames; @@ -35,8 +37,8 @@ public void Log(Microsoft.Extensions.Logging.LogLevel logLevel, EventId } var message = formatter(state, exception); - //message arguments are not needed as it is already checked that the loglevel is enabled. - var eventInfo = LogEventInfo.Create(nLogLogLevel, _logger.Name, message); + var messageTemplate = _options.EnableStructuredLogging ? state as IReadOnlyList> : null; + LogEventInfo eventInfo = CreateLogEventInfo(nLogLogLevel, message, messageTemplate); eventInfo.Exception = exception; if (!_options.IgnoreEmptyEventId || eventId.Id != 0 || !string.IsNullOrEmpty(eventId.Name)) { @@ -58,9 +60,64 @@ public void Log(Microsoft.Extensions.Logging.LogLevel logLevel, EventId eventInfo.Properties[eventIdPropertyNames.Item3] = eventId.Name; eventInfo.Properties["EventId"] = idIsZero && eventId.Name == null ? EmptyEventId : eventId; } + _logger.Log(eventInfo); } + private LogEventInfo CreateLogEventInfo(LogLevel nLogLogLevel, string message, IReadOnlyList> parameterList) + { + if (parameterList != null && parameterList.Count > 1) + { + // More than a single parameter (last parameter is the {OriginalFormat}) + var firstParameterName = parameterList[0].Key; + if (!string.IsNullOrEmpty(firstParameterName)) + { + if (firstParameterName.Length != 1 || !char.IsDigit(firstParameterName[0])) + { +#if NETSTANDARD2_0 + var originalFormat = parameterList[parameterList.Count - 1]; + string originalMessage = null; + if (originalFormat.Key == OriginalFormatPropertyName) + { + // Attempt to capture original message with placeholders + originalMessage = originalFormat.Value as string; + } + + var messageTemplateParameters = new NLogMessageParameterList(parameterList, originalMessage != null); + var eventInfo = new LogEventInfo(nLogLogLevel, _logger.Name, originalMessage ?? message, messageTemplateParameters); + if (originalMessage != null) + { + eventInfo.Parameters = new object[messageTemplateParameters.Count + 1]; + for (int i = 0; i < messageTemplateParameters.Count; ++i) + eventInfo.Parameters[i] = messageTemplateParameters[i].Value; + eventInfo.Parameters[messageTemplateParameters.Count] = message; + eventInfo.MessageFormatter = (l) => (string)l.Parameters[l.Parameters.Length - 1]; + } + return eventInfo; +#else + var eventInfo = LogEventInfo.Create(nLogLogLevel, _logger.Name, message); + for (int i = 0; i < parameterList.Count; ++i) + { + var parameter = parameterList[i]; + if (string.IsNullOrEmpty(parameter.Key)) + break; // Skip capture of invalid parameters + + var parameterName = parameter.Key; + switch (parameterName[0]) + { + case '@': parameterName = parameterName.Substring(1); break; + case '$': parameterName = parameterName.Substring(1); break; + } + eventInfo.Properties[parameterName] = parameter.Value; + } + return eventInfo; +#endif + } + } + } + return LogEventInfo.Create(nLogLogLevel, _logger.Name, message); + } + /// /// Is logging enabled for this logger at this ? /// @@ -119,7 +176,7 @@ public IDisposable BeginScope(TState state) { throw new ArgumentNullException(nameof(state)); } - + return NestedDiagnosticsLogicalContext.Push(state); } } diff --git a/src/NLog.Extensions.Logging/NLogMessageParameterList.cs b/src/NLog.Extensions.Logging/NLogMessageParameterList.cs new file mode 100644 index 00000000..44de54d6 --- /dev/null +++ b/src/NLog.Extensions.Logging/NLogMessageParameterList.cs @@ -0,0 +1,126 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace NLog.Extensions.Logging +{ +#if NETSTANDARD2_0 + /// + /// Converts Microsoft Extension Logging ParameterList into NLog MessageTemplate ParameterList + /// + internal class NLogMessageParameterList : IList + { + private IReadOnlyList> _parameterList; + + public NLogMessageParameterList(IReadOnlyList> parameterList, bool includesOriginalMessage) + { + List> validParameterList = includesOriginalMessage ? null : new List>(); + for (int i = 0; i < parameterList.Count; ++i) + { + if (!string.IsNullOrEmpty(parameterList[i].Key) && (parameterList[i].Key != NLogLogger.OriginalFormatPropertyName || i == parameterList.Count - 1)) + { + if (validParameterList != null) + { + if (parameterList[i].Key != NLogLogger.OriginalFormatPropertyName) + validParameterList.Add(parameterList[i]); + } + } + else + { + if (validParameterList == null) + { + validParameterList = new List>(); + for (int j = 0; j < i; ++i) + validParameterList.Add(parameterList[j]); + } + } + } + if (validParameterList != null) + { + validParameterList.Add(new KeyValuePair()); + } + _parameterList = validParameterList ?? parameterList; + } + + public NLog.MessageTemplates.MessageTemplateParameter this[int index] + { + get + { + var parameter = _parameterList[index]; + var parameterName = parameter.Key; + NLog.MessageTemplates.CaptureType captureType = NLog.MessageTemplates.CaptureType.Normal; + switch (parameterName[0]) + { + case '@': + parameterName = parameterName.Substring(1); + captureType = NLog.MessageTemplates.CaptureType.Serialize; + break; + case '$': + parameterName = parameterName.Substring(1); + captureType = NLog.MessageTemplates.CaptureType.Stringify; + break; + } + return new NLog.MessageTemplates.MessageTemplateParameter(parameter.Key, parameter.Value, null, captureType); + } + set => throw new NotImplementedException(); + } + + public int Count => _parameterList.Count - 1; + + public bool IsReadOnly => true; + + public void Add(NLog.MessageTemplates.MessageTemplateParameter item) + { + throw new NotImplementedException(); + } + + public void Clear() + { + throw new NotImplementedException(); + } + + public bool Contains(NLog.MessageTemplates.MessageTemplateParameter item) + { + throw new NotImplementedException(); + } + + public void CopyTo(NLog.MessageTemplates.MessageTemplateParameter[] array, int arrayIndex) + { + throw new NotImplementedException(); + } + + public IEnumerator GetEnumerator() + { + return _parameterList.Take(_parameterList.Count - 1).Select(p => new NLog.MessageTemplates.MessageTemplateParameter(p.Key, p.Value, null)).GetEnumerator(); + } + + public int IndexOf(NLog.MessageTemplates.MessageTemplateParameter item) + { + throw new NotImplementedException(); + } + + public void Insert(int index, NLog.MessageTemplates.MessageTemplateParameter item) + { + throw new NotImplementedException(); + } + + public bool Remove(NLog.MessageTemplates.MessageTemplateParameter item) + { + throw new NotImplementedException(); + } + + public void RemoveAt(int index) + { + throw new NotImplementedException(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + } +#endif +} diff --git a/src/NLog.Extensions.Logging/NLogProviderOptions.cs b/src/NLog.Extensions.Logging/NLogProviderOptions.cs index 3d3c780d..ea963457 100644 --- a/src/NLog.Extensions.Logging/NLogProviderOptions.cs +++ b/src/NLog.Extensions.Logging/NLogProviderOptions.cs @@ -22,6 +22,11 @@ public class NLogProviderOptions /// default(EventId) public bool IgnoreEmptyEventId { get; set; } + /// + /// Attempt to capture parameter names and values and insert into -dictionary + /// + public bool EnableStructuredLogging { get; set; } + /// Initializes a new instance of the class. public NLogProviderOptions() { diff --git a/test/LoggerTests.cs b/test/LoggerTests.cs index 7864305f..eb30d13e 100644 --- a/test/LoggerTests.cs +++ b/test/LoggerTests.cs @@ -24,7 +24,6 @@ public void TestInit() var target = GetTarget(); Assert.Equal("NLog.Extensions.Logging.Tests.LoggerTests.Runner|DEBUG|init runner |0", target.Logs.FirstOrDefault()); - } [Fact] @@ -34,7 +33,24 @@ public void TestEventId() var target = GetTarget(); Assert.Equal("NLog.Extensions.Logging.Tests.LoggerTests.Runner|DEBUG|message with id |20", target.Logs.FirstOrDefault()); - + } + + [Fact] + public void TestParameters() + { + GetRunner().LogDebugWithParameters(); + + var target = GetTarget(); + Assert.Equal("NLog.Extensions.Logging.Tests.LoggerTests.Runner|DEBUG|message with id and 1 parameters |0", target.Logs.FirstOrDefault()); + } + + [Fact] + public void TestStructuredLogging() + { + GetRunner().LogDebugWithStructuredParameters(); + + var target = GetTarget(); + Assert.Equal("NLog.Extensions.Logging.Tests.LoggerTests.Runner|DEBUG|message with id and 1 parameters |01", target.Logs.FirstOrDefault()); } [Theory] @@ -152,12 +168,11 @@ private static IServiceProvider BuildDi() var serviceProvider = services.BuildServiceProvider(); var loggerFactory = serviceProvider.GetRequiredService(); - loggerFactory.AddNLog(); + loggerFactory.AddNLog(new NLogProviderOptions() { EnableStructuredLogging = true }); loggerFactory.ConfigureNLog("nlog.config"); return serviceProvider; } - public class Runner { private readonly ILogger _logger; @@ -167,7 +182,6 @@ public Runner(ILoggerFactory fac) _logger = fac.CreateLogger(); } - public void LogDebugWithId() { _logger.LogDebug(20, "message with id"); @@ -200,6 +214,16 @@ public void Log(Microsoft.Extensions.Logging.LogLevel logLevel, int eventId, Exc } } + public void LogDebugWithParameters() + { + _logger.LogDebug("message with id and {0} parameters", "1"); + } + + public void LogDebugWithStructuredParameters() + { + _logger.LogDebug("message with id and {ParameterCount} parameters", "1"); + } + public void LogWithScope() { using (_logger.BeginScope("scope1")) @@ -211,7 +235,6 @@ public void LogWithScope() public void Init() { _logger.LogDebug("init runner"); - } } } diff --git a/test/NLog.Extensions.Logging.Tests.csproj b/test/NLog.Extensions.Logging.Tests.csproj index d4a14c95..c7991f7a 100644 --- a/test/NLog.Extensions.Logging.Tests.csproj +++ b/test/NLog.Extensions.Logging.Tests.csproj @@ -22,7 +22,7 @@ - + diff --git a/test/nlog.config b/test/nlog.config index 6ef7b8bf..a5d9aeff 100644 --- a/test/nlog.config +++ b/test/nlog.config @@ -8,7 +8,7 @@ + layout="${logger}|${uppercase:${level}}|${message} ${exception}|${event-properties:EventId}${event-properties:ParameterCount}" />