Skip to content

Commit

Permalink
MicrosoftConsoleJsonLayout - Simulates Microsoft AddJsonConsole Forma…
Browse files Browse the repository at this point in the history
…tter (#555)
  • Loading branch information
snakefoot authored Nov 21, 2021
1 parent 0f63040 commit bce11f5
Show file tree
Hide file tree
Showing 4 changed files with 265 additions and 14 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -14,26 +14,43 @@ class MicrosoftConsoleLayoutRenderer : LayoutRenderer
{
private static readonly string[] EventIdMapper = Enumerable.Range(0, 50).Select(id => id.ToString(System.Globalization.CultureInfo.InvariantCulture)).ToArray();

/// <summary>
/// Gets or sets format string used to format timestamp in logging messages. Defaults to <c>null</c>.
/// </summary>
public string TimestampFormat
{
get => _timestampFormat;
set
{
_timestampFormat = value;
_timestampFormatString = string.IsNullOrEmpty(value) ? null : $"{{0:{value}}}";
}
}
private string _timestampFormat;
private string _timestampFormatString;

/// <summary>
/// Gets or sets indication whether or not UTC timezone should be used to format timestamps in logging messages. Defaults to <c>false</c>.
/// </summary>
public bool UseUtcTimestamp { get; set; }

/// <inheritdoc />
protected override void Append(StringBuilder builder, LogEventInfo logEvent)
{
string timestampFormat = _timestampFormat;
if (timestampFormat != null)
{
var timestamp = UseUtcTimestamp ? logEvent.TimeStamp.ToUniversalTime() : logEvent.TimeStamp;
builder.AppendFormat(UseUtcTimestamp ? System.Globalization.CultureInfo.InvariantCulture : System.Globalization.CultureInfo.CurrentCulture, _timestampFormatString, timestamp);
builder.Append(' ');
}

var microsoftLogLevel = ConvertLogLevel(logEvent.Level);
builder.Append(microsoftLogLevel);
builder.Append(": ");
builder.Append(logEvent.LoggerName);
builder.Append('[');
int eventId = 0;
if (logEvent.HasProperties && logEvent.Properties.TryGetValue("EventId_Id", out var eventIdValue))
{
if (eventIdValue is int)
eventId = (int)eventIdValue;
else if (!int.TryParse(eventIdValue?.ToString() ?? string.Empty, out eventId))
eventId = 0;
}
else
{
eventId = 0;
}
int eventId = LookupEventId(logEvent);
builder.Append(ConvertEventId(eventId));
builder.Append(']');
builder.Append(System.Environment.NewLine);
Expand All @@ -46,7 +63,21 @@ protected override void Append(StringBuilder builder, LogEventInfo logEvent)
}
}

static string ConvertEventId(int eventId)
private static int LookupEventId(LogEventInfo logEvent)
{
int eventId = 0;
if (logEvent.HasProperties && (logEvent.Properties.TryGetValue("EventId_Id", out var eventIdValue) || logEvent.Properties.TryGetValue("EventId", out eventIdValue)))
{
if (eventIdValue is int)
eventId = (int)eventIdValue;
else if (eventIdValue is Microsoft.Extensions.Logging.EventId eventIdStuct)
eventId = eventIdStuct.Id;
}

return eventId;
}

private static string ConvertEventId(int eventId)
{
if (eventId == 0)
return "0";
Expand All @@ -56,7 +87,7 @@ static string ConvertEventId(int eventId)
return eventId.ToString(System.Globalization.CultureInfo.InvariantCulture);
}

string ConvertLogLevel(LogLevel logLevel)
private static string ConvertLogLevel(LogLevel logLevel)
{
if (logLevel == LogLevel.Trace)
return "trce";
Expand Down
154 changes: 154 additions & 0 deletions src/NLog.Extensions.Logging/Layouts/MicrosoftConsoleJsonLayout.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
using System;
using System.Collections.Generic;
using System.Linq;
using NLog.Config;
using NLog.Layouts;

namespace NLog.Extensions.Logging
{
/// <summary>
/// Renders output that simulates Microsoft Json Console Formatter from AddJsonConsol
/// </summary>
[Layout("MicrosoftConsoleJsonLayout")]
[ThreadAgnostic]
public class MicrosoftConsoleJsonLayout : JsonLayout
{
private static readonly string[] EventIdMapper = Enumerable.Range(0, 50).Select(id => id.ToString(System.Globalization.CultureInfo.InvariantCulture)).ToArray();

/// <summary>
/// Initializes a new instance of the <see cref="MicrosoftConsoleJsonLayout" /> class.
/// </summary>
public MicrosoftConsoleJsonLayout()
{
Attributes.Add(new JsonAttribute("Timestamp", "${date:format=O:universalTime=true}"));
Attributes.Add(new JsonAttribute("EventId", Layout.FromMethod(evt => LookupEventId(evt), LayoutRenderOptions.ThreadAgnostic)) { Encode = false });
Attributes.Add(new JsonAttribute("LogLevel", Layout.FromMethod(evt => ConvertLogLevel(evt.Level), LayoutRenderOptions.ThreadAgnostic)));
Attributes.Add(new JsonAttribute("Category", "${logger}"));
Attributes.Add(new JsonAttribute("Message", "${message}"));
Attributes.Add(new JsonAttribute("Exception", "${replace-newlines:${exception:format=tostring,data}}"));
var stateJsonLayout = new JsonLayout() { IncludeEventProperties = true };
stateJsonLayout.ExcludeProperties.Add("EventId");
stateJsonLayout.ExcludeProperties.Add("EventId_Id");
stateJsonLayout.Attributes.Add(new JsonAttribute("{OriginalFormat}", "${message:raw=true}"));
Attributes.Add(new JsonAttribute("State", stateJsonLayout) { Encode = false });
}

/// <summary>
/// Gets the array of attributes for the "state"-section
/// </summary>
[ArrayParameter(typeof(JsonAttribute), "state")]
public IList<JsonAttribute> StateAttributes
{
get
{
var index = LookupNamedAttributeIndex("State");
return index >= 0 ? (Attributes[index]?.Layout as JsonLayout)?.Attributes : null;
}
}

/// <summary>
/// Gets or sets whether to include "scopes"-section
/// </summary>
public bool IncludeScopes
{
get => LookupNamedAttributeIndex("Scopes") >= 0;
set
{
var index = LookupNamedAttributeIndex("Scopes");
if (index >= 0)
{
if (!value)
Attributes.RemoveAt(index);
}
else if (value)
{
Attributes.Add(new JsonAttribute("Scopes", "${scopenested:format=@}") { Encode = false });
}
}
}

/// <summary>
/// Gets or sets whether to include "Timestamp"-section
/// </summary>
public string TimestampFormat
{
get
{
var index = LookupNamedAttributeIndex("Timestamp");
return index >= 0 ? ((Attributes[index].Layout as SimpleLayout)?.LayoutRenderers?.FirstOrDefault() as NLog.LayoutRenderers.DateLayoutRenderer)?.Format : null;
}
set
{
var index = LookupNamedAttributeIndex("Timestamp");
if (index >= 0)
{
Attributes.RemoveAt(index);
}

if (!string.IsNullOrEmpty(value))
{
Attributes.Insert(0, new JsonAttribute("Timestamp", $"${{date:format={value}:universalTime=true}}"));
}
}
}

private int LookupNamedAttributeIndex(string attributeName)
{
for (int i = 0; i < Attributes.Count; ++i)
{
if (attributeName.Equals(Attributes[i].Name, StringComparison.OrdinalIgnoreCase))
{
return i;
}
}
return -1;
}

private static string LookupEventId(LogEventInfo logEvent)
{
if (logEvent.HasProperties)
{
if (logEvent.Properties.TryGetValue("EventId", out var eventObject))
{
if (eventObject is int eventId)
return ConvertEventId(eventId);
else if (eventObject is Microsoft.Extensions.Logging.EventId eventIdStruct)
return ConvertEventId(eventIdStruct.Id);
}

if (logEvent.Properties.TryGetValue("EventId_Id", out var eventid) && eventid is int)
{
return ConvertEventId((int)eventid);
}
}

return "0";
}

private static string ConvertEventId(int eventId)
{
if (eventId == 0)
return "0";
else if (eventId > 0 || eventId < EventIdMapper.Length)
return EventIdMapper[eventId];
else
return eventId.ToString(System.Globalization.CultureInfo.InvariantCulture);
}

private static string ConvertLogLevel(LogLevel logLevel)
{
if (logLevel == LogLevel.Trace)
return nameof(Microsoft.Extensions.Logging.LogLevel.Trace);
else if (logLevel == LogLevel.Debug)
return nameof(Microsoft.Extensions.Logging.LogLevel.Debug);
else if (logLevel == LogLevel.Info)
return nameof(Microsoft.Extensions.Logging.LogLevel.Information);
else if (logLevel == LogLevel.Warn)
return nameof(Microsoft.Extensions.Logging.LogLevel.Warning);
else if (logLevel == LogLevel.Error)
return nameof(Microsoft.Extensions.Logging.LogLevel.Error);
else
return nameof(Microsoft.Extensions.Logging.LogLevel.Critical);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Extensions.Logging;
using Xunit;

namespace NLog.Extensions.Logging.Tests
{
public class MicrosoftConsoleJsonLayoutTests
{
[Fact]
public void MicrosoftConsoleJsonLayout_NullEvent()
{
var layout = new MicrosoftConsoleJsonLayout() { TimestampFormat = null };
var result = layout.Render(LogEventInfo.CreateNullEvent());
Assert.Contains("{ \"EventId\": 0, \"LogLevel\": \"Critical\", \"State\": { } }", result);
}

[Fact]
public void MicrosoftConsoleJsonLayout_ExceptionEvent()
{
var layout = new MicrosoftConsoleJsonLayout();
var exception = new ArgumentException("Test");
var eventId = 42;
var logEvent1 = new LogEventInfo(LogLevel.Error, "MyLogger", null, "Alert {EventId}", new object[] { eventId }, exception);
var result1 = layout.Render(logEvent1);
Assert.Equal($"{{ \"Timestamp\": \"{logEvent1.TimeStamp.ToUniversalTime().ToString("O")}\", \"EventId\": {eventId}, \"LogLevel\": \"Error\", \"Category\": \"MyLogger\", \"Message\": \"Alert {eventId}\", \"Exception\": \"{exception.ToString()}\", \"State\": {{ \"{{OriginalFormat}}\": \"Alert {{EventId}}\" }} }}", result1);
var logEvent2 = new LogEventInfo(LogLevel.Error, "MyLogger", null, "Alert {EventId_Id}", new object[] { eventId }, exception);
var result2 = layout.Render(logEvent2);
Assert.Equal($"{{ \"Timestamp\": \"{logEvent2.TimeStamp.ToUniversalTime().ToString("O")}\", \"EventId\": {eventId}, \"LogLevel\": \"Error\", \"Category\": \"MyLogger\", \"Message\": \"Alert {eventId}\", \"Exception\": \"{exception.ToString()}\", \"State\": {{ \"{{OriginalFormat}}\": \"Alert {{EventId_Id}}\" }} }}", result2);
}

[Fact]
public void MicrosoftConsoleJsonLayout_IncludeScopesEvent()
{
var logFactory = new LogFactory().Setup().LoadConfiguration(builder =>
{
var layout = new MicrosoftConsoleJsonLayout() { IncludeScopes = true };
builder.ForLogger().WriteTo(new NLog.Targets.MemoryTarget("test") { Layout = layout });
}).LogFactory;
var logger = logFactory.GetCurrentClassLogger();

var exception = new ArgumentException("Test");
var eventId = 42;
using var requestScope = logger.PushScopeNested("Request Started");
using var activityScope = logger.PushScopeNested("Activity Started");
var logEvent = new LogEventInfo(LogLevel.Error, null, null, "Alert {EventId}", new object[] { eventId }, exception);
logger.Log(logEvent);
var result = logFactory.Configuration.FindTargetByName<NLog.Targets.MemoryTarget>("test")?.Logs?.FirstOrDefault();
Assert.Equal($"{{ \"Timestamp\": \"{logEvent.TimeStamp.ToUniversalTime().ToString("O")}\", \"EventId\": {eventId}, \"LogLevel\": \"Error\", \"Category\": \"{typeof(MicrosoftConsoleJsonLayoutTests).FullName}\", \"Message\": \"Alert {eventId}\", \"Exception\": \"{exception.ToString()}\", \"State\": {{ \"{{OriginalFormat}}\": \"Alert {{EventId}}\" }}, \"Scopes\": [ \"Request Started\", \"Activity Started\" ] }}", result);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,17 @@ public void MicrosoftConsoleLayoutRenderer_ExceptionEvent()
var result = layoutRenderer.Render(new LogEventInfo(LogLevel.Error, "MyLogger", null, "Alert {EventId_Id}", new object[] { eventId }, exception));
Assert.Equal($"fail: MyLogger[{eventId}]{Environment.NewLine} Alert 42{Environment.NewLine}{exception}", result);
}

[Fact]
public void MicrosoftConsoleLayoutRenderer_TimestampFormat()
{
var timestampFormat = "hh:mm:ss";
var layoutRenderer = new MicrosoftConsoleLayoutRenderer() { TimestampFormat = timestampFormat };
var exception = new ArgumentException("Test");
var eventId = 42;
var logEvent = new LogEventInfo(LogLevel.Error, "MyLogger", null, "Alert {EventId}", new object[] { eventId }, exception);
var result1 = layoutRenderer.Render(logEvent);
Assert.Equal($"{logEvent.TimeStamp.ToString(timestampFormat)} fail: MyLogger[{eventId}]{Environment.NewLine} Alert 42{Environment.NewLine}{exception}", result1);
}
}
}

0 comments on commit bce11f5

Please sign in to comment.