Improve LoggerFactory and Logger debugging (#88313)
authorJames Newton-King <james@newtonking.com>
Tue, 4 Jul 2023 01:42:39 +0000 (09:42 +0800)
committerGitHub <noreply@github.com>
Tue, 4 Jul 2023 01:42:39 +0000 (09:42 +0800)
src/libraries/Common/src/Extensions/Logging/DebuggerDisplayFormatting.cs
src/libraries/Microsoft.Extensions.Logging/src/Logger.cs
src/libraries/Microsoft.Extensions.Logging/src/LoggerFactory.cs
src/libraries/Microsoft.Extensions.Logging/src/LoggerFactoryOptions.cs
src/libraries/Microsoft.Extensions.Logging/src/LoggerFilterOptions.cs
src/libraries/Microsoft.Extensions.Logging/src/LoggerInformation.cs

index 6b82ecb..a72def0 100644 (file)
@@ -9,6 +9,28 @@ namespace Microsoft.Extensions.Logging
     {
         internal static string DebuggerToString(string name, ILogger logger)
         {
+            LogLevel? minimumLevel = CalculateEnabledLogLevel(logger);
+
+            var debugText = $@"Name = ""{name}""";
+            if (minimumLevel != null)
+            {
+                debugText += $", MinLevel = {minimumLevel}";
+            }
+            else
+            {
+                // Display "Enabled = false". This makes it clear that the entire ILogger
+                // is disabled and nothing is written.
+                //
+                // If "MinLevel = None" was displayed then someone could think that the
+                // min level is disabled and everything is written.
+                debugText += $", Enabled = false";
+            }
+
+            return debugText;
+        }
+
+        internal static LogLevel? CalculateEnabledLogLevel(ILogger logger)
+        {
             ReadOnlySpan<LogLevel> logLevels = stackalloc LogLevel[]
             {
                 LogLevel.Critical,
@@ -19,7 +41,7 @@ namespace Microsoft.Extensions.Logging
                 LogLevel.Trace,
             };
 
-            LogLevel minimumLevel = LogLevel.None;
+            LogLevel? minimumLevel = null;
 
             // Check log level from highest to lowest. Report the lowest log level.
             foreach (LogLevel logLevel in logLevels)
@@ -32,22 +54,7 @@ namespace Microsoft.Extensions.Logging
                 minimumLevel = logLevel;
             }
 
-            var debugText = $@"Name = ""{name}""";
-            if (minimumLevel != LogLevel.None)
-            {
-                debugText += $", MinLevel = {minimumLevel}";
-            }
-            else
-            {
-                // Display "Enabled = false". This makes it clear that the entire ILogger
-                // is disabled and nothing is written.
-                //
-                // If "MinLevel = None" was displayed then someone could think that the
-                // min level is disabled and everything is written.
-                debugText += $", Enabled = false";
-            }
-
-            return debugText;
+            return minimumLevel;
         }
     }
 }
index 1bed6fb..1443230 100644 (file)
@@ -4,10 +4,12 @@
 using System;
 using System.Collections.Generic;
 using System.Diagnostics;
+using System.Linq;
 
 namespace Microsoft.Extensions.Logging
 {
     [DebuggerDisplay("{DebuggerToString(),nq}")]
+    [DebuggerTypeProxy(typeof(LoggerDebugView))]
     internal sealed class Logger : ILogger
     {
         private readonly string _categoryName;
@@ -150,15 +152,103 @@ namespace Microsoft.Extensions.Logging
             return scope;
         }
 
+        private static void ThrowLoggingError(List<Exception> exceptions)
+        {
+            throw new AggregateException(
+                message: "An error occurred while writing to logger(s).", innerExceptions: exceptions);
+        }
+
         internal string DebuggerToString()
         {
             return DebuggerDisplayFormatting.DebuggerToString(_categoryName, this);
         }
 
-        private static void ThrowLoggingError(List<Exception> exceptions)
+        private sealed class LoggerDebugView(Logger logger)
         {
-            throw new AggregateException(
-                message: "An error occurred while writing to logger(s).", innerExceptions: exceptions);
+            public string Name => logger._categoryName;
+
+            // The list of providers includes the full list of configured providers from the logger factory.
+            // It then mentions the min level and enable status of each provider for this logger.
+            public List<LoggerProviderDebugView> Providers
+            {
+                get
+                {
+                    List<LoggerProviderDebugView> providers = new List<LoggerProviderDebugView>();
+                    for (int i = 0; i < logger.Loggers.Length; i++)
+                    {
+                        LoggerInformation loggerInfo = logger.Loggers[i];
+                        string providerName = ProviderAliasUtilities.GetAlias(loggerInfo.ProviderType) ?? loggerInfo.ProviderType.Name;
+                        MessageLogger? messageLogger = logger.MessageLoggers?.FirstOrDefault(messageLogger => messageLogger.Logger == loggerInfo.Logger);
+
+                        providers.Add(new LoggerProviderDebugView(providerName, messageLogger));
+                    }
+
+                    return providers;
+                }
+            }
+
+            public List<object?>? Scopes
+            {
+                get
+                {
+                    var scopeProvider = logger.ScopeLoggers?.FirstOrDefault().ExternalScopeProvider;
+                    if (scopeProvider == null)
+                    {
+                        return null;
+                    }
+
+                    List<object?> scopes = new List<object?>();
+                    scopeProvider.ForEachScope((scope, scopes) => scopes!.Add(scope), scopes);
+                    return scopes;
+                }
+            }
+            public LogLevel? MinLevel => DebuggerDisplayFormatting.CalculateEnabledLogLevel(logger);
+            public bool Enabled => DebuggerDisplayFormatting.CalculateEnabledLogLevel(logger) != null;
+        }
+
+        [DebuggerDisplay("{DebuggerToString(),nq}")]
+        private sealed class LoggerProviderDebugView(string providerName, MessageLogger? messageLogger)
+        {
+            public string Name => providerName;
+            public LogLevel LogLevel => CalculateEnabledLogLevel(messageLogger) ?? LogLevel.None;
+
+            private static LogLevel? CalculateEnabledLogLevel(MessageLogger? logger)
+            {
+                if (logger == null)
+                {
+                    return null;
+                }
+
+                ReadOnlySpan<LogLevel> logLevels = stackalloc LogLevel[]
+                {
+                    LogLevel.Critical,
+                    LogLevel.Error,
+                    LogLevel.Warning,
+                    LogLevel.Information,
+                    LogLevel.Debug,
+                    LogLevel.Trace,
+                };
+
+                LogLevel? minimumLevel = null;
+
+                // Check log level from highest to lowest. Report the lowest log level.
+                foreach (LogLevel logLevel in logLevels)
+                {
+                    if (!logger.Value.IsEnabled(logLevel))
+                    {
+                        break;
+                    }
+
+                    minimumLevel = logLevel;
+                }
+
+                return minimumLevel;
+            }
+
+            private string DebuggerToString()
+            {
+                return $@"Name = ""{providerName}"", LogLevel = {LogLevel}";
+            }
         }
 
         private sealed class Scope : IDisposable
index 305860f..2cb0994 100644 (file)
@@ -4,7 +4,9 @@
 using System;
 using System.Collections.Concurrent;
 using System.Collections.Generic;
+using System.Diagnostics;
 using System.Diagnostics.CodeAnalysis;
+using System.Linq;
 using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.Options;
 
@@ -13,6 +15,8 @@ namespace Microsoft.Extensions.Logging
     /// <summary>
     /// Produces instances of <see cref="ILogger"/> classes based on the given providers.
     /// </summary>
+    [DebuggerDisplay("{DebuggerToString(),nq}")]
+    [DebuggerTypeProxy(typeof(LoggerFactoryDebugView))]
     public class LoggerFactory : ILoggerFactory
     {
         private readonly ConcurrentDictionary<string, Logger> _loggers = new ConcurrentDictionary<string, Logger>(StringComparer.Ordinal);
@@ -314,5 +318,17 @@ namespace Microsoft.Extensions.Logging
                 _loggerFactory.AddProvider(provider);
             }
         }
