File: Logging\ExtendedLogger.cs
Web Access
Project: src\src\Libraries\Microsoft.Extensions.Telemetry\Microsoft.Extensions.Telemetry.csproj (Microsoft.Extensions.Telemetry)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System;
using System.Collections.Generic;
using System.Diagnostics;
using Microsoft.Extensions.Logging;
using Microsoft.Shared.Pools;
 
namespace Microsoft.Extensions.Logging;
 
#pragma warning disable CA1031
 
// NOTE: This implementation uses thread local storage. As a result, it will fail if formatter code, enricher code, or
//       redactor code calls recursively back into the logger. Don't do that.
//
// NOTE: Unlike the original logger in dotnet/runtime, this logger eats exceptions thrown from invoked loggers, enrichers,
//       and redactors, rather than forwarding the exceptions to the caller. The fact an exception occured is recorded in
//       the event log instead. The idea is that failures in the telemetry stack should not lead to failures in the
//       application. It's better to keep running with missing telemetry rather than crashing the process completely.
 
internal sealed partial class ExtendedLogger : ILogger
{
    private const string ExceptionType = "exception.type";
    private const string ExceptionMessage = "exception.message";
    private const string ExceptionStackTrace = "exception.stacktrace";
 
    private readonly ExtendedLoggerFactory _factory;
 
    public LoggerInformation[] Loggers { get; set; }
    public MessageLogger[] MessageLoggers { get; set; } = Array.Empty<MessageLogger>();
    public ScopeLogger[] ScopeLoggers { get; set; } = Array.Empty<ScopeLogger>();
 
    public ExtendedLogger(ExtendedLoggerFactory factory, LoggerInformation[] loggers)
    {
        _factory = factory;
        Loggers = loggers;
    }
 
    public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
    {
        if (typeof(TState) == typeof(LoggerMessageState))
        {
            var msgState = (LoggerMessageState?)(object?)state;
            if (msgState != null)
            {
                ModernPath(logLevel, eventId, msgState, exception, (Func<LoggerMessageState, Exception?, string>)(object)formatter);
                return;
            }
        }
 
        LegacyPath<TState>(logLevel, eventId, state, exception, formatter);
    }
 
    public IDisposable? BeginScope<TState>(TState state)
        where TState : notnull
    {
        var loggers = ScopeLoggers;
 
        if (loggers.Length == 0)
        {
            return NullScope.Instance;
        }
        else if (loggers.Length == 1)
        {
            return loggers[0].CreateScope(state);
        }
 
        var scope = new Scope(loggers.Length);
        List<Exception>? exceptions = null;
        for (int i = 0; i < loggers.Length; i++)
        {
            try
            {
                scope.SetDisposable(i, loggers[i].CreateScope(state));
            }
            catch (Exception ex)
            {
#pragma warning disable CA1508 // Avoid dead conditional code
                exceptions ??= [];
#pragma warning restore CA1508 // Avoid dead conditional code
                exceptions.Add(ex);
            }
        }
 
        HandleExceptions(exceptions);
 
        return scope;
    }
 
    public bool IsEnabled(LogLevel logLevel)
    {
        var loggers = MessageLoggers;
 
        List<Exception>? exceptions = null;
        int i = 0;
        for (; i < loggers.Length; i++)
        {
            ref readonly MessageLogger loggerInfo = ref loggers[i];
            if (loggerInfo.IsNotFilteredOut(logLevel))
            {
                try
                {
                    if (loggerInfo.LoggerIsEnabled(logLevel))
                    {
                        break;
                    }
                }
                catch (Exception ex)
                {
#pragma warning disable CA1508 // Avoid dead conditional code
                    exceptions ??= [];
#pragma warning restore CA1508 // Avoid dead conditional code
                    exceptions.Add(ex);
                }
            }
        }
 
        HandleExceptions(exceptions);
 
        return i < loggers.Length;
    }
 
    private static void HandleExceptions(IEnumerable<Exception>? exceptions)
    {
        if (exceptions != null)
        {
            LoggingEventSource.Instance.LoggingException(new AggregateException("An error occurred while logging.", exceptions));
        }
    }
 
    private static void RecordException(Exception exception, EnrichmentTagCollector tags, LoggerConfig config)
    {
        tags.Add(ExceptionType, exception.GetType().ToString());
 
        if (config.IncludeExceptionMessage)
        {
            tags.Add(ExceptionMessage, exception.Message);
        }
 
        if (config.CaptureStackTraces)
        {
            tags.Add(ExceptionStackTrace, GetExceptionStackTrace(exception, config));
        }
    }
 
