简体   繁体   中英

How to conditionally render an attribute with NLog and structured logging?

I have inherited an ASP.NET application which is using NLog for logging. One of the logged attributes is named module which is hardcoded to "Core". This is not useful at all, because all parts of the application (eg health check, various business contexts) push the same value.

I am trying to use structured logging to override this value in some cases (I cannot afford to refactor the entire logging now).

Extension method

public static void LogInfoWithModule<T>(this ILogger<T> logger, string message, string module)
{
    var escapedMessage = message.Replace("{", "{{");
    escapedMessage = escapedMessage.Replace("}", "}}");
    logger.Log(LogLevel.Information, "[{module}] " + escapedMessage, module);
}

Usage

logger.LogInfoWithModule($"Health check response = {response}", Constants.LoggingModules.Health);

NLog layout (most of the attributes removed for brevity)

<target name="logJson" xsi:type="File">
  <layout xsi:type="JsonLayout" includeAllProperties="true">
    <attribute name="module" layout="${when:when='${event-properties:item=module}'=='':Core" />
  </layout>
</target>

I get want want, that is all existing calls will not provide the module and they will work as before. Any logging passing through LogInfoWithModule will override the default value.

However, I find this solution very messy because:

  • the actual message is containing the module name
  • structured logging cannot be used
  • cannot delay string formatting (by providing params to Log function)

I have tried the solution suggested here (and here ), but ${mdlc:item=module} does not contain anything.

The easy way, but will have a performance hit (Notice ${mdlc:item=module} is case-sensitive)

    public static void LogInfoWithModule<T>(this ILogger<T> logger, string message, string module)
    {
        using (logger.BeginScope(new [] { new KeyValuePair<string,object>("module", module) }))
        {
           logger.Log(LogLevel.Information, message);
        }
    }

The advanced way for injecting properties (Notice ${event-properties:module} is case-sensitive):

    public static void LogInfoWithModule<T>(this ILogger<T> logger, string message, string module)
    {
        logger.Log(LogLevel.Information, default(EventId), new ModuleLogEvent(message, module), default(Exception), ModuleLogEvent.Formatter);
    }

    class ModuleLogEvent : IReadOnlyList<KeyValuePair<string, object>>
    {
        public static Func<ModuleLogEvent, Exception, string> Formatter { get; } = (l, ex) => l.Message;
    
        public string Message { get; }
    
        public string Module { get; }
                   
        public MyLogEvent(string message, string module)
        {
            Message = message;
            Module = module;
        }
    
        public override string ToString() => Message;
    
        // IReadOnlyList-interface
        public int Count => 1;
    
        public KeyValuePair<string, object> this[int index] => new KeyValuePair<string,object>("module", Module);
    
        public IEnumerator<KeyValuePair<string, object>> GetEnumerator() => yield return new KeyValuePair<string,object>("module", Module);
    
        IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
    }

Beware typos and compile-errors might be included in the above example code.

Alternative you might find happiness here: https://github.com/viktor-nikolaev/XeonApps.Extensions.Logging.WithProperty :

public static class LoggingExtensions
{
    public static NLog.Logger WithModule(this NLog.Logger logger, object propertyValue) =>
        logger.WithProperty("module", propertyValue);

    public static ILogger WithModule(this ILogger logger, object propertyValue) =>
        logger.WithProperty("module", propertyValue);
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM