Search code examples
c#.netnlogformatter

Set ValueFormatter for specific NLog instance?


I have 2 NLog instances where one needs a special ValueFormatter to serialize parameters. The ValueFormatter is set with this code :

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

So as you can see it will be applied to all loggers. I can´t find any property on the NLogger itself that might take a ValueFormatter.

Is there any way to bind this ValueFormatter just to one of the loggers?

Edit 1:

private CommunicationFormatProvider provider = new CommunicationFormatProvider();
        public void LogCommunication(string message, params object[] args)
        {
            _comLogger.Log(LogLevel.Info, provider,  message, args);
        }

public class CommunicationFormatProvider : IFormatProvider, ICustomFormatter
{
    public string Format(string format, object arg, IFormatProvider formatProvider)
    {
        StringBuilder strBuilder = new StringBuilder();
        strBuilder.Append(format);

        var myTarget = LogManager.Configuration.FindTargetByName("communicationTarget");
        myTarget = ((myTarget as NLog.Targets.Wrappers.WrapperTargetBase)?.WrappedTarget) ?? myTarget;
        var jsonLayout = (myTarget as NLog.Targets.TargetWithLayout)?.Layout as NLog.Layouts.JsonLayout;

        if (jsonLayout?.MaxRecursionLimit > 0)
            strBuilder.Append(JsonConvert.SerializeObject(arg, new JsonSerializerSettings() { MaxDepth = jsonLayout?.MaxRecursionLimit }));

        return strBuilder.ToString();
    }

    object IFormatProvider.GetFormat(Type formatType)
    {
        return (formatType == typeof(ICustomFormatter)) ? this : null;
    }
}

EDIT 2 :

public bool FormatValue(object value, string format, CaptureType captureType, IFormatProvider formatProvider, StringBuilder builder)
{

    if (value.GetType() == typeof(LogData))
        return false;

    builder.Append(format);

    try
    {

        var myTarget = LogManager.Configuration.FindTargetByName("communicationTarget");
        myTarget = ((myTarget as NLog.Targets.Wrappers.WrapperTargetBase)?.WrappedTarget) ?? myTarget;
        var jsonLayout = (myTarget as NLog.Targets.TargetWithLayout)?.Layout as NLog.Layouts.JsonLayout;

        if (jsonLayout?.MaxRecursionLimit > 0)
        {
            var jsonSettings = new JsonSerializerSettings() { MaxDepth = jsonLayout?.MaxRecursionLimit };
            using (var stringWriter = new StringWriter())
            {
                using (var jsonWriter = new JsonTextWriterMaxDepth(stringWriter, jsonSettings))
                    JsonSerializer.Create(jsonSettings).Serialize(jsonWriter, value);
                builder.Append(stringWriter.ToString());
            }
        }
        else
            value = null;
    }
    catch(Exception ex)
    {
        builder.Append($"Failed to serlize {value.GetType()} : {ex.ToString()}");
    }
    return true;
}

JSON Serializer from here : json.net limit maxdepth when serializing


Solution

  • NLog JsonLayout has two options that are important for serialization of LogEvent Properties:

    • IncludeAllProperties
    • MaxRecursionLimit

    The default parameters is like this:

    <layout type="JsonLayout" includeAllProperties="false" maxRecursionLimit="0">
       <attribute name="time" layout="${longdate}" />
       <attribute name="level" layout="${level}"/>
       <attribute name="message" layout="${message}" />
    </layout>
    

    But you can also activate inclusion of LogEvent properties like this:

    <layout type="JsonLayout" includeAllProperties="true" maxRecursionLimit="10">
       <attribute name="time" layout="${longdate}" />
       <attribute name="level" layout="${level}"/>
       <attribute name="message" layout="${message}" />
    </layout>
    

    If you have a custom object like this:

    public class Planet
    {
        public string Name { get; set; }
        public string PlanetType { get; set; }
        public override string ToString()
        {
            return Name;  // Will be called in normal message-formatting
        }
    }
    

    Then you can log the object like this:

    logger.Info("Hello {World}", new Planet() { Name = "Earth", PlanetType = "Water Planet" });
    

    The default JsonLayout will just include the default attributes, where message-attribute says Hello Earth.

    But the JsonLayout with includeAllProperties="true" will include any additional LogEvent-properties. And will include World-propety that has been serialized fully.

    The idea is that one should not care about how NLog Targets are configured when logging. It is the Logging-Rules + Target-Configuration + Layout-Configuration that decides how things should be finally written.

    If you really want the object to be serialized into ${message}, then you can also do this:

    logger.Info("Hello {@World}", new Planet() { Name = "Earth", PlanetType = "Water Planet" });
    

    If you don't want the LogEvent-properties mixed together with your default properties, then you can do this:

    <layout type="JsonLayout" maxRecursionLimit="10">
       <attribute name="time" layout="${longdate}" />
       <attribute name="level" layout="${level}"/>
       <attribute name="message" layout="${message}" />
       <attribute name="properties" encode="false">
          <layout type="JsonLayout" includeAllProperties="true" maxRecursionLimit="10" />
       </attribute>
    </layout>
    

    If you have an object that mixes fields with properties, then you can tell NLog to perform custom reflection for that type:

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