Remove WebAssemblyLoggerFactory and refactor logging setup (#20432)

**Changes in this PR**
 - Replaces `WebAssemblyLoggerFactory` with `LoggerFactory` from logging extensions package
 - Moves WebAssemblyConsoleLogger and PrependMessageLogger to provider model

Now that we are using the standard `LoggerFactory` support for config options like `SetMinimumLevel` and `AddProvider` is available.

Compared to what is currently in the `blazor-wasm` branch, the changes in this PR add an additional 12 kb to the total compressed size.

Addresses #19737
This commit is contained in:
Safia Abdalla 2020-04-03 10:22:05 -07:00 committed by GitHub
parent 41a9588353
commit e67e7a08ca
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 126 additions and 62 deletions

View File

@ -56,7 +56,9 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Build
"Microsoft.Extensions.FileProviders.Abstractions.dll", "Microsoft.Extensions.FileProviders.Abstractions.dll",
"Microsoft.Extensions.FileProviders.Physical.dll", "Microsoft.Extensions.FileProviders.Physical.dll",
"Microsoft.Extensions.FileSystemGlobbing.dll", "Microsoft.Extensions.FileSystemGlobbing.dll",
"Microsoft.Extensions.Logging.dll",
"Microsoft.Extensions.Logging.Abstractions.dll", "Microsoft.Extensions.Logging.Abstractions.dll",
"Microsoft.Extensions.Options.dll",
"Microsoft.Extensions.Primitives.dll", "Microsoft.Extensions.Primitives.dll",
"Microsoft.JSInterop.dll", "Microsoft.JSInterop.dll",
"Microsoft.JSInterop.WebAssembly.dll", "Microsoft.JSInterop.WebAssembly.dll",

View File

@ -0,0 +1,18 @@
// 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 Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
namespace Microsoft.AspNetCore.Components.WebAssembly.Hosting
{
internal class LoggingBuilder : ILoggingBuilder
{
public LoggingBuilder(IServiceCollection services)
{
Services = services;
}
public IServiceCollection Services { get; }
}
}

View File

@ -55,6 +55,9 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Hosting
Configuration = new ConfigurationBuilder(); Configuration = new ConfigurationBuilder();
RootComponents = new RootComponentMappingCollection(); RootComponents = new RootComponentMappingCollection();
Services = new ServiceCollection(); Services = new ServiceCollection();
Logging = new LoggingBuilder(Services);
Logging.SetMinimumLevel(LogLevel.Warning);
// Retrieve required attributes from JSRuntimeInvoker // Retrieve required attributes from JSRuntimeInvoker
InitializeNavigationManager(jsRuntimeInvoker); InitializeNavigationManager(jsRuntimeInvoker);
@ -128,6 +131,11 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Hosting
/// </summary> /// </summary>
public IWebAssemblyHostEnvironment HostEnvironment { get; } public IWebAssemblyHostEnvironment HostEnvironment { get; }
/// <summary>
/// Gets the logging builder for configuring logging services.
/// </summary>
public ILoggingBuilder Logging { get; }
/// <summary> /// <summary>
/// Registers a <see cref="IServiceProviderFactory{TBuilder}" /> instance to be used to create the <see cref="IServiceProvider" />. /// Registers a <see cref="IServiceProviderFactory{TBuilder}" /> instance to be used to create the <see cref="IServiceProvider" />.
/// </summary> /// </summary>
@ -186,8 +194,9 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Hosting
Services.AddSingleton<IJSRuntime>(DefaultWebAssemblyJSRuntime.Instance); Services.AddSingleton<IJSRuntime>(DefaultWebAssemblyJSRuntime.Instance);
Services.AddSingleton<NavigationManager>(WebAssemblyNavigationManager.Instance); Services.AddSingleton<NavigationManager>(WebAssemblyNavigationManager.Instance);
Services.AddSingleton<INavigationInterception>(WebAssemblyNavigationInterception.Instance); Services.AddSingleton<INavigationInterception>(WebAssemblyNavigationInterception.Instance);
Services.AddSingleton<ILoggerFactory, WebAssemblyLoggerFactory>(); Services.AddLogging(builder => {
Services.TryAdd(ServiceDescriptor.Singleton(typeof(ILogger<>), typeof(WebAssemblyConsoleLogger<>))); builder.AddProvider(new WebAssemblyConsoleLoggerProvider(DefaultWebAssemblyJSRuntime.Instance));
});
} }
} }
} }

