Skip to content

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

Closed
wants to merge 1 commit into from
Closed
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 @@ -186,8 +186,7 @@ internal void InitializeDefaultServices()
Services.AddSingleton<IJSRuntime>(DefaultWebAssemblyJSRuntime.Instance);
Services.AddSingleton<NavigationManager>(WebAssemblyNavigationManager.Instance);
Services.AddSingleton<INavigationInterception>(WebAssemblyNavigationInterception.Instance);
Services.AddSingleton<ILoggerFactory, WebAssemblyLoggerFactory>();
Services.TryAdd(ServiceDescriptor.Singleton(typeof(ILogger<>), typeof(WebAssemblyConsoleLogger<>)));
Services.AddSingleton<ILoggerFactory>(s => new WebAssemblyLoggerFactory(new List<ILoggerProvider> { new WebAssemblyConsoleLoggerProvider(DefaultWebAssemblyJSRuntime.Instance) }));
}
}
}
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
{
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));
Copy link
Member

@SteveSandersonMS SteveSandersonMS Mar 31, 2020

Choose a reason for hiding this comment

The 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 name that's already registered, since the underlying dictionary can't store more than one with the same name.

Copy link
Member Author

Choose a reason for hiding this comment

The 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.

since the underlying dictionary can't store more than one with the same name.

Hm. GetOrAdd is used here so that constraint should be satisfied even if the user calls CreateLogger("someName") twice.

}

/// <inheritdoc />
public void Dispose()
{
if (!_disposed)
{
_loggers = null;
Copy link
Member

Choose a reason for hiding this comment

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

Is there any chance we need to Dispose the ILogger instances before nulling this out?

_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;
Copy link
Member

Choose a reason for hiding this comment

The 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?

Copy link
Member Author

Choose a reason for hiding this comment

The 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:

  • It's more helpful to the developer to get all the exceptions from buggy loggers instead of getting them one-by-one.
  • Wanted to follow existing experiences that devs would be used to. This didn't seem, outrageous enough to nuke.

}
}

/// <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
{
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)
{
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; }
}
}
Loading