< Summary

Information
Class: ArturRios.Logging.Adapter.MicrosoftLoggerAdapter
Assembly: ArturRios.Logging
File(s): /home/runner/work/dotnet-logging/dotnet-logging/src/Adapter/MicrosoftLoggerAdapter.cs
Line coverage
79%
Covered lines: 64
Uncovered lines: 17
Coverable lines: 81
Total lines: 227
Line coverage: 79%
Branch coverage
76%
Covered branches: 64
Total branches: 84
Branch coverage: 76.1%
Method coverage
72%
Covered methods: 8
Fully covered methods: 5
Total methods: 11
Method coverage: 72.7%
Full method coverage: 45.4%

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)50%22100%
.cctor()100%11100%
get_TraceId()100%44100%
set_TraceId(...)100%11100%
BeginScope(...)100%210%
IsEnabled(...)100%11100%
Log(...)56.25%493274.28%
StateContainsCallerInfo(...)96.15%262694.11%
FindCallerFromStack()80%232080%
get_Instance()100%210%
Dispose()100%210%

File(s)

/home/runner/work/dotnet-logging/dotnet-logging/src/Adapter/MicrosoftLoggerAdapter.cs

#LineLine coverage
 1using System.Diagnostics;
 2using ArturRios.Logging.Interfaces;
 3using Microsoft.Extensions.DependencyInjection;
 4using Microsoft.Extensions.Logging;
 5
 6namespace ArturRios.Logging.Adapter;
 7
 8/// <summary>
 9/// Bridges <see cref="ILogger"/> onto <see cref="IStateLogger"/>, so code written against
 10/// Microsoft.Extensions.Logging reaches this library's loggers.
 11/// </summary>
 12/// <remarks>
 13/// <para>
 14/// Each call resolves <see cref="IStateLogger"/> from a fresh dependency injection scope, so a scoped
 15/// dependency of the state logger is isolated per log entry. The correlation id therefore does not travel
 16/// through the scope; it is read from the ambient <see cref="Activity"/> instead.
 17/// </para>
 18/// <para>
 19/// <see cref="Activity"/> is the correlation primitive of the platform, not of any one hosting model: an
 20/// ASP.NET Core request has one, and so does a worker, a console app or a test. Reading the id from there
 21/// is what lets this library log correlated entries without depending on ASP.NET Core.
 22/// </para>
 23/// </remarks>
 24/// <param name="services">The provider used to resolve the state logger.</param>
 25/// <exception cref="ArgumentNullException"><paramref name="services"/> is <c>null</c>.</exception>
 2126public class MicrosoftLoggerAdapter(IServiceProvider services) : ILogger
 27{
 28    /// <summary>
 29    /// A correlation id set explicitly by a caller, overriding the ambient activity's. Held in an
 30    /// <see cref="AsyncLocal{T}"/> so it flows with the execution context — per request under a server,
 31    /// per logical operation anywhere else — rather than leaking across concurrent work.
 32    /// </summary>
 133    private static readonly AsyncLocal<string?> Override = new();
 34
 2135    private readonly IServiceProvider _services = services ?? throw new ArgumentNullException(nameof(services));
 36
 37    /// <summary>
 38    /// Gets or sets the correlation id stamped on log entries.
 39    /// </summary>
 40    /// <value>
 41    /// The id set explicitly on this property, or the trace id of <see cref="Activity.Current"/> when none
 42    /// was set, or <c>null</c> when there is no ambient activity either.
 43    /// </value>
 44    /// <remarks>
 45    /// Setting it establishes the id for the current execution context and everything that flows from it.
 46    /// <c>ArturRios.Util.WebApi</c>'s <c>TraceActivityMiddleware</c> starts a W3C activity per request and
 47    /// derives its own trace id from exactly this activity, so under that middleware the value read here is
 48    /// the same one the middleware publishes — with no ASP.NET Core dependency on this side.
 49    /// </remarks>
 50    public string? TraceId
 51    {
 1652        get => Override.Value ?? Activity.Current?.TraceId.ToString();
 753        set => Override.Value = value;
 54    }
 55
 56    /// <inheritdoc />
 057    public IDisposable BeginScope<TState>(TState state) where TState : notnull => NullScope.Instance;
 58
 59    /// <inheritdoc />
 1360    public bool IsEnabled(LogLevel logLevel) => logLevel != LogLevel.None;
 61
 62    /// <inheritdoc />
 63    public void Log<TState>(
 64        LogLevel logLevel,
 65        EventId eventId,
 66        TState state,
 67        Exception? exception,
 68        Func<TState, Exception?, string>? formatter)
 69    {
 1170        if (!IsEnabled(logLevel)) return;
 71
 1172        var message = formatter != null ? formatter(state!, exception) : state?.ToString() ?? string.Empty;
 73
 74        // Resolve the scoped IStateLogger (if not registered you'll get null)
 1175        using var scope = _services.CreateScope();
 1176        var stateLogger = scope.ServiceProvider.GetService<IStateLogger>();
 77
 78        // Propagate the ambient correlation id, whether set explicitly or carried by the current activity.
 1179        if (stateLogger is not null && TraceId is { } ambientTraceId)
 80        {
 481            stateLogger.TraceId = ambientTraceId;
 82        }
 83
 1184        if (stateLogger is null)
 85        {
 86            // No IStateLogger registered — nothing to forward to
 187            return;
 88        }
 89
 90        // Enrich state with caller info if not present so StateLogger.ResolveCallerInfo can pick it up.
 1091        object? enrichedState = state;
 1092        if (!StateContainsCallerInfo(state))
 93        {
 894            var (filePath, memberName) = FindCallerFromStack();
 895            var kvList = new List<KeyValuePair<string, object>>();
 96
 897            if (state is IEnumerable<KeyValuePair<string, object>> existingPairs)
 98            {
 1899                foreach (var kv in existingPairs)
 100                {
 1101                    kvList.Add(kv);
 102                }
 103            }
 104
 8105            kvList.Add(new KeyValuePair<string, object>("CallerFilePath", filePath));
 8106            kvList.Add(new KeyValuePair<string, object>("CallerMemberName", memberName));
 8107            kvList.Add(new KeyValuePair<string, object>("OriginalMessage", message));
 108
 8109            enrichedState = kvList.ToArray();
 110        }
 111
 10112        if (exception is not null)
 113        {
 1114            stateLogger.Exception(exception, enrichedState);
 115        }
 116
 117        switch (logLevel)
 118        {
 119            case LogLevel.Trace:
 0120                stateLogger.Trace(message, enrichedState);
 0121                break;
 122            case LogLevel.Debug:
 0123                stateLogger.Debug(message, enrichedState);
 0124                break;
 125            case LogLevel.Information:
 9126                stateLogger.Info(message, enrichedState);
 9127                break;
 128            case LogLevel.Warning:
 0129                stateLogger.Warn(message, enrichedState);
 0130                break;
 131            case LogLevel.Error:
 1132                stateLogger.Error(message, enrichedState);
 1133                break;
 134            case LogLevel.Critical:
 0135                stateLogger.Critical(message, enrichedState);
 0136                break;
 137            case LogLevel.None:
 138            default:
 0139                throw new ArgumentOutOfRangeException(nameof(logLevel), logLevel, null);
 140        }
 11141    }
 142
 143    private static bool StateContainsCallerInfo(object? state)
 144    {
 10145        if (state is not IEnumerable<KeyValuePair<string, object?>> pairs)
 146        {
 0147            return false;
 148        }
 149
 20150        bool hasFile = false, hasMember = false;
 151
 28152        foreach (var (k, v) in pairs)
 153        {
 5154            if (v is null)
 155            {
 156                continue;
 157            }
 158
 5159            if (!hasFile && (string.Equals(k, "CallerFilePath", StringComparison.OrdinalIgnoreCase) ||
 5160                             string.Equals(k, "FilePath", StringComparison.OrdinalIgnoreCase) ||
 5161                             string.Equals(k, "callerFilePath", StringComparison.OrdinalIgnoreCase)))
 162            {
 2163                hasFile = true;
 164            }
 165
 5166            if (!hasMember && (string.Equals(k, "CallerMemberName", StringComparison.OrdinalIgnoreCase) ||
 5167                               string.Equals(k, "MemberName", StringComparison.OrdinalIgnoreCase) ||
 5168                               string.Equals(k, "Method", StringComparison.OrdinalIgnoreCase) ||
 5169                               string.Equals(k, "callerMemberName", StringComparison.OrdinalIgnoreCase)))
 170            {
 2171                hasMember = true;
 172            }
 173
 7174            if (hasFile && hasMember) return true;
 175        }
 176
 8177        return false;
 2178    }
 179
 180    private static (string filePath, string memberName) FindCallerFromStack()
 181    {
 182        try
 183        {
 8184            var st = new StackTrace(skipFrames: 1, fNeedFileInfo: false);
 185
 104186            for (var i = 0; i < st.FrameCount; i++)
 187            {
 52188                var frame = st.GetFrame(i);
 52189                var method = frame?.GetMethod();
 52190                if (method == null) continue;
 52191                var declaring = method.DeclaringType;
 52192                if (declaring == null) continue;
 193
 52194                var ns = declaring.Namespace ?? string.Empty;
 195
 196                // Skip known logging infrastructure namespaces so we find the real caller
 197                // ArturRios.Logging is this library's own namespace. The predecessor of this check named
 198                // ArturRios.Common.Logging, which no longer exists, so nothing here was ever skipped and the
 199                // frame reported as the caller was MicrosoftLoggerAdapter.Log itself.
 52200                if (ns.StartsWith("Microsoft.Extensions.Logging", StringComparison.Ordinal) ||
 52201                    ns.Equals("ArturRios.Logging", StringComparison.Ordinal) ||
 52202                    ns.StartsWith("ArturRios.Logging.", StringComparison.Ordinal) ||
 52203                    ns.StartsWith("System.", StringComparison.Ordinal))
 204                {
 205                    continue;
 206                }
 207
 8208                var memberName = method.Name;
 8209                var filePath = declaring.FullName ?? declaring.Name;
 210
 8211                return (filePath, memberName);
 212            }
 0213        }
 0214        catch
 215        {
 216            // swallow any errors and fall through to unknowns
 0217        }
 218
 0219        return ("unknown", "unknown");
 8220    }
 221
 222    private class NullScope : IDisposable
 223    {
 0224        public static NullScope Instance { get; } = new();
 0225        public void Dispose() { }
 226    }
 227}