如何使用 NLog 将未指定的对象记录到保持 ELK 堆栈兼容性的文件?



我数周来一直在努力让 NLog 记录我的通信数据,包括不特定的(数据合同(参数,以 ELK 堆栈兼容的格式文件。它需要在运行时进行配置,如果输出参数可以像 MAX 字符或深度一样受到限制,则需要在运行时配置它的首选。

NLog 有一个内置的 JSON 序列化程序,但它只会读取没有默认值的属性,字段将被忽略,如下所示。调整我的数据模型将是一项艰巨的工作,我真的不认为这是正确的方法,NLog 不应该影响数据模型的外观。

有几种方法可以添加自定义 JSON 序列化程序:

  1. 我可以像这样在每个类(数据合同(上使用 SetupSerialization:

    LogManager.Setup().SetupSerialization(s =>
    s.RegisterObjectTransformation<GetEntityViewRequest>(obj => 
    return Newtonsoft.Json.Linq.JToken.FromObject(obj)
    )
    );
    

因为我想记录所有通信数据,所以必须注册整个数据模型,它的工作巨大且无效。

  1. 我可以使用自定义 IValueFormatter,但它不能只添加到我的通信 NLog 实例中,它必须全局添加到所有记录器中,如下所示:

    NLog.Config.ConfigurationItemFactory.Default.ValueFormatter = new NLogValueFormatter();
    

因此,IValueFormatter需要过滤,以便仅操作来自通信记录器的数据。我可能需要用一个标志将我的数据包装在一个类中,该标志告诉IValueFormatter数据来自哪里,但是它确实不像是最佳解决方案。

实际上,让NLog 发布ValueFormatter过滤的数据也存在问题,如下所示。ValueFormatter仍在运行,但最终将出现在文件中的是常规 NLog JSON 数据。

我需要从 NLog 得到的是这个:

  • 将所有通信数据(包括参数(从对象转换为字符串格式,以便 ELK 堆栈读取它。
  • 参数的序列化深度或字符串 MAX 长度,以避免数据溢出
  • 可在运行时从 NLog.config 进行配置(如 NLog 一样(
  • 仅影响特定的 NLogger 实例

我的数据是通过IParameterInspector进入的,它被编译成一个特殊的CallInformation类,该类还包含参数(类型对象(。参数可以因多层而异。整个 CallInforamtion 对象被发送到 NLog,如下所示:

_comLogger.Log(LogLevel.Info, "ComLogger : {@callInfo}", callInfo);

Nlog.config现在看起来像这样:

<logger name="CommunicationLogger" minlevel="Trace" writeto="communicationFileLog"></logger>
<target xsi:type="File"
name="communicationFileLog"
fileName="${basedir}/logs/${shortdate}.log"
maxArchiveDays="5"
maxArchiveFiles="10">
<layout xsi:type="JsonLayout" includeAllProperties="true" maxRecursionLimit="1">
</layout>
</target>

我错过了什么?是否有其他日志库可以更好地支持我的需求?

我认为 Rolf 的建议是最好的 - 创建一个将使用 JSON.NET 的布局。那个可以做所有花哨的技巧,比如序列化字段和处理[JsonIgnore]

基本版本如下所示:

using System.Collections.Generic;
using Newtonsoft.Json;
using NLog;
using NLog.Config;
using NLog.Layouts;
namespace MyNamespace
{
/// <summary>
/// Render all properties to Json with JSON.NET, also include message and exception
/// </summary>
[Layout("JsonNetLayout")]
[ThreadAgnostic] // different thread, same result
[ThreadSafe]
public class JsonNetLayout : Layout
{
public Formatting Formatting { get; set; } = Formatting.Indented; // This option could be set from the XML config
/// <inheritdoc />
protected override string GetFormattedMessage(LogEventInfo logEvent)
{
var allProperties = logEvent.Properties ?? new Dictionary<object, object>();
allProperties["message"] = logEvent.FormattedMessage;
if (logEvent.Exception != null)
{
allProperties["exception"] = logEvent.Exception.ToString(); //toString to prevent too much data properties
}
return JsonConvert.SerializeObject(allProperties, Formatting);
}
}
}

并注册布局,我将使用:

Layout.Register<JsonNetLayout>("JsonNetLayout"); // namespace NLog.Layouts

所需的配置:

<target xsi:type="File"
name="communicationFileLog"
fileName="${basedir}/logs/${shortdate}.log"
maxArchiveDays="5"
maxArchiveFiles="10">
<layout xsi:type="JsonNetLayout" />
</target>

记录此对象时:

public class ObjectWithFieldsAndJsonStuff
{
[JsonProperty]
private string _myField = "value1";
[JsonProperty("newname")]
public string FieldWithName { get; set; } = "value2";
[JsonIgnore]
public string IgnoreMe { get; set; } = "value3";
}

而这个记录器调用:

logger
.WithProperty("prop1", "value1")
.WithProperty("prop2", objectWithFieldsAndJsonStuff)
.Info("Hi");

这将导致:

{
"prop1": "value1",
"prop2": {
"_myField": "value1",
"newname": "value2"
},
"message": "Hi"
}

单元测试

以上所有这一切都在单元测试中 - 使用 xUnit

[Fact]
public void JsonNetLayoutTest()
{
// Arrange
Layout.Register<JsonNetLayout>("JsonNetLayout");
var xmlConfig = @"
<nlog xmlns=""http://www.nlog-project.org/schemas/NLog.xsd""
xmlns:xsi=""http://www.w3.org/2001/XMLSchema-instance"" 
throwExceptions=""true"">
<targets>
<target xsi:type=""Memory"" name=""target1"" >
<layout xsi:type=""JsonNetLayout"" />
</target>
</targets>
<rules>
<logger name=""*"" minlevel=""Trace"" writeTo=""target1"" />
</rules>
</nlog>
";
LogManager.Configuration = XmlLoggingConfiguration.CreateFromXmlString(xmlConfig);
var logger = LogManager.GetLogger("logger1");
var memoryTarget = LogManager.Configuration.FindTargetByName<MemoryTarget>("target1");
// Act
var objectWithFieldsAndJsonStuff = new ObjectWithFieldsAndJsonStuff();
logger
.WithProperty("prop1", "value1")
.WithProperty("prop2", objectWithFieldsAndJsonStuff)
.Info("Hi");
// Assert
var actual = memoryTarget.Logs.Single();
var expected =
@"{
""prop1"": ""value1"",
""prop2"": {
""_myField"": ""value1"",
""newname"": ""value2""
},
""message"": ""Hi""
}";
Assert.Equal(expected, actual);
}        

最新更新