View File

@ -10,6 +10,7 @@
<ItemGroup> <ItemGroup>
<Reference Include="Microsoft.AspNetCore.Components.Web" /> <Reference Include="Microsoft.AspNetCore.Components.Web" />
<Reference Include="Microsoft.Extensions.Configuration.Json" /> <Reference Include="Microsoft.Extensions.Configuration.Json" />
<Reference Include="Microsoft.Extensions.Logging" />
<Reference Include="Microsoft.JSInterop.WebAssembly" /> <Reference Include="Microsoft.JSInterop.WebAssembly" />
</ItemGroup> </ItemGroup>

View File

@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using System.Collections.Concurrent;
using System.Text; using System.Text;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.JSInterop; using Microsoft.JSInterop;
@ -43,7 +44,7 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Services
public bool IsEnabled(LogLevel logLevel) public bool IsEnabled(LogLevel logLevel)
{ {
return logLevel >= LogLevel.Warning && logLevel != LogLevel.None; return logLevel != LogLevel.None;
} }
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter) public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)

View File

@ -0,0 +1,45 @@
// 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 Microsoft.Extensions.Logging;
using Microsoft.JSInterop;
namespace Microsoft.AspNetCore.Components.WebAssembly.Services
{
/// <summary>
/// A provider of <see cref="WebAssemblyConsoleLogger{T}"/> instances.
/// </summary>
internal class WebAssemblyConsoleLoggerProvider : ILoggerProvider
{
private readonly ConcurrentDictionary<string, WebAssemblyConsoleLogger<object>> _loggers;
private readonly 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));
}
/// <inheritdoc />
public void Dispose()
{
if (!_disposed)
{
_disposed = true;
}
}
}
}

View File

@ -1,32 +0,0 @@
// 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.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.JSInterop;
namespace Microsoft.AspNetCore.Components.WebAssembly.Services
{
internal class WebAssemblyLoggerFactory : ILoggerFactory
{
private readonly IJSInProcessRuntime _jsRuntime;
public WebAssemblyLoggerFactory(IServiceProvider services)
{
_jsRuntime = (IJSInProcessRuntime)services.GetRequiredService<IJSRuntime>();
}
// We might implement this in the future, but it's not required currently
public void AddProvider(ILoggerProvider provider)
=> throw new NotSupportedException();
public ILogger CreateLogger(string categoryName)
=> new WebAssemblyConsoleLogger<object>(categoryName, _jsRuntime);
public void Dispose()
{
// No-op
}
}
}

View File

@ -230,12 +230,28 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Hosting
// Arrange & Act // Arrange & Act
var builder = new WebAssemblyHostBuilder(new TestWebAssemblyJSRuntimeInvoker()); var builder = new WebAssemblyHostBuilder(new TestWebAssemblyJSRuntimeInvoker());
// Assert
Assert.Equal(DefaultServiceTypes.Count, builder.Services.Count);
foreach (var type in DefaultServiceTypes) foreach (var type in DefaultServiceTypes)
{ {
Assert.Single(builder.Services, d => d.ServiceType == type); Assert.Single(builder.Services, d => d.ServiceType == type);
} }
} }
[Fact]
public void Builder_SupportsConfiguringLogging()
{
// Arrange
var builder = new WebAssemblyHostBuilder(new TestWebAssemblyJSRuntimeInvoker());
var provider = new Mock<ILoggerProvider>();
// Act
builder.Logging.AddProvider(provider.Object);
var host = builder.Build();
// Assert
var loggerProvider = host.Services.GetRequiredService<ILoggerProvider>();
Assert.NotNull(loggerProvider);
Assert.Equal<ILoggerProvider>(provider.Object, loggerProvider);
}
} }
} }

View File

