| | | 1 | | using System; |
| | | 2 | | using System.Collections.Generic; |
| | | 3 | | using System.Collections.Immutable; |
| | | 4 | | using System.Threading; |
| | | 5 | | |
| | | 6 | | namespace AmbientServices; |
| | | 7 | | |
| | | 8 | | /// <summary> |
| | | 9 | | /// A "static" class to track context-specific identifiers that should be appended to every log entry, for example a req |
| | | 10 | | /// The class isn't really static, as its members are <see cref="AsyncLocal{T}"/> instances, whose contents vary based o |
| | | 11 | | /// </summary> |
| | | 12 | | /// <remarks> |
| | | 13 | | /// <pitch>Ambient log enrichment: attach key-value pairs (request id, user id, hostname, …) once at a scope boundary an |
| | | 14 | | /// <pledge> |
| | | 15 | | /// A pair added here applies to log entries made in the current async context and in contexts it subsequently forks, un |
| | | 16 | | /// Enumerating the pairs yields most-recently-added first; the default log renderer consumes them in reverse so that wh |
| | | 17 | | /// </pledge> |
| | | 18 | | /// <plan>An <see cref="AsyncLocal{T}"/> holding an <see cref="System.Collections.Immutable.ImmutableStack{T}"/> of entr |
| | | 19 | | /// </remarks> |
| | | 20 | | public static class AmbientLogContext |
| | | 21 | | { |
| | 2 | 22 | | private static readonly AsyncLocal<ImmutableStack<LogContextEntry>> aStack = new(); |
| | | 23 | | |
| | | 24 | | /// <summary> |
| | | 25 | | /// Reset the async-local stack just in case this context has been recycled and something was left in it. |
| | | 26 | | /// </summary> |
| | | 27 | | /// <param name="baselineKey">The baseline key.</param> |
| | | 28 | | /// <param name="baselineValue">The baseline value.</param> |
| | | 29 | | public static void Reset(string baselineKey, object baselineValue) |
| | | 30 | | { |
| | 2 | 31 | | aStack.Value = ImmutableStack<LogContextEntry>.Empty.Push(new(baselineKey, baselineValue)); |
| | 2 | 32 | | } |
| | | 33 | | /// <summary> |
| | | 34 | | /// Puts the key-value pair into the context so that that key-value pair will be added to every log entry called fro |
| | | 35 | | /// </summary> |
| | | 36 | | /// <param name="entry">The <see cref="LogContextEntry"/> whose key-value pair will be added to all log entries duri |
| | | 37 | | /// <returns>An object that will remove the key-value pair from the stack when it is disposed.</returns> |
| | | 38 | | public static IDisposable AddKeyValuePair(LogContextEntry entry) |
| | | 39 | | { |
| | 2 | 40 | | aStack.Value ??= ImmutableStack<LogContextEntry>.Empty; |
| | 2 | 41 | | return new LogContextLifetime(aStack, entry); |
| | | 42 | | } |
| | | 43 | | /// <summary> |
| | | 44 | | /// Puts the key-value pair into the context so that that key-value pair will be added to every log entry called fro |
| | | 45 | | /// </summary> |
| | | 46 | | /// <param name="entries">An enumeration of <see cref="LogContextEntry"/> records containing key-value pairs to add |
| | | 47 | | /// <returns>An object that will remove all of the key-value pairs from the stack when it is disposed.</returns> |
| | | 48 | | public static IDisposable AddKeyValuePairs(IEnumerable<LogContextEntry> entries) |
| | | 49 | | { |
| | | 50 | | #if NET5_0_OR_GREATER |
| | 2 | 51 | | ArgumentNullException.ThrowIfNull(entries); |
| | | 52 | | #else |
| | | 53 | | if (entries is null) throw new ArgumentNullException(nameof(entries)); |
| | | 54 | | #endif |
| | 2 | 55 | | aStack.Value ??= ImmutableStack<LogContextEntry>.Empty; |
| | 2 | 56 | | return new LogContextLifetime(aStack, entries); |
| | | 57 | | } |
| | | 58 | | /// <summary> |
| | | 59 | | /// Gets all the key-value pairs in the current context. |
| | | 60 | | /// </summary> |
| | 2 | 61 | | public static IEnumerable<LogContextEntry> ContextLogPairs => aStack.Value ?? ImmutableStack<LogContextEntry>.Empty; |
| | | 62 | | } |
| | | 63 | | |
| | | 64 | | /// <summary> |
| | | 65 | | /// A disposable that keeps a key-value pair in the log context until it is disposed. |
| | | 66 | | /// </summary> |
| | | 67 | | class LogContextLifetime : IDisposable |
| | | 68 | | { |
| | | 69 | | private readonly ImmutableStack<LogContextEntry> _previousValue; |
| | | 70 | | private readonly AsyncLocal<ImmutableStack<LogContextEntry>> _asyncLocal; |
| | | 71 | | |
| | | 72 | | public LogContextLifetime(AsyncLocal<ImmutableStack<LogContextEntry>> stack, LogContextEntry entry) |
| | | 73 | | { |
| | | 74 | | _asyncLocal = stack; |
| | | 75 | | _previousValue = stack.Value ?? ImmutableStack<LogContextEntry>.Empty; |
| | | 76 | | ImmutableStack<LogContextEntry> newValue = _previousValue.Push(entry); |
| | | 77 | | stack.Value = newValue; |
| | | 78 | | } |
| | | 79 | | |
| | | 80 | | public LogContextLifetime(AsyncLocal<ImmutableStack<LogContextEntry>> stack, IEnumerable<LogContextEntry> entries) |
| | | 81 | | { |
| | | 82 | | _asyncLocal = stack; |
| | | 83 | | _previousValue = stack.Value ?? ImmutableStack<LogContextEntry>.Empty; |
| | | 84 | | ImmutableStack<LogContextEntry> newValue = _previousValue; |
| | | 85 | | foreach (LogContextEntry entry in entries) newValue = newValue.Push(entry); |
| | | 86 | | stack.Value = newValue; |
| | | 87 | | } |
| | | 88 | | |
| | | 89 | | public void Dispose() |
| | | 90 | | { |
| | | 91 | | _asyncLocal.Value = _previousValue; |
| | | 92 | | } |
| | | 93 | | } |
| | | 94 | | |
| | | 95 | | /// <summary> |
| | | 96 | | /// A record containing a key-value pair for the context to be added to every log entry in the context. |
| | | 97 | | /// </summary> |
| | | 98 | | /// <param name="Key">The name for the context entry.</param> |
| | | 99 | | /// <param name="Value">The value for the context entry.</param> |
| | | 100 | | public record struct LogContextEntry(string Key, object? Value); |