Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Layout NLog properties as JSON?

I am trying to figure out how to log all the properties in a LogEventInfo object to a JSON-formatted string. Per the issue on github, I tried to do something like this:

<target xsi:type="ColoredConsole" name="coloredConsole">
  <layout xsi:type="JsonLayout">
    <attribute name="timestamp" layout="${longdate}"/>
    <attribute name="level" layout="${level:uppercase=true}"/>
    <attribute name="exception" layout="${onexception:${exception:format=tostring}}" />
    <attribute name="properties" encode="false">
      <layout type="JsonLayout">
        <attribute name="properties" layout="${all-event-properties}" />
      </layout>
    </attribute>
  </layout>
</target>

... but unfortunately, my properties contain complex objects (I have two properties with the names of "properties" and "tags", where "properties" is a IDictionary<string, object> and "tags" is an IList<string> in the LogEventInfo.Properties property) that simply do not serialize. I end up with something resembling this:

{ "timestamp": "2017-05-18 08:41:28.7730", "level": "INFO", "properties": { "properties": "properties=System.Collections.Generic.Dictionary`2[System.String,System.Object], tags=System.Collections.Generic.List`1[System.String]" } }

I was expecting (and hoping for) a serialized JSON dictionary that would give me the context of the log message, but clearly that is not what I am getting.

How can I properly serialize the properties in my LogEventInfo object?

like image 794
Jeremy Holovacs Avatar asked May 18 '17 12:05

Jeremy Holovacs


2 Answers

Using NLog 4.5.1 here.

With this config:

 <target xsi:type="File" name="jsonFile2" fileName="c:\temp\nlog-json-nested-${shortdate}.log">
     <layout type="JsonLayout">
         <attribute name="time" layout="${longdate}" />
         <attribute name="level" layout="${level}" />
         <attribute name="message" layout="${message}" />
         <attribute name="eventProperties" encode="false" >
             <layout type='JsonLayout' includeAllProperties="true"  maxRecursionLimit="20"/>
         </attribute>
     </layout>
 </target>

this code:

var nestedData = new
{
    A = "a value",
    B = "b value",
    Nested = new
    {
        A = "nested a value",
        B = "nested b value",
    },
    Ints = new Collection<int> { 1, 2, 3},
    Dictionary = new Dictionary<string, object>
    {
        {"nested", new { X= 'x', y = 'y' }},
        { "awesome", "nlog"}
    }
};
LogEventInfo eventInfo = new LogEventInfo
{
    Level = LogLevel.Info,
    Properties = { {"nestedData", nestedData } }
};
logger.Log(eventInfo);

outputs:

{  
   "time":"2018-04-05 18:08:01.0813",
   "level":"INFO",
   "eventProperties":{  
      "nestedData":{  
         "A":"a value",
         "B":"b value",
         "Nested":{  
            "A":"nested a value",
            "B":"nested b value"
         },
         "Ints":[  
            1,
            2,
            3
         ],
         "Dictionary":{  
            "nested":{  
               "X":"x",
               "y":"y"
            },
            "awesome":"nlog"
         }
      }
   }
}

Actually, it prints the ugly one-line version of this.

See Nested JSON with structured logging on the NLog wiki

Pay attention to the maxRecursionLimit setting. By default it's 0, meaning "No object reflection", meaning you'll get the ToString() representation of your event properties.

like image 55
Călin Darie Avatar answered Sep 23 '22 16:09

Călin Darie


Well, it looks like there's a bug in NLog, so I kinda made my own renderer. Here's what I did. First, I made an extension method using JSON.NET:

public static string ToJson(this object obj, bool format = false, string dateFormat = null)
{
    var settings = new JsonSerializerSettings
    {
        NullValueHandling = NullValueHandling.Ignore
    };

    if (!String.IsNullOrWhiteSpace(dateFormat))
    {
        settings.Converters = new List<JsonConverter>
        {
            new IsoDateTimeConverter {DateTimeFormat = dateFormat}
        };

        return JsonConvert.SerializeObject(obj, format ? Formatting.Indented : Formatting.None, settings);
    }

    return JsonConvert.SerializeObject(obj, format ? Formatting.Indented : Formatting.None, settings);
}

...next, I created a LayoutRenderer like so:

[LayoutRenderer("json-event-properties")]
public class JsonEventPropertiesLayoutRenderer : LayoutRenderer
{
    /// <summary>
    /// Renders the specified environmental information and appends it to the specified <see cref="T:System.Text.StringBuilder" />.
    /// </summary>
    /// <param name="builder">The <see cref="T:System.Text.StringBuilder" /> to append the rendered data to.</param>
    /// <param name="logEvent">Logging event.</param>
    protected override void Append(StringBuilder builder, LogEventInfo logEvent) {
        if (logEvent.Properties == null || logEvent.Properties.Count == 0)
            return;
        var serialized = logEvent.Properties.ToJson();
        builder.Append(serialized);
    }
}

In my application, when starting up, I registered my LayoutRenderer like so:

LayoutRenderer.Register<JsonEventPropertiesLayoutRenderer>("json-event-properties");

... and finally, I configured the NLog target like this:

    <layout xsi:type="JsonLayout">
    <attribute name="timestamp" layout="${longdate}"/>
    <attribute name="level" layout="${level:uppercase=true}"/>
    <attribute name="exception" layout="${onexception:${exception:format=tostring}}" />
    <attribute name="message" layout="${message}" />
    <attribute name="properties" layout="${json-event-properties}" encode="false"/>
  </layout>

Running like this, the JSON properly formatted and I got access to the properties in my LogEventInfo object.

like image 22
Jeremy Holovacs Avatar answered Sep 22 '22 16:09

Jeremy Holovacs