< Summary

Information
Class: AmbientServices.AmbientLogContext
Assembly: AmbientServices
File(s): /home/runner/work/AmbientServices/AmbientServices/AmbientServices/Helpers/AmbientLogContext.cs
Tag: 332_35464845198
Line coverage
100%
Covered lines: 9
Uncovered lines: 0
Coverable lines: 9
Total lines: 100
Line coverage: 100%
Branch coverage
66%
Covered branches: 4
Total branches: 6
Branch coverage: 66.6%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.cctor()100%11100%
Reset(...)100%11100%
AddKeyValuePair(...)50%22100%
AddKeyValuePairs(...)50%22100%
get_ContextLogPairs()100%22100%

File(s)

/home/runner/work/AmbientServices/AmbientServices/AmbientServices/Helpers/AmbientLogContext.cs

#LineLine coverage
 1using System;
 2using System.Collections.Generic;
 3using System.Collections.Immutable;
 4using System.Threading;
 5
 6namespace 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>
 20public static class AmbientLogContext
 21{
 222    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    {
 231        aStack.Value = ImmutableStack<LogContextEntry>.Empty.Push(new(baselineKey, baselineValue));
 232    }
 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    {
 240        aStack.Value ??= ImmutableStack<LogContextEntry>.Empty;
 241        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
 251        ArgumentNullException.ThrowIfNull(entries);
 52#else
 53        if (entries is null) throw new ArgumentNullException(nameof(entries));
 54#endif
 255        aStack.Value ??= ImmutableStack<LogContextEntry>.Empty;
 256        return new LogContextLifetime(aStack, entries);
 57    }
 58    /// <summary>
 59    /// Gets all the key-value pairs in the current context.
 60    /// </summary>
 261    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>
 67class 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>
 100public record struct LogContextEntry(string Key, object? Value);