< Summary

Information
Class: ArturRios.Logging.Adapter.MicrosoftLoggerAdapter
Assembly: ArturRios.Logging
File(s): D:\Repositories\dotnet-logging\src\Adapter\MicrosoftLoggerAdapter.cs
Line coverage
73%
Covered lines: 84
Uncovered lines: 31
Coverable lines: 115
Total lines: 197
Line coverage: 73%
Branch coverage
57%
Covered branches: 52
Total branches: 90
Branch coverage: 57.7%
Method coverage
70%
Covered methods: 7
Fully covered methods: 2
Total methods: 10
Method coverage: 70%
Full method coverage: 20%

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)50%22100%
get_TraceId()75%44100%
set_TraceId(...)100%44100%
BeginScope(...)100%210%
IsEnabled(...)100%11100%
Log(...)58.33%513677.55%
StateContainsCallerInfo(...)53.84%602662.96%
FindCallerFromStack()50%251872%
get_Instance()100%210%
Dispose()100%210%

File(s)

D:\Repositories\dotnet-logging\src\Adapter\MicrosoftLoggerAdapter.cs

#LineLine coverage
 1using System.Diagnostics;
 2using ArturRios.Logging.Interfaces;
 3using Microsoft.AspNetCore.Http;
 4using Microsoft.Extensions.DependencyInjection;
 5using Microsoft.Extensions.Logging;
 6
 7namespace ArturRios.Logging.Adapter;
 8
 99public class MicrosoftLoggerAdapter(IServiceProvider services) : ILogger
 10{
 911    private readonly IServiceProvider _services = services ?? throw new ArgumentNullException(nameof(services));
 12
 13    // Expose TraceId by reading/writing the current HttpContext item (if available)
 14    public string? TraceId
 15    {
 16        get
 417        {
 418            var accessor = _services.GetService<IHttpContextAccessor>();
 419            return accessor?.HttpContext?.Items["TraceId"] as string;
 420        }
 21        set
 322        {
 323            var accessor = _services.GetService<IHttpContextAccessor>();
 24
 325            accessor?.HttpContext?.Items["TraceId"] = value;
 326        }
 27    }
 28
 29    /// <inheritdoc />
 030    public IDisposable BeginScope<TState>(TState state) where TState : notnull => NullScope.Instance;
 31
 32    /// <inheritdoc />
 433    public bool IsEnabled(LogLevel logLevel) => logLevel != LogLevel.None;
 34
 35    /// <inheritdoc />
 36    public void Log<TState>(
 37        LogLevel logLevel,
 38        EventId eventId,
 39        TState state,
 40        Exception? exception,
 41        Func<TState, Exception?, string>? formatter)
 242    {
 243        if (!IsEnabled(logLevel)) return;
 44
 245        var message = formatter != null ? formatter(state!, exception) : state?.ToString() ?? string.Empty;
 46
 47        // Resolve the scoped IStateLogger (if not registered you'll get null)
 248        using var scope = _services.CreateScope();
 249        var stateLogger = scope.ServiceProvider.GetService<IStateLogger>();
 250        var httpAccessor = scope.ServiceProvider.GetService<IHttpContextAccessor>();
 51
 52        // Propagate TraceId from HttpContext (middleware should set this per request)
 253        if (stateLogger is not null && httpAccessor?.HttpContext?.Items["TraceId"] is string httpTrace)
 154        {
 155            stateLogger.TraceId = httpTrace;
 156        }
 57
 258        if (stateLogger is null)
 059        {
 60            // No IStateLogger registered — nothing to forward to
 061            return;
 62        }
 63
 64        // Enrich state with caller info if not present so StateLogger.ResolveCallerInfo can pick it up.
 265        object? enrichedState = state;
 266        if (!StateContainsCallerInfo(state))
 267        {
 268            var (filePath, memberName) = FindCallerFromStack();
 269            var kvList = new List<KeyValuePair<string, object>>();
 70
 271            if (state is IEnumerable<KeyValuePair<string, object>> existingPairs)
 272            {
 873                foreach (var kv in existingPairs)
 174                {
 175                    kvList.Add(kv);
 176                }
 277            }
 78
 279            kvList.Add(new KeyValuePair<string, object>("CallerFilePath", filePath));
 280            kvList.Add(new KeyValuePair<string, object>("CallerMemberName", memberName));
 281            kvList.Add(new KeyValuePair<string, object>("OriginalMessage", message));
 82
 283            enrichedState = kvList.ToArray();
 284        }
 85
 286        if (exception is not null)
 187        {
 188            stateLogger.Exception(exception, enrichedState);
 189        }
 90
 291        switch (logLevel)
 92        {
 93            case LogLevel.Trace:
 094                stateLogger.Trace(message, enrichedState);
 095                break;
 96            case LogLevel.Debug:
 097                stateLogger.Debug(message, enrichedState);
 098                break;
 99            case LogLevel.Information:
 1100                stateLogger.Info(message, enrichedState);
 1101                break;
 102            case LogLevel.Warning:
 0103                stateLogger.Warn(message, enrichedState);
 0104                break;
 105            case LogLevel.Error:
 1106                stateLogger.Error(message, enrichedState);
 1107                break;
 108            case LogLevel.Critical:
 0109                stateLogger.Critical(message, enrichedState);
 0110                break;
 111            case LogLevel.None:
 112            default:
 0113                throw new ArgumentOutOfRangeException(nameof(logLevel), logLevel, null);
 114        }
 4115    }
 116
 117    private static bool StateContainsCallerInfo(object? state)
 2118    {
 2119        if (state is not IEnumerable<KeyValuePair<string, object?>> pairs)
 0120        {
 0121            return false;
 122        }
 123
 4124        bool hasFile = false, hasMember = false;
 125
 8126        foreach (var (k, v) in pairs)
 1127        {
 1128            if (v is null)
 0129            {
 0130                continue;
 131            }
 132
 1133            if (!hasFile && (string.Equals(k, "CallerFilePath", StringComparison.OrdinalIgnoreCase) ||
 1134                             string.Equals(k, "FilePath", StringComparison.OrdinalIgnoreCase) ||
 1135                             string.Equals(k, "callerFilePath", StringComparison.OrdinalIgnoreCase)))
 0136            {
 0137                hasFile = true;
 0138            }
 139
 1140            if (!hasMember && (string.Equals(k, "CallerMemberName", StringComparison.OrdinalIgnoreCase) ||
 1141                               string.Equals(k, "MemberName", StringComparison.OrdinalIgnoreCase) ||
 1142                               string.Equals(k, "Method", StringComparison.OrdinalIgnoreCase) ||
 1143                               string.Equals(k, "callerMemberName", StringComparison.OrdinalIgnoreCase)))
 0144            {
 0145                hasMember = true;
 0146            }
 147
 1148            if (hasFile && hasMember) return true;
 1149        }
 150
 2151        return false;
 2152    }
 153
 154    private static (string filePath, string memberName) FindCallerFromStack()
 2155    {
 156        try
 2157        {
 2158            var st = new StackTrace(skipFrames: 1, fNeedFileInfo: false);
 159
 4160            for (var i = 0; i < st.FrameCount; i++)
 2161            {
 2162                var frame = st.GetFrame(i);
 2163                var method = frame?.GetMethod();
 2164                if (method == null) continue;
 2165                var declaring = method.DeclaringType;
 2166                if (declaring == null) continue;
 167
 2168                var ns = declaring.Namespace ?? string.Empty;
 169
 170                // Skip known logging infrastructure namespaces so we find the real caller
 2171                if (ns.StartsWith("Microsoft.Extensions.Logging", StringComparison.Ordinal) ||
 2172                    ns.StartsWith("ArturRios.Common.Logging", StringComparison.Ordinal) ||
 2173                    ns.StartsWith("System.", StringComparison.Ordinal))
 0174                {
 0175                    continue;
 176                }
 177
 2178                var memberName = method.Name;
 2179                var filePath = declaring.FullName ?? declaring.Name;
 180
 2181                return (filePath, memberName);
 182            }
 0183        }
 0184        catch
 0185        {
 186            // swallow any errors and fall through to unknowns
 0187        }
 188
 0189        return ("unknown", "unknown");
 2190    }
 191
 192    private class NullScope : IDisposable
 193    {
 0194        public static NullScope Instance { get; } = new();
 0195        public void Dispose() { }
 196    }
 197}