+
+        private string DebuggerToString()
+        {
+            return $"Providers = {_providerRegistrations.Count}, {_filterOptions.DebuggerToString()}";
+        }
+
+        private sealed class LoggerFactoryDebugView(LoggerFactory loggerFactory)
+        {
+            public List<ILoggerProvider> Providers => loggerFactory._providerRegistrations.Select(r => r.Provider).ToList();
+            public bool Disposed => loggerFactory._disposed;
+            public LoggerFilterOptions FilterOptions => loggerFactory._filterOptions;
+        }
     }
 }
index fbc0e2f..996f08c 100644 (file)
@@ -1,13 +1,14 @@
 // Licensed to the .NET Foundation under one or more agreements.
 // The .NET Foundation licenses this file to you under the MIT license.
 
-using System.Collections.Generic;
+using System.Diagnostics;
 
 namespace Microsoft.Extensions.Logging
 {
     /// <summary>
     /// The options for a LoggerFactory.
     /// </summary>
+    [DebuggerDisplay("ActivityTrackingOptions = {ActivityTrackingOptions}")]
     public class LoggerFactoryOptions
     {
         /// <summary>
index 3c8165e..b19970c 100644 (file)
@@ -2,12 +2,14 @@
 // The .NET Foundation licenses this file to you under the MIT license.
 
 using System.Collections.Generic;
+using System.Diagnostics;
 
 namespace Microsoft.Extensions.Logging
 {
     /// <summary>
     /// The options for a LoggerFilter.
     /// </summary>
+    [DebuggerDisplay("{DebuggerToString(),nq}")]
     public class LoggerFilterOptions
     {
         /// <summary>
@@ -32,5 +34,30 @@ namespace Microsoft.Extensions.Logging
 
         // Concrete representation of the rule list
         internal List<LoggerFilterRule> RulesInternal { get; } = new List<LoggerFilterRule>();
+
+        internal string DebuggerToString()
+        {
+            string debugText;
+            if (MinLevel != LogLevel.None)
+            {
+                debugText = $"MinLevel = {MinLevel}";
+            }
+            else
+            {
+                // Display "Enabled = false". This makes it clear that the entire ILogger
+                // is disabled and nothing is written.
+                //
+                // If "MinLevel = None" was displayed then someone could think that the
+                // min level is disabled and everything is written.
+                debugText = $"Enabled = false";
+            }
+
+            if (Rules.Count > 0)
+            {
+                debugText += $", Rules = {Rules.Count}";
+            }
+
+            return debugText;
+        }
     }
 }
index 1a0a01a..92ba477 100644 (file)
@@ -8,7 +8,7 @@ namespace Microsoft.Extensions.Logging
 {
     internal readonly struct MessageLogger
     {
-        public MessageLogger(ILogger logger, string? category, string? providerTypeFullName, LogLevel? minLevel, Func<string?, string?, LogLevel, bool>? filter)
+        public MessageLogger(ILogger logger, string category, string? providerTypeFullName, LogLevel? minLevel, Func<string?, string?, LogLevel, bool>? filter)
         {
             Logger = logger;
             Category = category;
@@ -19,7 +19,7 @@ namespace Microsoft.Extensions.Logging
 
         public ILogger Logger { get; }
 
-        public string? Category { get; }
+        public string Category { get; }
 
         private string? ProviderTypeFullName { get; }