Search code examples
.netloggingnlog

How to override the built-in JSON formatter / JSON converter in NLog ≥ v5.0?


In our project, we have some class properties that we want to censor when being logged. Each property that is to be censored is attributed with a custom-defined attribute; let's call it CensorMeAttribute.

Hence, any property we want to censor may be implemented as:

[CensorMe]
public string Secret { get; set; }

We currently override NLog's built-in JSON formatter/converter in order to censor the value of the CensorMe-attributed properties by following the guidelines provided by NLog's wiki article on How to use structured logging:

  1. We have implemented a custom NLog.IJsonConverter with the following signature:
public class CensorJsonConverter : NLog.IJsonConverter

and the appropriate logic for our use-case.

  1. In our project's Program.cs, we currently override the JSON converter and enable NLog as logging provider in this static method:
private static void AddNLog(HostBuilderContext context, ILoggingBuilder loggingBuilder)
{
    NLog.Config.ConfigurationItemFactory.Default.JsonConverter = new CensorJsonConverter();

    var config = new NLogLoggingConfiguration(context.Configuration.GetSection("NLog"));

    loggingBuilder.AddNLog(config);
}

Here, the call to loggingBuilder.AddNLog(config) is a call to ILoggingBuilder.AddNLog(LoggingConfiguration).

With this implementation, Secret is logged as \"Secret\":\"***\" rather than \"Secret\":\"actual secret value\". This is the desired logging output, which we want to keep.


Upon upgrading the NLog package version to ≥ v5.0, NLog.Config.ConfigurationItemFactory.Default.JsonConverter became obsolete. The workaround message says:

Instead use LogFactory.ServiceRepository.ResolveInstance(typeof(IJsonConverter)).

I cannot find any ResolveInstance() method on the LogFactory.ServiceRepository property. There is the RegisterService() method, which registers instance of singleton object for use in NLog (ServiceRepository docs). What I hence have tried is to replace the line

NLog.[...].JsonConverter = new CensorJsonConverter();

with a call to

config.LogFactory.ServiceRepository.RegisterService([...]);

as follows:

private static void AddNLog(HostBuilderContext context, ILoggingBuilder loggingBuilder)
{
    var config = new NLogLoggingConfiguration(context.Configuration.GetSection("NLog"));

    config.LogFactory.ServiceRepository.RegisterService(
        typeof(CensorJsonConverter),
        new CensorJsonConverter());

    loggingBuilder.AddNLog(config);
}

With this implementation, Secret is logged as \"Secret\":\"actual secret value\". This is not the desired logging output.


How do I override the built-in JSON converter without setting the now (NLog ≥ v5.0) obsolete NLog.Config.ConfigurationItemFactory.Default.JsonConverter?

Or is there perhaps another way to achieve a similar handling of the CensorMe-attributed properties, with or without the use of NLog?


Solution

  • You can do this:

    private static void AddNLog(HostBuilderContext context, ILoggingBuilder loggingBuilder)
    {
        LogManager.Setup()
          .SetupExtensions(e => e.RegisterNLogWeb().RegisterConfigSettings(context.Configuration))
          .SetupSerialization(s => s.RegisterJsonConverter(new CensorJsonConverter())
          .LoadConfigurationFromSection(context.Configuration);
        loggingBuilder.AddNLogWeb();
    }
    

    Notice that if you wrap your secrets in a custom type that inherits from IFormattable, then NLog will by default ignore properties and just do ToString() (No need for custom JsonConverter)

    public struct SecretValue : IFormattable
    {
        public string Value;
    
        string IFormattable.ToString(string format, IFormatProvider formatProvider) => ToString();
        public override string ToString() => string.IsNullOrEmpty(Value) ? "" : "*****";
    }
    

    See also: https://github.com/NLog/NLog/wiki/How-to-use-structured-logging#customize-object-reflection