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.Physical.dll",
"Microsoft.Extensions.FileSystemGlobbing.dll",
"Microsoft.Extensions.Logging.dll",
"Microsoft.Extensions.Logging.Abstractions.dll",
"Microsoft.Extensions.Options.dll",
"Microsoft.Extensions.Primitives.dll",
"Microsoft.JSInterop.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();
RootComponents = new RootComponentMappingCollection();
Services = new ServiceCollection();
Logging = new LoggingBuilder(Services);
Logging.SetMinimumLevel(LogLevel.Warning);
// Retrieve required attributes from JSRuntimeInvoker
InitializeNavigationManager(jsRuntimeInvoker);
@ -128,6 +131,11 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Hosting
/// </summary>
public IWebAssemblyHostEnvironment HostEnvironment { get; }
/// <summary>
/// Gets the logging builder for configuring logging services.
/// </summary>
public ILoggingBuilder Logging { get; }
/// <summary>
/// Registers a <see cref="IServiceProviderFactory{TBuilder}" /> instance to be used to create the <see cref="IServiceProvider" />.
/// </summary>
@ -186,8 +194,9 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Hosting
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.AddLogging(builder => {
builder.AddProvider(new WebAssemblyConsoleLoggerProvider(DefaultWebAssemblyJSRuntime.Instance));
});
}
}
}

View File

@ -10,6 +10,7 @@
<ItemGroup>
<Reference Include="Microsoft.AspNetCore.Components.Web" />
<Reference Include="Microsoft.Extensions.Configuration.Json" />
<Reference Include="Microsoft.Extensions.Logging" />
<Reference Include="Microsoft.JSInterop.WebAssembly" />
</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.
using System;
using System.Collections.Concurrent;
using System.Text;
using Microsoft.Extensions.Logging;
using Microsoft.JSInterop;
@ -43,7 +44,7 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Services
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)

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
var builder = new WebAssemblyHostBuilder(new TestWebAssemblyJSRuntimeInvoker());
// Assert
Assert.Equal(DefaultServiceTypes.Count, builder.Services.Count);
foreach (var type in DefaultServiceTypes)
{
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-trace")).Click();
Browser.FindElement(By.Id("log-debug")).Click();
Browser.FindElement(By.Id("log-information")).Click();
AssertLastLogMessage(LogLevel.Info, "Test log message");
// 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();
AssertLastLogMessage(LogLevel.Warning, "[Custom logger] This is a Warning message with count=5");
Browser.FindElement(By.Id("log-error")).Click();

View File

@ -3,33 +3,42 @@
using System;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Configuration;
using Microsoft.AspNetCore.Components.WebAssembly.Services;
using Microsoft.JSInterop;
namespace BasicTestApp
{
// The goal for this class is to make it possible for E2E tests to observe that a custom
// 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
internal class PrependMessageLoggerProvider : ILoggerProvider
{
private readonly string _message;
private readonly ILoggerFactory _underlyingFactory;
ILogger _logger;
string _message;
ILogger _defaultLogger;
private bool _disposed = false;
public PrependMessageLoggerFactory(string message, ILoggerFactory underlyingFactory)
public PrependMessageLoggerProvider(string message, IJSRuntime runtime)
{
_message = message;
_underlyingFactory = underlyingFactory;
_defaultLogger = new WebAssemblyConsoleLogger<object>(runtime);
}
public void AddProvider(ILoggerProvider provider)
=> _underlyingFactory.AddProvider(provider);
public ILogger CreateLogger(string categoryName)
=> new PrependMessageLogger(_message, _underlyingFactory.CreateLogger(categoryName));
{
if (_logger == null)
{
_logger = new PrependMessageLogger(_message, _defaultLogger);
}
return _logger;
}
public void Dispose()
=> _underlyingFactory.Dispose();
{
if (!_disposed)
{
_logger = null;
}
_disposed = true;
}
private class PrependMessageLogger : ILogger
{

View File

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