-
Notifications
You must be signed in to change notification settings - Fork 10.4k
Add support for provider registration in logging #20215
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
// Copyright (c) .NET Foundation. All rights reserved. | ||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. | ||
|
||
using System; | ||
using System.Collections.Concurrent; | ||
using System.Runtime.InteropServices; | ||
using Microsoft.Extensions.Logging; | ||
using Microsoft.Extensions.Options; | ||
using Microsoft.JSInterop; | ||
|
||
|
||
namespace Microsoft.AspNetCore.Components.WebAssembly.Services | ||
{ | ||
/// <summary> | ||
/// A provider of <see cref="WebAssemblyConsoleLogger{T}"/> instances. | ||
/// </summary> | ||
public class WebAssemblyConsoleLoggerProvider : ILoggerProvider | ||
captainsafia marked this conversation as resolved.
Show resolved
Hide resolved
|
||
{ | ||
private ConcurrentDictionary<string, WebAssemblyConsoleLogger<object>> _loggers; | ||
private IJSInProcessRuntime _jsRuntime; | ||
private bool _disposed; | ||
|
||
/// <summary> | ||
/// Creates an instance of <see cref="WebAssemblyConsoleLoggerProvider"/>. | ||
/// </summary> | ||
/// <param name="options">The options to create <see cref="WebAssemblyConsoleLogger"/> instances with.</param> | ||
public WebAssemblyConsoleLoggerProvider(IJSInProcessRuntime jsRuntime) | ||
{ | ||
_loggers = new ConcurrentDictionary<string, WebAssemblyConsoleLogger<object>>(); | ||
_jsRuntime = jsRuntime; | ||
} | ||
|
||
/// <inheritdoc /> | ||
public ILogger CreateLogger(string name) | ||
{ | ||
return _loggers.GetOrAdd(name, loggerName => new WebAssemblyConsoleLogger<object>(name, _jsRuntime)); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe this implementation comes from somewhere else, but I'm surprised it's not an error to call this with a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is based on the implementation of the ConsoleLoggerProvider in Microsoft.Extensions.Logging.
Hm. |
||
} | ||
|
||
/// <inheritdoc /> | ||
public void Dispose() | ||
{ | ||
if (!_disposed) | ||
{ | ||
_loggers = null; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is there any chance we need to |
||
_jsRuntime = null; | ||
} | ||
_disposed = true; | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,134 @@ | ||
// Copyright (c) .NET Foundation. All rights reserved. | ||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. | ||
|
||
using System; | ||
using System.Collections.Generic; | ||
using Microsoft.Extensions.Logging; | ||
|
||
namespace Microsoft.AspNetCore.Components.WebAssembly.Services | ||
{ | ||
internal class Logger : ILogger | ||
{ | ||
public WebAssemblyLoggerInformation[] Loggers { get; set; } | ||
|
||
/// <inheritdoc /> | ||
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter) | ||
{ | ||
var loggers = Loggers; | ||
if (loggers == null) | ||
{ | ||
return; | ||
} | ||
|
||
List<Exception> exceptions = null; | ||
for (var i = 0; i < loggers.Length; i++) | ||
{ | ||
ref readonly var loggerInfo = ref loggers[i]; | ||
var logger = loggerInfo.Logger; | ||
if (!logger.IsEnabled(logLevel)) | ||
{ | ||
continue; | ||
} | ||
|
||
LoggerLog(logLevel, eventId, logger, exception, formatter, ref exceptions, state); | ||
} | ||
|
||
if (exceptions != null && exceptions.Count > 0) | ||
{ | ||
ThrowLoggingError(exceptions); | ||
} | ||
|
||
static void LoggerLog(LogLevel logLevel, EventId eventId, ILogger logger, Exception exception, Func<TState, Exception, string> formatter, ref List<Exception> exceptions, in TState state) | ||
{ | ||
try | ||
{ | ||
logger.Log(logLevel, eventId, state, exception, formatter); | ||
} | ||
catch (Exception ex) | ||
{ | ||
if (exceptions == null) | ||
{ | ||
exceptions = new List<Exception>(); | ||
} | ||
|
||
exceptions.Add(ex); | ||
} | ||
} | ||
} | ||
|
||
/// <inheritdoc /> | ||
public bool IsEnabled(LogLevel logLevel) | ||
{ | ||
var loggers = Loggers; | ||
if (loggers == null) | ||
{ | ||
return false; | ||
} | ||
|
||
List<Exception> exceptions = null; | ||
var i = 0; | ||
for (; i < loggers.Length; i++) | ||
{ | ||
ref readonly var loggerInfo = ref loggers[i]; | ||
var logger = loggerInfo.Logger; | ||
if (!logger.IsEnabled(logLevel)) | ||
{ | ||
continue; | ||
} | ||
|
||
if (LoggerIsEnabled(logLevel, loggerInfo.Logger, ref exceptions)) | ||
{ | ||
break; | ||
} | ||
} | ||
|
||
if (exceptions != null && exceptions.Count > 0) | ||
{ | ||
ThrowLoggingError(exceptions); | ||
} | ||
|
||
return i < loggers.Length ? true : false; | ||
|
||
static bool LoggerIsEnabled(LogLevel logLevel, ILogger logger, ref List<Exception> exceptions) | ||
{ | ||
try | ||
{ | ||
if (logger.IsEnabled(logLevel)) | ||
{ | ||
return true; | ||
} | ||
} | ||
catch (Exception ex) | ||
{ | ||
if (exceptions == null) | ||
{ | ||
exceptions = new List<Exception>(); | ||
} | ||
|
||
exceptions.Add(ex); | ||
} | ||
|
||
return false; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This logic to aggregate exceptions looks super robust, though TBH I'm not certain what the use case for it is. If we didn't catch any of this, and just let the first exception be unhandled (since it's never a legit scenario to have loggers throw), would that be sufficient? Are there cases where it's necessary to collect more information than that? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is heavily inspired by the Logger implementation in Microsoft.Extensions.Logging. The intent behind this logic was to collect all the exceptions that arise from each logger that is registered and throw an aggregate exception for them. It is possible to bail and throw at the first exception thrown from a logger but I figured I would keep this logic because:
|
||
} | ||
} | ||
|
||
/// <inheritdoc /> | ||
public IDisposable BeginScope<TState>(TState state) | ||
{ | ||
return NoOpDisposable.Instance; | ||
} | ||
|
||
private static void ThrowLoggingError(List<Exception> exceptions) | ||
{ | ||
throw new AggregateException( | ||
message: "An error occurred while writing to logger(s).", innerExceptions: exceptions); | ||
} | ||
} | ||
|
||
public class NoOpDisposable : IDisposable | ||
{ | ||
public static NoOpDisposable Instance = new NoOpDisposable(); | ||
|
||
public void Dispose() { } | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,150 @@ | ||
// Copyright (c) .NET Foundation. All rights reserved. | ||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. | ||
|
||
using System; | ||
using System.Collections.Generic; | ||
using System.Linq; | ||
using Microsoft.Extensions.DependencyInjection; | ||
using Microsoft.Extensions.Logging; | ||
using Microsoft.Extensions.Options; | ||
using Microsoft.JSInterop; | ||
|
||
namespace Microsoft.AspNetCore.Components.WebAssembly.Services | ||
{ | ||
/// <summary> | ||
/// Produces instances of <see cref="ILogger"/> classes based on the given providers. | ||
/// </summary> | ||
public class WebAssemblyLoggerFactory : ILoggerFactory | ||
captainsafia marked this conversation as resolved.
Show resolved
Hide resolved
|
||
{ | ||
private readonly Dictionary<string, Logger> _loggers = new Dictionary<string, Logger>(); | ||
private readonly List<ProviderRegistration> _providerRegistrations = new List<ProviderRegistration>(); | ||
private readonly object _sync = new object(); | ||
private volatile bool _disposed; | ||
|
||
public WebAssemblyLoggerFactory() : this(Enumerable.Empty<ILoggerProvider>()) { } | ||
|
||
/// <summary> | ||
/// Creates a new <see cref="WebAssemblyLoggerFactory"/> instance. | ||
/// </summary> | ||
/// <param name="providers">The providers to use in producing <see cref="ILogger"/> instances.</param> | ||
public WebAssemblyLoggerFactory(IEnumerable<ILoggerProvider> providers) | ||
{ | ||
foreach (var provider in providers) | ||
{ | ||
AddProviderRegistration(provider, dispose: false); | ||
} | ||
} | ||
|
||
/// <summary> | ||
/// Creates an <see cref="ILogger"/> with the given <paramref name="categoryName"/>. | ||
/// </summary> | ||
/// <param name="categoryName">The category name for messages produced by the logger.</param> | ||
/// <returns>The <see cref="ILogger"/> that was created.</returns> | ||
public ILogger CreateLogger(string categoryName) | ||
{ | ||
if (CheckDisposed()) | ||
{ | ||
throw new ObjectDisposedException(nameof(WebAssemblyLoggerFactory)); | ||
} | ||
|
||
lock (_sync) | ||
{ | ||
if (!_loggers.TryGetValue(categoryName, out var logger)) | ||
{ | ||
logger = new Logger | ||
{ | ||
Loggers = CreateLoggers(categoryName), | ||
}; | ||
|
||
_loggers[categoryName] = logger; | ||
} | ||
|
||
return logger; | ||
} | ||
} | ||
|
||
/// <summary> | ||
/// Adds the given provider to those used in creating <see cref="ILogger"/> instances. | ||
/// </summary> | ||
/// <param name="provider">The <see cref="ILoggerProvider"/> to add.</param> | ||
public void AddProvider(ILoggerProvider provider) | ||
{ | ||
if (CheckDisposed()) | ||
{ | ||
throw new ObjectDisposedException(nameof(WebAssemblyLoggerFactory)); | ||
} | ||
|
||
lock (_sync) | ||
captainsafia marked this conversation as resolved.
Show resolved
Hide resolved
|
||
{ | ||
AddProviderRegistration(provider, dispose: true); | ||
|
||
foreach (var existingLogger in _loggers) | ||
{ | ||
var logger = existingLogger.Value; | ||
var WebAssemblyLoggerInformation = logger.Loggers; | ||
|
||
var newLoggerIndex = WebAssemblyLoggerInformation.Length; | ||
Array.Resize(ref WebAssemblyLoggerInformation, WebAssemblyLoggerInformation.Length + 1); | ||
WebAssemblyLoggerInformation[newLoggerIndex] = new WebAssemblyLoggerInformation(provider, existingLogger.Key); | ||
|
||
logger.Loggers = WebAssemblyLoggerInformation; | ||
} | ||
} | ||
} | ||
|
||
private void AddProviderRegistration(ILoggerProvider provider, bool dispose) | ||
{ | ||
_providerRegistrations.Add(new ProviderRegistration | ||
{ | ||
Provider = provider, | ||
ShouldDispose = dispose | ||
}); | ||
} | ||
|
||
private WebAssemblyLoggerInformation[] CreateLoggers(string categoryName) | ||
{ | ||
var loggers = new WebAssemblyLoggerInformation[_providerRegistrations.Count]; | ||
for (var i = 0; i < _providerRegistrations.Count; i++) | ||
{ | ||
loggers[i] = new WebAssemblyLoggerInformation(_providerRegistrations[i].Provider, categoryName); | ||
} | ||
return loggers; | ||
} | ||
|
||
/// <summary> | ||
/// Check if the factory has been disposed. | ||
/// </summary> | ||
/// <returns>True when <see cref="Dispose()"/> as been called</returns> | ||
protected virtual bool CheckDisposed() => _disposed; | ||
|
||
/// <inheritdoc/> | ||
public void Dispose() | ||
{ | ||
if (!_disposed) | ||
{ | ||
_disposed = true; | ||
|
||
foreach (var registration in _providerRegistrations) | ||
{ | ||
try | ||
{ | ||
if (registration.ShouldDispose) | ||
{ | ||
registration.Provider.Dispose(); | ||
} | ||
} | ||
catch | ||
{ | ||
// Swallow exceptions on dispose | ||
} | ||
} | ||
} | ||
} | ||
|
||
private struct ProviderRegistration | ||
{ | ||
public ILoggerProvider Provider; | ||
public bool ShouldDispose; | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
// Copyright (c) .NET Foundation. All rights reserved. | ||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. | ||
|
||
using System; | ||
using Microsoft.Extensions.Logging; | ||
|
||
namespace Microsoft.AspNetCore.Components.WebAssembly.Services | ||
{ | ||
internal readonly struct WebAssemblyLoggerInformation | ||
{ | ||
public WebAssemblyLoggerInformation(ILoggerProvider provider, string category) : this() | ||
{ | ||
ProviderType = provider.GetType(); | ||
Logger = provider.CreateLogger(category); | ||
Category = category; | ||
} | ||
|
||
public ILogger Logger { get; } | ||
|
||
public string Category { get; } | ||
|
||
public Type ProviderType { get; } | ||
} | ||
} |
Uh oh!
There was an error while loading. Please reload this page.