Skip to content

Commit

Permalink
NLog EcsLayout - Added support for ProcessThreadName + MessageTemplate (
Browse files Browse the repository at this point in the history
  • Loading branch information
snakefoot authored Apr 4, 2024
1 parent 31528cd commit 1c91c00
Show file tree
Hide file tree
Showing 5 changed files with 194 additions and 71 deletions.
120 changes: 102 additions & 18 deletions src/Elastic.CommonSchema.NLog/EcsLayout.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@

using System;
using System.Collections.Generic;
using System.Runtime.Serialization;
using System.Text;
using System.Text.Json.Serialization;
using Elastic.CommonSchema.Serialization;
using NLog;
using NLog.Config;
using NLog.Layouts;
Expand Down Expand Up @@ -46,6 +49,8 @@ public EcsLayout()
{
IncludeEventProperties = true;

MessageTemplate = "${onhasproperties:${message:raw=true}}";

LogOriginCallSiteMethod = "${exception:format=method}";
LogOriginCallSiteFile = "${exception:format=source}";

Expand Down Expand Up @@ -101,7 +106,7 @@ protected override void InitializeLayout()
UrlUserName = "${aspnet-user-identity}";

if (!NLogApmLoaded())
ApmTraceId = "${scopeproperty:item=RequestId:whenEmpty=${aspnet-TraceIdentifier}}}";
ApmTraceId = "${scopeproperty:item=RequestId:whenEmpty=${aspnet-TraceIdentifier}}";
}

base.InitializeLayout();
Expand Down Expand Up @@ -212,6 +217,8 @@ private static bool NLogWeb4Registered() =>
[ArrayParameter(typeof(TargetPropertyWithContext), "metadata")]
public IList<TargetPropertyWithContext> Metadata { get; } = new List<TargetPropertyWithContext>();

/// <summary></summary>
public Layout MessageTemplate { get; set; }
/// <summary></summary>
public Layout ProcessExecutable { get; set; }
/// <summary></summary>
Expand All @@ -221,6 +228,8 @@ private static bool NLogWeb4Registered() =>
/// <summary></summary>
public Layout ProcessThreadId { get; set; }
/// <summary></summary>
public Layout ProcessThreadName { get; set; }
/// <summary></summary>
public Layout ProcessTitle { get; set; }

/// <summary></summary>
Expand Down Expand Up @@ -274,7 +283,7 @@ private static bool NLogWeb4Registered() =>
/// <inheritdoc cref="Layout.RenderFormattedMessage"/>
protected override void RenderFormattedMessage(LogEventInfo logEvent, StringBuilder target)
{
var ecsEvent = EcsDocument.CreateNewWithDefaults<EcsDocument>(logEvent.TimeStamp, logEvent.Exception, NlogEcsDocumentCreationOptions.Default);
var ecsEvent = EcsDocument.CreateNewWithDefaults<NLogEcsDocument>(logEvent.TimeStamp, logEvent.Exception, NlogEcsDocumentCreationOptions.Default);

// prefer tracing information set by Elastic APM
SetApmTraceId(ecsEvent, logEvent);
Expand All @@ -297,16 +306,23 @@ protected override void RenderFormattedMessage(LogEventInfo logEvent, StringBuil
ecsEvent.Http = GetHttp(logEvent);
ecsEvent.Url = GetUrl(logEvent);

var metadata = GetMetadata(logEvent) ?? new MetadataDictionary();
foreach(var kv in metadata)
ecsEvent.AssignField(kv.Key, kv.Value);
var messageTemplate = MessageTemplate?.Render(logEvent);
ecsEvent.MessageTemplate = !string.IsNullOrEmpty(messageTemplate) ? messageTemplate : null;

var metadata = GetMetadata(logEvent);
if (metadata?.Count > 0)
{
foreach (var kv in metadata)
ecsEvent.AssignField(kv.Key, kv.Value);
}

//Give any deriving classes a chance to enrich the event
EnrichEvent(logEvent, ref ecsEvent);
EcsDocument ecsDocument = ecsEvent;
EnrichEvent(logEvent, ref ecsDocument);
//Allow programmatic actions to enrich before serializing
EnrichAction?.Invoke(ecsEvent, logEvent);
EnrichAction?.Invoke(ecsDocument, logEvent);

ecsEvent.Serialize(target);
ecsDocument.Serialize(target);
}

private Service GetService(LogEventInfo logEventInfo)
Expand Down Expand Up @@ -423,8 +439,8 @@ private Log GetLog(LogEventInfo logEventInfo)
{
Level = logEventInfo.Level.ToString(),
Logger = logEventInfo.LoggerName,
OriginFunction = logOriginMethod,
OriginFileName = logOriginSourceFile,
OriginFunction = !string.IsNullOrEmpty(logOriginMethod) ? logOriginMethod : null,
OriginFileName = !string.IsNullOrEmpty(logOriginSourceFile) ? logOriginSourceFile : null,
OriginFileLine = logOriginSourceLineNo
};

Expand All @@ -444,7 +460,7 @@ private Log GetLog(LogEventInfo logEventInfo)

private string[] GetTags(LogEventInfo e)
{
if (Tags is null || Tags.Count == 0)
if (Tags.Count == 0)
return null;

if (Tags.Count == 1)
Expand All @@ -459,7 +475,10 @@ private string[] GetTags(LogEventInfo e)
var tag = targetPropertyWithContext.Layout.Render(e);
tags.AddRange(GetTagsSplit(tag));
}
return tags.ToArray();

return tags.Count > 0
? tags.ToArray()
: null;
}

private static string[] GetTagsSplit(string tags) =>
Expand All @@ -469,11 +488,11 @@ private static string[] GetTagsSplit(string tags) =>

private Labels GetLabels(LogEventInfo e)
{
if (Labels?.Count == 0)
if (Labels.Count == 0)
return null;

var labels = new Labels();
for (var i = 0; i < Labels?.Count; ++i)
for (var i = 0; i < Labels.Count; ++i)
{
var value = Labels[i].Layout?.Render(e);
if (!string.IsNullOrEmpty(value) || Labels[i].IncludeEmptyValue)
Expand Down Expand Up @@ -504,7 +523,7 @@ private Event GetEvent(LogEventInfo logEventInfo)
Severity = !string.IsNullOrEmpty(eventSeverity)
? long.Parse(eventSeverity)
: GetSysLogSeverity(logEventInfo.Level),
Timezone = TimeZoneInfo.Local.StandardName
Timezone = TimeZoneInfo.Local.StandardName,
};

if (!string.IsNullOrEmpty(eventDurationMs) && double.TryParse(eventDurationMs, System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture, out var durationMs))
Expand Down Expand Up @@ -544,21 +563,24 @@ private Process GetProcess(LogEventInfo logEventInfo)
var processTitle = ProcessTitle?.Render(logEventInfo);
var processExecutable = ProcessExecutable?.Render(logEventInfo);
var processThreadId = ProcessThreadId?.Render(logEventInfo);
var processThreadName = ProcessThreadName?.Render(logEventInfo);

if (string.IsNullOrEmpty(processId)
&& string.IsNullOrEmpty(processName)
&& string.IsNullOrEmpty(processTitle)
&& string.IsNullOrEmpty(processExecutable)
&& string.IsNullOrEmpty(processThreadId))
&& string.IsNullOrEmpty(processThreadId)
&& string.IsNullOrEmpty(processThreadName))
return null;

return new Process
{
Title = processTitle,
Name = processName,
Pid = !string.IsNullOrEmpty(processId) ? long.Parse(processId) : 0,
Pid = !string.IsNullOrEmpty(processId) ? long.Parse(processId) : null,
Executable = processExecutable,
ThreadId = !string.IsNullOrEmpty(processThreadId) ? long.Parse(processThreadId) : null
ThreadId = !string.IsNullOrEmpty(processThreadId) ? long.Parse(processThreadId) : null,
ThreadName = !string.IsNullOrEmpty(processThreadName) ? processThreadName : null,
};
}