@ -85,10 +85,11 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
Browser.FindElement(By.Id("log-none")).Click(); Browser.FindElement(By.Id("log-none")).Click();
Browser.FindElement(By.Id("log-trace")).Click(); Browser.FindElement(By.Id("log-trace")).Click();
Browser.FindElement(By.Id("log-debug")).Click(); Browser.FindElement(By.Id("log-debug")).Click();
Browser.FindElement(By.Id("log-information")).Click();
AssertLastLogMessage(LogLevel.Info, "Test log message"); AssertLastLogMessage(LogLevel.Info, "Test log message");
// These severity levels are displayed // These severity levels are displayed
Browser.FindElement(By.Id("log-information")).Click();
AssertLastLogMessage(LogLevel.Info, "[Custom logger] This is a Information message with count=4");
Browser.FindElement(By.Id("log-warning")).Click(); Browser.FindElement(By.Id("log-warning")).Click();
AssertLastLogMessage(LogLevel.Warning, "[Custom logger] This is a Warning message with count=5"); AssertLastLogMessage(LogLevel.Warning, "[Custom logger] This is a Warning message with count=5");
Browser.FindElement(By.Id("log-error")).Click(); Browser.FindElement(By.Id("log-error")).Click();

View File

@ -3,33 +3,42 @@
using System; using System;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Configuration;
using Microsoft.AspNetCore.Components.WebAssembly.Services;
using Microsoft.JSInterop;
namespace BasicTestApp namespace BasicTestApp
{ {
// The goal for this class is to make it possible for E2E tests to observe that a custom internal class PrependMessageLoggerProvider : ILoggerProvider
// logger factory can be plugged in and gets used when logging unhandled exceptions.
// However, it's valuable to pass through all calls to the default implementation too
// so that if any defect in the underlying implementation would break tests, we still see it.
public class PrependMessageLoggerFactory : ILoggerFactory
{ {
private readonly string _message; ILogger _logger;
private readonly ILoggerFactory _underlyingFactory; string _message;
ILogger _defaultLogger;
private bool _disposed = false;
public PrependMessageLoggerFactory(string message, ILoggerFactory underlyingFactory) public PrependMessageLoggerProvider(string message, IJSRuntime runtime)
{ {
_message = message; _message = message;
_underlyingFactory = underlyingFactory; _defaultLogger = new WebAssemblyConsoleLogger<object>(runtime);
} }
public void AddProvider(ILoggerProvider provider)
=> _underlyingFactory.AddProvider(provider);
public ILogger CreateLogger(string categoryName) public ILogger CreateLogger(string categoryName)
=> new PrependMessageLogger(_message, _underlyingFactory.CreateLogger(categoryName)); {
if (_logger == null)
{
_logger = new PrependMessageLogger(_message, _defaultLogger);
}
return _logger;
}
public void Dispose() public void Dispose()
=> _underlyingFactory.Dispose(); {
if (!_disposed)
{
_logger = null;
}
_disposed = true;
}
private class PrependMessageLogger : ILogger private class PrependMessageLogger : ILogger
{ {

View File

@ -16,6 +16,7 @@ using Microsoft.AspNetCore.Components.WebAssembly.Http;
using Microsoft.AspNetCore.Components.WebAssembly.Services; using Microsoft.AspNetCore.Components.WebAssembly.Services;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.JSInterop;
namespace BasicTestApp namespace BasicTestApp
{ {
@ -44,15 +45,8 @@ namespace BasicTestApp
policy.RequireAssertion(ctx => ctx.User.Identity.Name?.StartsWith("B") ?? false)); policy.RequireAssertion(ctx => ctx.User.Identity.Name?.StartsWith("B") ?? false));
}); });
// Replace the default logger with a custom one that wraps it builder.Logging.Services.AddSingleton<ILoggerProvider, PrependMessageLoggerProvider>(s => new PrependMessageLoggerProvider("Custom logger", s.GetService<IJSRuntime>()));
var originalLoggerDescriptor = builder.Services.Single(d => d.ServiceType == typeof(ILoggerFactory)); builder.Logging.SetMinimumLevel(LogLevel.Information);
builder.Services.AddSingleton<ILoggerFactory>(services =>
{
var originalLogger = (ILoggerFactory)Activator.CreateInstance(
originalLoggerDescriptor.ImplementationType,
new object[] { services });
return new PrependMessageLoggerFactory("Custom logger", originalLogger);
});
var host = builder.Build(); var host = builder.Build();
ConfigureCulture(host); ConfigureCulture(host);