首页 > 解决方案 > 如何使用 NLog 将未指定的对象记录到保持 ELK Stack 兼容性的文件中?

问题描述

数周以来,我一直在努力让 NLog 以与 ELK 堆栈兼容的格式记录我的通信数据,包括非特定(DataContracts)参数。它需要在运行时配置,如果输出参数可以限制为 MAX 字符或深度,则首选它。

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

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

  1. 我可以像这样在每个类(Datacontract)上使用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数据来自哪里,但它感觉不是一个最佳解决方案。

ValueFormatter如您在此处看到的,实际上让 NLog 输出过滤器的数据也存在问题。仍然运行,ValueFormatter但它的常规 NLog JSON 数据将最终出现在文件中。

我需要的 NLog 是这样的:

我的数据通过 IParameterInspector 进入,它被编译成一个特殊的 CallInformation 类,该类也包含参数(类型对象)。参数可以随着多层而变得复杂。整个 CallInformation 对象像这样发送到 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>

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

标签: c#loggingelastic-stacknlog

解决方案


我认为 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);
        }        

推荐阅读