-
Notifications
You must be signed in to change notification settings - Fork 151
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Added MicrosoftILoggerTarget for Azure Lambda Logging to ILogger (#260)
* MicrosoftLoggerTarget for Azure Lambda Logging to ILogger * ExtensionILoggerTarget for Azure Lambda Logging to ILogger * ExtensionILoggerTarget for Azure Lambda Logging to ILogger (TargetWithContext) * ExtensionILoggerTarget for Azure Lambda Logging to ILogger (Code review) * ExtensionILoggerTarget for Azure Lambda Logging to ILogger (Code review) * ExtensionILoggerTarget for Azure Lambda Logging to ILogger (Code review) * ExtensionILoggerTarget for Azure Lambda Logging to ILogger (rename) * ExtensionILoggerTarget for Azure Lambda Logging to ILogger (rename)
- Loading branch information
1 parent
96e6756
commit 2460b35
Showing
3 changed files
with
318 additions
and
22 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
181 changes: 181 additions & 0 deletions
181
src/NLog.Extensions.Logging/Targets/MicrosoftILoggerTarget.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,181 @@ | ||
using System; | ||
using System.Collections; | ||
using System.Collections.Generic; | ||
using System.Linq; | ||
using NLog.Layouts; | ||
using NLog.Targets; | ||
using EventId = Microsoft.Extensions.Logging.EventId; | ||
|
||
namespace NLog.Extensions.Logging | ||
{ | ||
/// <summary> | ||
/// Forwards NLog LogEvents to Microsoft ILogger-interface with support for NLog Layout-features | ||
/// </summary> | ||
[Target("MicrosoftILogger")] | ||
public class MicrosoftILoggerTarget : TargetWithContext | ||
{ | ||
private readonly Microsoft.Extensions.Logging.ILogger _logger; | ||
|
||
/// <summary> | ||
/// EventId forwarded to ILogger | ||
/// </summary> | ||
public Layout EventId { get; set; } | ||
|
||
/// <summary> | ||
/// EventId-Name forwarded to ILogger | ||
/// </summary> | ||
public Layout EventName { get; set; } | ||
|
||
/// <summary> | ||
/// Initializes a new instance of the <see cref="MicrosoftILoggerTarget" /> class. | ||
/// </summary> | ||
/// <param name="logger">Microsoft ILogger instance</param> | ||
public MicrosoftILoggerTarget(Microsoft.Extensions.Logging.ILogger logger) | ||
{ | ||
_logger = logger; | ||
Layout = "${message}"; | ||
OptimizeBufferReuse = true; | ||
} | ||
|
||
/// <summary> | ||
/// Converts NLog-LogEvent into Microsoft Extension Logging LogState | ||
/// </summary> | ||
/// <param name="logEvent"></param> | ||
protected override void Write(LogEventInfo logEvent) | ||
{ | ||
var logLevel = ConvertToLogLevel(logEvent.Level); | ||
if (!_logger.IsEnabled(logLevel)) | ||
return; | ||
|
||
var eventId = default(EventId); | ||
if (EventId != null) | ||
{ | ||
var eventIdValue = RenderLogEvent(EventId, logEvent); | ||
if (!string.IsNullOrEmpty(eventIdValue) && int.TryParse(eventIdValue, out int eventIdParsed) && eventIdParsed != 0) | ||
eventId = new EventId(eventIdParsed); | ||
} | ||
if (EventName != null) | ||
{ | ||
var eventNameValue = RenderLogEvent(EventName, logEvent); | ||
if (!string.IsNullOrEmpty(eventNameValue)) | ||
eventId = new EventId(eventId.Id, eventNameValue); | ||
} | ||
|
||
var layoutMessage = RenderLogEvent(Layout, logEvent); | ||
IDictionary<string, object> contextProperties = null; | ||
if (ContextProperties.Count > 0 || IncludeMdlc || IncludeMdc || IncludeGdc) | ||
{ | ||
contextProperties = GetContextProperties(logEvent); | ||
if (contextProperties?.Count == 0) | ||
contextProperties = null; | ||
} | ||
|
||
_logger.Log(ConvertToLogLevel(logEvent.Level), eventId, new LogState(logEvent, layoutMessage, contextProperties), logEvent.Exception, LogStateFormatter); | ||
} | ||
|
||
struct LogState : IReadOnlyList<KeyValuePair<string, object>> | ||
{ | ||
public readonly LogEventInfo LogEvent; | ||
public readonly string LayoutMessage; | ||
public readonly IDictionary<string, object> ContextProperties; | ||
|
||
public int Count => (LogEvent.HasProperties ? LogEvent.Properties.Count : 0) + (ContextProperties?.Count ?? 0) + 1; | ||
|
||
public KeyValuePair<string, object> this[int index] | ||
{ | ||
get | ||
{ | ||
if (LogEvent.HasProperties) | ||
{ | ||
if (TryGetPropertyFromIndex(LogEvent.Properties, CreateLogEventProperty, ref index, out var property)) | ||
return property; | ||
} | ||
if (ContextProperties != null) | ||
{ | ||
if (TryGetPropertyFromIndex(ContextProperties, p => p, ref index, out var property)) | ||
return property; | ||
} | ||
if (index != 0) | ||
throw new ArgumentOutOfRangeException(nameof(index)); | ||
return CreateOriginalFormatProperty(); | ||
} | ||
} | ||
|
||
public LogState(LogEventInfo logEvent, string layoutMessage, IDictionary<string, object> contextProperties) | ||
{ | ||
LogEvent = logEvent; | ||
LayoutMessage = layoutMessage; | ||
ContextProperties = contextProperties; | ||
} | ||
|
||
public IEnumerator<KeyValuePair<string, object>> GetEnumerator() | ||
{ | ||
IList<KeyValuePair<string, object>> originalMessage = new[] { CreateOriginalFormatProperty() }; | ||
IEnumerable<KeyValuePair<string, object>> allProperties = ContextProperties?.Concat(originalMessage) ?? originalMessage; | ||
if (LogEvent.HasProperties) | ||
{ | ||
allProperties = LogEvent.Properties.Select(prop => CreateLogEventProperty(prop)).Concat(allProperties); | ||
} | ||
return allProperties.GetEnumerator(); | ||
} | ||
|
||
IEnumerator IEnumerable.GetEnumerator() | ||
{ | ||
return GetEnumerator(); | ||
} | ||
|
||
private static bool TryGetPropertyFromIndex<TKey, TValue>(ICollection<KeyValuePair<TKey, TValue>> properties, Func<KeyValuePair<TKey, TValue>, KeyValuePair<string, object>> converter, ref int index, out KeyValuePair<string, object> property) | ||
{ | ||
if (index < properties.Count) | ||
{ | ||
foreach (var prop in properties) | ||
{ | ||
if (index-- == 0) | ||
{ | ||
property = converter(prop); | ||
return true; | ||
} | ||
} | ||
} | ||
else | ||
{ | ||
index -= properties.Count; | ||
} | ||
|
||
property = default(KeyValuePair<string, object>); | ||
return false; | ||
} | ||
|
||
private static KeyValuePair<string, object> CreateLogEventProperty(KeyValuePair<object, object> prop) | ||
{ | ||
return new KeyValuePair<string, object>(prop.Key.ToString(), prop.Value); | ||
} | ||
|
||
private KeyValuePair<string, object> CreateOriginalFormatProperty() | ||
{ | ||
return new KeyValuePair<string, object>(NLogLogger.OriginalFormatPropertyName, LogEvent.Message); | ||
} | ||
} | ||
|
||
static string LogStateFormatter(LogState logState, Exception _) | ||
{ | ||
return logState.LayoutMessage; | ||
} | ||
|
||
static Microsoft.Extensions.Logging.LogLevel ConvertToLogLevel(NLog.LogLevel logLevel) | ||
{ | ||
if (logLevel == NLog.LogLevel.Trace) | ||
return Microsoft.Extensions.Logging.LogLevel.Trace; | ||
else if (logLevel == NLog.LogLevel.Debug) | ||
return Microsoft.Extensions.Logging.LogLevel.Debug; | ||
else if (logLevel == NLog.LogLevel.Info) | ||
return Microsoft.Extensions.Logging.LogLevel.Information; | ||
else if (logLevel == NLog.LogLevel.Warn) | ||
return Microsoft.Extensions.Logging.LogLevel.Warning; | ||
else if (logLevel == NLog.LogLevel.Error) | ||
return Microsoft.Extensions.Logging.LogLevel.Error; | ||
else // if (logLevel == NLog.LogLevel.Fatal) | ||
return Microsoft.Extensions.Logging.LogLevel.Critical; | ||
} | ||
} | ||
} |
120 changes: 120 additions & 0 deletions
120
test/NLog.Extensions.Logging.Tests/MicrosoftILoggerTargetTests.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,120 @@ | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Linq; | ||
using Microsoft.Extensions.Logging; | ||
using Xunit; | ||
|
||
namespace NLog.Extensions.Logging.Tests | ||
{ | ||
public class MicrosoftILoggerTargetTests | ||
{ | ||
[Fact] | ||
public void SimpleILoggerMessageTest() | ||
{ | ||
var logFactory = new NLog.LogFactory(); | ||
var logConfig = new NLog.Config.LoggingConfiguration(); | ||
var ilogger = new TestLogger(); | ||
logConfig.AddRuleForAllLevels(new MicrosoftILoggerTarget(ilogger) { Layout = "${message}" }); | ||
logFactory.Configuration = logConfig; | ||
var logger = logFactory.GetCurrentClassLogger(); | ||
logger.Info("Hello World"); | ||
Assert.Equal("Hello World", ilogger.LastLogMessage); | ||
Assert.Single(ilogger.LastLogProperties); | ||
Assert.Equal("Hello World", ilogger.LastLogProperties[0].Value); | ||
} | ||
|
||
[Fact] | ||
public void FilterILoggerMessageTest() | ||
{ | ||
var logFactory = new NLog.LogFactory(); | ||
var logConfig = new NLog.Config.LoggingConfiguration(); | ||
var ilogger = new TestLogger(); | ||
logConfig.AddRuleForAllLevels(new MicrosoftILoggerTarget(ilogger) { Layout = "${message}" }); | ||
logFactory.Configuration = logConfig; | ||
var logger = logFactory.GetCurrentClassLogger(); | ||
logger.Debug("Hello World"); | ||
Assert.Equal(null, ilogger.LastLogMessage); | ||
} | ||
|
||
[Fact] | ||
public void StructuredILoggerMessageTest() | ||
{ | ||
var logFactory = new NLog.LogFactory(); | ||
var logConfig = new NLog.Config.LoggingConfiguration(); | ||
var ilogger = new TestLogger(); | ||
logConfig.AddRuleForAllLevels(new MicrosoftILoggerTarget(ilogger) { Layout = "${message}" }); | ||
logFactory.Configuration = logConfig; | ||
var logger = logFactory.GetCurrentClassLogger(); | ||
logger.Info("Hello {Planet}", "Earth"); | ||
Assert.Equal("Hello \"Earth\"", ilogger.LastLogMessage); | ||
Assert.Equal(2, ilogger.LastLogProperties.Count); | ||
Assert.Equal("Planet", ilogger.LastLogProperties[0].Key); | ||
Assert.Equal("Earth", ilogger.LastLogProperties[0].Value); | ||
Assert.Equal("Hello {Planet}", ilogger.LastLogProperties[1].Value); | ||
} | ||
|
||
[Fact] | ||
public void ContextPropertiesILoggerTest() | ||
{ | ||
var logFactory = new NLog.LogFactory(); | ||
var logConfig = new NLog.Config.LoggingConfiguration(); | ||
var ilogger = new TestLogger(); | ||
var target = new MicrosoftILoggerTarget(ilogger) { Layout = "${message}" }; | ||
target.ContextProperties.Add(new Targets.TargetPropertyWithContext() { Name = "ThreadId", Layout = "${threadid}" }); | ||
logConfig.AddRuleForAllLevels(target); | ||
logFactory.Configuration = logConfig; | ||
var logger = logFactory.GetCurrentClassLogger(); | ||
logger.Info("Hello {Planet}", "Earth"); | ||
Assert.Equal("Hello \"Earth\"", ilogger.LastLogMessage); | ||
Assert.Equal(3, ilogger.LastLogProperties.Count); | ||
Assert.Equal("Planet", ilogger.LastLogProperties[0].Key); | ||
Assert.Equal("Earth", ilogger.LastLogProperties[0].Value); | ||
Assert.Equal("ThreadId", ilogger.LastLogProperties[1].Key); | ||
Assert.Equal(System.Threading.Thread.CurrentThread.ManagedThreadId.ToString(), ilogger.LastLogProperties[1].Value); | ||
Assert.Equal("Hello {Planet}", ilogger.LastLogProperties[2].Value); | ||
} | ||
|
||
class TestLogger : Microsoft.Extensions.Logging.ILogger | ||
{ | ||
public Microsoft.Extensions.Logging.LogLevel LastLogLevel; | ||
public string LastLogMessage; | ||
public Exception LastLogException; | ||
public IList<KeyValuePair<string, object>> LastLogProperties; | ||
public Microsoft.Extensions.Logging.EventId LastLogEventId; | ||
|
||
public IDisposable BeginScope<TState>(TState state) | ||
{ | ||
return null; | ||
} | ||
|
||
public bool IsEnabled(Microsoft.Extensions.Logging.LogLevel logLevel) | ||
{ | ||
switch (logLevel) | ||
{ | ||
case Microsoft.Extensions.Logging.LogLevel.Trace: | ||
case Microsoft.Extensions.Logging.LogLevel.Debug: | ||
return false; | ||
} | ||
return true; | ||
} | ||
|
||
public void Log<TState>(Microsoft.Extensions.Logging.LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter) | ||
{ | ||
LastLogLevel = logLevel; | ||
LastLogEventId = eventId; | ||
LastLogMessage = formatter(state, exception); | ||
LastLogException = exception; | ||
var propertiesList = (state as IReadOnlyList<KeyValuePair<string, object>>); | ||
LastLogProperties = propertiesList?.ToList(); | ||
for (int i = 0; i < propertiesList?.Count; ++i) | ||
{ | ||
var property = propertiesList[i]; | ||
if (property.Key != LastLogProperties[i].Key) | ||
throw new ArgumentException($"Property key mismatch {LastLogProperties[i].Key} <-> {property.Key}"); | ||
if (property.Value != LastLogProperties[i].Value) | ||
throw new ArgumentException($"Property Value mismatch {LastLogProperties[i].Value} <-> {property.Value}"); | ||
} | ||
} | ||
} | ||
} | ||
} |