Expand Down Expand Up @@ -726,5 +748,67 @@ private static void Populate(IDictionary<string, string> propertyBag, string key

propertyBag.Add(usedKey, value);
}

/// <summary>
/// A subclass of <see cref="EcsDocument"/> that adds additional properties related to Extensions logging.
/// <para>For instance it adds scope information to each logged event</para>
/// </summary>
[JsonConverter(typeof(EcsDocumentJsonConverterFactory))]
public class NLogEcsDocument : EcsDocument
{
// Custom fields; use capitalisation as per ECS
private const string MessageTemplatePropertyName = nameof(MessageTemplate);

/// <summary>
/// Custom field with the original template used to generate the message, with token placeholders
/// for inserted label values, e.g. "Unexpected error processing customer {CustomerId}."
/// </summary>
[JsonPropertyName(MessageTemplatePropertyName), DataMember(Name = MessageTemplatePropertyName)]
public string MessageTemplate { get; set; }

/// <summary>
/// If <see cref="TryRead" /> returns <c>true</c> this will be called with the deserialized <paramref name="value" />
/// </summary>
/// <param name="propertyName">The additional property <see cref="EcsDocumentJsonConverter" /> encountered</param>
/// <param name="value">
/// The deserialized boxed value you will have to manually unbox to the type that
/// <see cref="TryRead" /> set
/// </param>
/// <returns></returns>
protected override bool ReceiveProperty(string propertyName, object value) =>
propertyName switch
{
MessageTemplatePropertyName => null != (MessageTemplate = value as string),
_ => false
};

/// <summary>
/// If implemented in a subclass, this allows you to hook into <see cref="EcsDocumentJsonConverter" />
/// and make it aware of properties on a subclass of <see cref="EcsDocument" />.
/// If <paramref name="propertyName" /> is known, set <paramref name="type" /> to the correct type and return true.
/// </summary>
/// <param name="propertyName">The additional property that <see cref="EcsDocumentJsonConverter" /> encountered</param>
/// <param name="type">Set this to the type you wish to deserialize to</param>
/// <returns>Return true if <paramref name="propertyName" /> is handled</returns>
protected override bool TryRead(string propertyName, out Type type)
{
type = propertyName switch
{
MessageTemplatePropertyName => typeof(string),
_ => null
};
return type != null;
}

/// <summary>
/// Write any additional properties in your subclass during <see cref="EcsDocumentJsonConverter" /> serialization.
/// </summary>
/// <param name="write">An action taking a <c>property name</c> and <c>boxed value</c> to write to the output</param>
protected override void WriteAdditionalProperties(Action<string, object> write)
{
if (MessageTemplate != null)
write(MessageTemplatePropertyName, MessageTemplate);
}
}
}
}
73 changes: 42 additions & 31 deletions src/Elastic.CommonSchema.NLog/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,9 @@ filesystem target and [Elastic Filebeat](https://www.elastic.co/downloads/beats/
- _ProcessExecutable_ - Default: `${processname:FullName=true}`
- _ProcessId_ - Default: `${processid}`
- _ProcessName_ - Default: `${processname:FullName=false}`
- _ProcessThreadId_ - Default: `${threadid}`
- _ProcessTitle_ - Default: `${processinfo:MainWindowTitle}`
- _ProcessThreadId_ - Default: `${threadid}`
- _ProcessThreadName_ -

* **Server Options**
- _ServerAddress_ -
Expand Down Expand Up @@ -125,36 +126,46 @@ An example of the output is given below:

```json
{
"@timestamp":"2020-02-20T16:07:06.7109766+11:00",
"log.level":"Info",
"message":"Info \"X\" 2.2",
"metadata":{
"value_x":"X",
"some_y":2.2
},
"ecs":{
"version":"1.4.0"
},
"event":{
"severity":6,
"timezone":"AUS Eastern Standard Time",
"created":"2020-02-20T16:07:06.7109766+11:00"
},
"host":{
"name":"LAPTOP"
},
"log":{
"logger":"Elastic.CommonSchema.NLog",
"original":"Info {ValueX} {SomeY}"
},
"process":{
"thread":{
"id":17592
},
"pid":17592,
"name":"dotnet",
"executable":"C:\\Program Files\\dotnet\\dotnet.exe"
}
"@timestamp": "2020-02-20T16:07:06.7109766+11:00",
"log.level": "Info",
"message": "Info \"X\" 2.2",
"ecs.version": "8.6.0",
"log": {
"logger": "Elastic.CommonSchema.NLog.Tests.LogTestsBase",
},
"labels": {
"ValueX": "X",
"MessageTemplate": "Info {ValueX} {SomeY} {NotX}"
},
"agent": {
"type": "Elastic.CommonSchema.NLog",
"version": "1.6.0"
},
"event": {
"created": "2020-02-20T16:07:06.7109766+11:00",
"severity": 6,
"timezone": "Romance Standard Time"
},
"host": {
"ip": [ "127.0.0.1" ],
"name": "LOCALHOST"
},
"process": {
"executable": "C:\\Program Files\\dotnet\\dotnet.exe",
"name": "dotnet",
"pid": 17592,
"thread.id": 17592,
"title": "15.0.0.0"
},
"server": { "user": { "name": "MyUser" } },
"service": {
"name": "Elastic.CommonSchema",
"type": "dotnet",
"version": "1.6.0"
},
"metadata": {
"SomeY": 2.2
}
}
```

Expand Down
2 changes: 1 addition & 1 deletion src/Elastic.CommonSchema.Serilog/LogEventConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ private static MetadataDictionary GetMetadata(LogEvent logEvent, ISet<string>? l
dict.Add(logEventPropertyValue.Key, PropertyValueToObject(logEventPropertyValue.Value));
}

return dict.Count == 0 ? new MetadataDictionary() : dict;
return dict;
}

private static bool PropertyAlreadyMapped(string property)
Expand Down
Loading

0 comments on commit 1c91c00

Please sign in to comment.