    private static string GetExceptionStackTrace(Exception exception, LoggerConfig config)
    {
        const int IndentAmount = 3;
 
        var sb = PoolFactory.SharedStringBuilderPool.Get();
        try
        {
            HandleException(exception, 0);
 
            return sb.Length > config.MaxStackTraceLength
                ? sb.ToString(0, config.MaxStackTraceLength)
                : sb.ToString();
        }
        finally
        {
            PoolFactory.SharedStringBuilderPool.Return(sb);
        }
 
        void HandleException(Exception exception, int indent)
        {
            var indentStr = new string(' ', indent);
 
            if (sb.Length > 0)
            {
                _ = sb.Append(indentStr);
                _ = sb.Append("---> ");
            }
 
            var trace = new StackTrace(exception, config.UseFileInfoForStackTraces).ToString();
 
#if NETCOREAPP3_1_OR_GREATER
            trace = trace.Replace(Environment.NewLine, Environment.NewLine + indentStr + "   ", StringComparison.Ordinal).Trim(' ');
#else
            trace = trace.Replace(Environment.NewLine, Environment.NewLine + indentStr + "   ").Trim(' ');
#endif
 
            _ = sb.Append(exception.GetType());
            _ = sb.Append(": ");
 
            if (config.IncludeExceptionMessage)
            {
                _ = sb.AppendLine(exception.Message);
                _ = sb.Append(indentStr);
            }
 
            _ = sb.Append(trace);
 
            if (exception is AggregateException aggregateException)
            {
                foreach (var ex in aggregateException.InnerExceptions)
                {
                    HandleException(ex, indent + IndentAmount);
                }
            }
            else if (exception.InnerException != null)
            {
                HandleException(exception.InnerException, indent + IndentAmount);
            }
        }
    }
 
    private void ModernPath(LogLevel logLevel, EventId eventId, LoggerMessageState msgState, Exception? exception, Func<LoggerMessageState, Exception?, string> formatter)
    {
        var loggers = MessageLoggers;
        var config = _factory.Config;
 
        // redact
        JustInTimeRedactor? jitRedactors = null;
        for (int i = 0; i < msgState.ClassifiedTagsCount; i++)
        {
            ref var cp = ref msgState.ClassifiedTagArray[i];
            if (cp.Value != null)
            {
                var jr = JustInTimeRedactor.Get(
                    cp.Value,
                    config.GetRedactor(cp.Classifications),
                    config.AddRedactionDiscriminator ? cp.Name : string.Empty);
 
                jr.Next = jitRedactors;
                jitRedactors = jr;
 
                msgState.RedactedTagArray[i] = new(cp.Name, jr);
            }
            else
            {
                msgState.RedactedTagArray[i] = new(cp.Name, null);
            }
        }
 
        var joiner = ModernJoiner;
        joiner.StaticTags = config.StaticTags;
        joiner.Formatter = formatter;
        joiner.State = msgState;
        joiner.SetIncomingTags(msgState);
 
        List<Exception>? exceptions = null;
 
        // enrich
        foreach (var enricher in config.Enrichers)
        {
            try
            {
                enricher(joiner.EnrichmentTagCollector);
            }
            catch (Exception ex)
            {
                exceptions ??= [];
                exceptions.Add(ex);
            }
        }
 
        // one last dedicated bit of enrichment
        if (exception != null)
        {
            RecordException(exception, joiner.EnrichmentTagCollector, config);
        }
 
        for (int i = 0; i < loggers.Length; i++)
        {
            ref readonly MessageLogger loggerInfo = ref loggers[i];
            if (loggerInfo.IsNotFilteredOut(logLevel))
            {
                try
                {
                    loggerInfo.LoggerLog(logLevel, eventId, joiner, exception, static (s, e) =>
                    {
                        var fmt = s.Formatter!;
                        return fmt(s.State!, e);
                    });
                }
                catch (Exception ex)
                {
                    exceptions ??= [];
                    exceptions.Add(ex);
                }
            }
        }
 
        joiner.Clear();
 
        // return the jit redactors to the pool
        while (jitRedactors != null)
        {
            var next = jitRedactors.Next;
            jitRedactors.Return();
            jitRedactors = next;
        }
 
        HandleExceptions(exceptions);
    }
 
    private void LegacyPath<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
    {
        var loggers = MessageLoggers;
        var config = _factory.Config;
 
        var joiner = LegacyJoiner;
        joiner.StaticTags = config.StaticTags;
        joiner.Formatter = formatter;
        joiner.State = state;
 
        switch (state)
        {
            case IReadOnlyList<KeyValuePair<string, object?>> stateList:
                joiner.SetIncomingTags(stateList);
                break;
 
            case IEnumerable<KeyValuePair<string, object?>> stateList:
                joiner.EnrichmentTagCollector.AddRange(stateList);
                break;
 
            case null:
                break;
 
            default:
                joiner.EnrichmentTagCollector.Add("{OriginalFormat}", state);
                break;
        }
 
        List<Exception>? exceptions = null;
 
        // enrich
        foreach (var enricher in config.Enrichers)
        {
            try
            {
                enricher(joiner.EnrichmentTagCollector);
            }
            catch (Exception ex)
            {
                exceptions ??= [];
                exceptions.Add(ex);
            }
        }
 
        // one last dedicated bit of enrichment
        if (exception != null)
        {
            RecordException(exception, joiner.EnrichmentTagCollector, config);
        }
 
        for (int i = 0; i < loggers.Length; i++)
        {
            ref readonly MessageLogger loggerInfo = ref loggers[i];
            if (loggerInfo.IsNotFilteredOut(logLevel))
            {
                try
                {
                    loggerInfo.Logger.Log(logLevel, eventId, joiner, exception, static (s, e) =>
                    {
                        var fmt = (Func<TState, Exception?, string>)s.Formatter!;
                        return fmt((TState)s.State!, e);
                    });
                }
                catch (Exception ex)
                {
                    exceptions ??= [];
                    exceptions.Add(ex);
                }
            }
        }
 
        joiner.Clear();
        HandleExceptions(exceptions);
    }
}