Skip to content

Instantly share code, notes, and snippets.

@wekempf
Created June 9, 2021 00:11
Show Gist options
  • Save wekempf/dd855ac44064794adf5d69da56261915 to your computer and use it in GitHub Desktop.
Save wekempf/dd855ac44064794adf5d69da56261915 to your computer and use it in GitHub Desktop.
Microsoft.Extensions.Logging with extra data
using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
var host = Host.CreateDefaultBuilder()
.ConfigureLogging(ConfigureLogging)
.Build();
ActivatorUtilities.CreateInstance<App>(host.Services).Run();
static void ConfigureLogging(HostBuilderContext context, ILoggingBuilder logging)
{
logging.AddJsonConsole();
}
internal class App
{
private readonly ILogger logger;
public App(ILogger<App> logger)
{
this.logger = logger;
}
public void Run()
{
logger.LogInformationExtra("Example: {name}", "properties", new { extra = Guid.NewGuid() });
logger.LogInformationExtra("Example: {name}", "key value pairs", new Dictionary<string, object>
{
["one"] = 1,
["two"] = 2,
["three"] = 3
});
}
}
internal class LogValuesExtraFormatter
{
private const string NullValue = "(null)";
private static readonly char[] FormatDelimiters = { ',', ':' };
private readonly string format;
private readonly List<string> valueNames = new List<string>();
public LogValuesExtraFormatter(string format)
{
OriginalFormat = format ?? throw new ArgumentNullException(nameof(format));
var sb = new StringBuilder();
int scanIndex = 0;
int endIndex = format.Length;
while (scanIndex < endIndex)
{
int openBraceIndex = FindBraceIndex(format, '{', scanIndex, endIndex);
if (scanIndex == 0 && openBraceIndex == endIndex)
{
// No holes found.
this.format = format;
return;
}
int closeBraceIndex = FindBraceIndex(format, '}', openBraceIndex, endIndex);
if (closeBraceIndex == endIndex)
{
sb.Append(format.AsSpan(scanIndex, endIndex - scanIndex));
scanIndex = endIndex;
}
else
{
// Format item syntax : { index[,alignment][ :formatString] }.
int formatDelimiterIndex = FindIndexOfAny(format, FormatDelimiters, openBraceIndex, closeBraceIndex);
sb.Append(format.AsSpan(scanIndex, openBraceIndex - scanIndex + 1));
sb.Append(valueNames.Count.ToString());
valueNames.Add(format.Substring(openBraceIndex + 1, formatDelimiterIndex - openBraceIndex - 1));
sb.Append(format.AsSpan(formatDelimiterIndex, closeBraceIndex - formatDelimiterIndex + 1));
scanIndex = closeBraceIndex + 1;
}
}
this.format = sb.ToString();
}
public string OriginalFormat { get; }
public IReadOnlyList<string> ValueNames => valueNames;
public string Format(object[] values)
{
object[] formattedValues = values;
if (values != null)
{
for (int i = 0; i < values.Length; i++)
{
object formattedValue = FormatArgument(values[i]);
// If the formatted value is changed, we allocate and copy items to a new array to avoid mutating the array passed in to this method
if (!ReferenceEquals(formattedValue, values[i]))
{
formattedValues = new object[values.Length];
Array.Copy(values, formattedValues, i);
formattedValues[i++] = formattedValue;
for (; i < values.Length; i++)
{
formattedValues[i] = FormatArgument(values[i]);
}
break;
}
}
}
return string.Format(CultureInfo.InvariantCulture, format, formattedValues ?? Array.Empty<object>());
}
public KeyValuePair<string, object> GetValue(object[] values, int index)
{
if (index < 0 || index > valueNames.Count)
{
throw new ArgumentOutOfRangeException(nameof(index));
}
if (valueNames.Count > index)
{
return new KeyValuePair<string, object>(valueNames[index], values[index]);
}
return new KeyValuePair<string, object>("{OriginalFormat}", OriginalFormat);
}
public IEnumerable<KeyValuePair<string, object>> GetValues(object[] values)
{
for (int index = 0; index != valueNames.Count; ++index)
{
yield return new KeyValuePair<string, object>(valueNames[index], values[index]);
}
yield return new KeyValuePair<string, object>("{OriginalFormat}", OriginalFormat);
}
private object FormatArgument(object value)
{
if (value == null)
{
return NullValue;
}
// since 'string' implements IEnumerable, special case it
if (value is string)
{
return value;
}
// if the value implements IEnumerable, build a comma separated string.
if (value is IEnumerable enumerable)
{
var sb = new StringBuilder(256);
bool first = true;
foreach (object e in enumerable)
{
if (!first)
{
sb.Append(", ");
}
sb.Append(e != null ? e.ToString() : NullValue);
first = false;
}
return sb.ToString();
}
return value;
}
private static int FindBraceIndex(string format, char brace, int startIndex, int endIndex)
{
// Example: {{prefix{{{Argument}}}suffix}}.
int braceIndex = endIndex;
int scanIndex = startIndex;
int braceOccurrenceCount = 0;
while (scanIndex < endIndex)
{
if (braceOccurrenceCount > 0 && format[scanIndex] != brace)
{
if (braceOccurrenceCount % 2 == 0)
{
// Even number of '{' or '}' found. Proceed search with next occurrence of '{' or '}'.
braceOccurrenceCount = 0;
braceIndex = endIndex;
}
else
{
// An unescaped '{' or '}' found.
break;
}
}
else if (format[scanIndex] == brace)
{
if (brace == '}')
{
if (braceOccurrenceCount == 0)
{
// For '}' pick the first occurrence.
braceIndex = scanIndex;
}
}
else
{
// For '{' pick the last occurrence.
braceIndex = scanIndex;
}
braceOccurrenceCount++;
}
scanIndex++;
}
return braceIndex;
}
private static int FindIndexOfAny(string format, char[] chars, int startIndex, int endIndex)
{
int findIndex = format.IndexOfAny(chars, startIndex, endIndex - startIndex);
return findIndex == -1 ? endIndex : findIndex;
}
}
internal class FormattedLogValuesExtra : IReadOnlyList<KeyValuePair<string, object>>
{
private readonly LogValuesExtraFormatter formatter;
private readonly object[] values;
private readonly IReadOnlyList<KeyValuePair<string, object>> extra;
public FormattedLogValuesExtra(string format, params object[] values)
{
formatter = new LogValuesExtraFormatter(format);
this.values = values;
if (values.Length == formatter.ValueNames.Count + 1)
{
extra = GetExtraValues(values[formatter.ValueNames.Count]);
}
}
public KeyValuePair<string, object> this[int index]
{
get
{
if (index < 0 || index >= Count)
{
throw new ArgumentOutOfRangeException(nameof(index));
}
if (index <= formatter.ValueNames.Count)
{
return formatter.GetValue(values, index);
}
return extra[index - formatter.ValueNames.Count];
}
}
public int Count => formatter.ValueNames.Count + extra.Count + 1;
public override string ToString() => formatter.Format(values);
public IEnumerator<KeyValuePair<string, object>> GetEnumerator()
{
foreach (var item in formatter.GetValues(values))
{
yield return item;
}
foreach (var item in extra)
{
yield return item;
}
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
private static IReadOnlyList<KeyValuePair<string, object>> GetExtraValues(object extra)
{
if (extra is IReadOnlyList<KeyValuePair<string, object>> list)
{
return list;
}
if (extra is IEnumerable<KeyValuePair<string, object>> keyValuePairs)
{
return keyValuePairs.ToArray();
}
return extra.GetType()
.GetProperties()
.Select(p => new KeyValuePair<string, object>(p.Name, p.GetValue(extra)))
.ToArray();
}
}
public static class LoggingExtensions
{
public static void LogExtra(this ILogger logger, LogLevel logLevel, string message, params object[] args)
{
logger.LogExtra(logLevel, 0, null, message, args);
}
public static void LogExtra(
this ILogger logger,
LogLevel logLevel,
EventId eventId,
string message,
params object[] args)
{
logger.LogExtra(logLevel, eventId, null, message, args);
}
public static void LogExtra(
this ILogger logger,
LogLevel logLevel,
Exception exception,
string message,
params object[] args)
{
logger.LogExtra(logLevel, 0, exception, message, args);
}
public static void LogExtra(
this ILogger logger,
LogLevel logLevel,
EventId eventId,
Exception exception,
string message,
params object[] args)
{
if (logger == null)
{
throw new ArgumentNullException(nameof(logger));
}
var state = new FormattedLogValuesExtra(message, args);
logger.Log(logLevel, eventId, state, exception, MessageFormatter);
}
public static void LogInformationExtra(
this ILogger logger,
EventId eventId,
Exception exception,
string message,
params object[] args)
{
logger.LogExtra(LogLevel.Information, eventId, exception, message, args);
}
public static void LogInformationExtra(this ILogger logger, EventId eventId, string message, params object[] args)
{
logger.LogExtra(LogLevel.Information, eventId, message, args);
}
public static void LogInformationExtra(this ILogger logger, Exception exception, string message, params object[] args)
{
logger.LogExtra(LogLevel.Information, exception, message, args);
}
public static void LogInformationExtra(this ILogger logger, string message, params object[] args)
{
logger.LogExtra(LogLevel.Information, message, args);
}
private static string MessageFormatter(FormattedLogValuesExtra state, Exception error) => state.ToString();
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment