Skip to content

NLog properties with Microsoft Extension Logging

Rolf Kristensen edited this page Nov 26, 2017 · 35 revisions

The official extension methods in Microsoft Extension Logging does not provide any obvious way to provide custom NLog properties.

But there are still different options available:

Message Templates

NLog 4.5 supports properties captured from structured logging message templates:

_logger.LogDebug("Logon from {userid}", request.UserId);

These properties can then be extracted using the ${event-properties} renderer.

BeginScope

NLog.Extensions.Logging ver. 1.0 supports async properties captured from the BeginScope input parameter:

using (_logger.BeginScope(new[] { new KeyValuePair<string, object>("userid", request.UserId) }))
{
   _logger.LogDebug("Logon from {0}", request.UserId);
}

These async properties can then be extracted using the ${mdlc} renderer

Log<TState> Simple

Microsoft Extension Logging allows one to log any kind of TState-object, when using the direct Log-method instead of the extension methods.

NLog.Extensions.Logging ver. 1.0 will attempt to cast the TState-object into IEnumerable<KeyValuePair<string, object>>, and if successful then include them as NLog LogEventInfo.Properties.

This can be used to create a custom logevent object that contains both a message and properties.

class MyLogEvent : IEnumerable<KeyValuePair<string, object>>
{
    List<KeyValuePair<string, object>> _properties = new List<KeyValuePair<string, object>>();

    public string Message { get; }
                
    public MyLogEvent(string message)
    {
        Message = message;
    }

    public IEnumerator<KeyValuePair<string, object>> GetEnumerator()
    {
        return _properties.GetEnumerator();
    }

    IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); }

    public MyLogEvent AddProp(string name, object value)
    {
        _properties.Add(new KeyValuePair<string, object>(name, value));
        return this;
    }

    public static Func<MyLogEvent, Exception, string> Formatter { get; } = (l, e) => l.Message;
}

_logger.Log( Microsoft.Extensions.Logging.LogLevel.Debug,
             default(EventId),
             new MyLogEvent($"Logon from {request.UserId}").AddProp("userid", request.UserId),
             (Exception)null,
             MyLogEvent.Formatter );

These properties can then be extracted using the ${event-properties} renderer.

Log<TState> Advanced

Combines structured logging message template with additional properties. It wraps the message formatter from Microsoft Extension Logging, and allows injection of extra properties:

class MyLogEvent2 : IReadOnlyList<KeyValuePair<string, object>>
{
    readonly string _format;
    readonly object[] _parameters;
    Microsoft.Extensions.Logging.Internal.FormattedLogValues LogValues => _logValues ??
        (_logValues = new Microsoft.Extensions.Logging.Internal.FormattedLogValues(_format, _parameters));
    Microsoft.Extensions.Logging.Internal.FormattedLogValues _logValues;
    List<KeyValuePair<string, object>> _extraProperties;

    public MyLogEvent2(string format, params object[] values)
    {
        _format = format;
        _parameters = values;
    }

    public IEnumerator<KeyValuePair<string, object>> GetEnumerator()
    {
        if (MessagePropertyCount == 0)
        {
            if (ExtraPropertyCount > 0)
                return _extraProperties.GetEnumerator();
            else
                return Enumerable.Empty<KeyValuePair<string, object>>().GetEnumerator();
        }
        else
        {
            if (ExtraPropertyCount > 0)
                return _extraProperties.Concat(LogValues).GetEnumerator();
            else
                return LogValues.GetEnumerator();
        }
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }

    private int MessagePropertyCount
    {
        get
        {
            if (LogValues.Count > 1 && !string.IsNullOrEmpty(LogValues[0].Key) && !char.IsDigit(LogValues[0].Key[0]))
                return LogValues.Count;
            else
                return 0;
        }
    }

    private int ExtraPropertyCount => _extraProperties?.Count ?? 0;

    public int Count => MessagePropertyCount + ExtraPropertyCount;

    public KeyValuePair<string, object> this[int index]
    {
        get
        {
            int extraCount = ExtraPropertyCount;
            if (index < extraCount)
            {
                return _extraProperties[index];
            }
            else
            {
                return LogValues[index - extraCount];
            }
        }
    }

    public MyLogEvent2 AddProp(string name, object value)
    {
        var properties = _extraProperties ?? (_extraProperties = new List<KeyValuePair<string, object>>());
        properties.Add(new KeyValuePair<string, object>(name, value));
        return this;
    }

    public static Func<MyLogEvent2, Exception, string> Formatter { get; } = (l, e) => l.LogValues.ToString();
};

_logger.Log( Microsoft.Extensions.Logging.LogLevel.Debug,
             default(EventId),
             new MyLogEvent2("Logon from {userid}", request.UserId).AddProp("ipaddress", request.IpAddress),
             (Exception)null,
             MyLogEvent2.Formatter );

The properties can then be extracted using the ${event-properties} renderer.