Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve LoggerFactory and Logger debugging #88313

Merged
merged 5 commits into from
Jul 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,28 @@ namespace Microsoft.Extensions.Logging
internal static class DebuggerDisplayFormatting
{
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[]
{
Expand All @@ -19,7 +41,7 @@ internal static string DebuggerToString(string name, ILogger logger)
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)
Expand All @@ -32,22 +54,7 @@ internal static string DebuggerToString(string name, ILogger logger)
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;
}
}
}
96 changes: 93 additions & 3 deletions src/libraries/Microsoft.Extensions.Logging/src/Logger.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -150,15 +152,103 @@ static bool LoggerIsEnabled(LogLevel logLevel, ILogger logger, ref List<Exceptio
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;
}
}
JamesNK marked this conversation as resolved.
Show resolved Hide resolved
JamesNK marked this conversation as resolved.
Show resolved Hide resolved

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)
tarekgh marked this conversation as resolved.
Show resolved Hide resolved
{
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
Expand Down
16 changes: 16 additions & 0 deletions src/libraries/Microsoft.Extensions.Logging/src/LoggerFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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);
Expand Down Expand Up @@ -314,5 +318,17 @@ public void AddProvider(ILoggerProvider provider)
_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;
}
}
}
Original file line number Diff line number Diff line change
@@ -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>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand All @@ -32,5 +34,30 @@ public LoggerFilterOptions() { }

// 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;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -19,7 +19,7 @@ public MessageLogger(ILogger logger, string? category, string? providerTypeFullN

public ILogger Logger { get; }

public string? Category { get; }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

public string? Category { get; }

are we sure the category never be null?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nullable analysis says it can't. I changed it because I didn't see any code paths that allowed null.

public string Category { get; }

private string? ProviderTypeFullName { get; }

Expand Down