Created
June 9, 2021 00:11
-
-
Save wekempf/dd855ac44064794adf5d69da56261915 to your computer and use it in GitHub Desktop.
Microsoft.Extensions.Logging with extra data
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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