Skip to content

Commit

Permalink
feat(templates): improve Boilerplate logging #9136 (#9137)
Browse files Browse the repository at this point in the history
  • Loading branch information
ysmoradi authored Nov 7, 2024
1 parent df7bdcc commit 3d1ece7
Show file tree
Hide file tree
Showing 22 changed files with 364 additions and 190 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using Microsoft.Extensions.Logging;
//#if (signalr == true)
using Microsoft.AspNetCore.SignalR.Client;
using Boilerplate.Client.Core.Services.HttpMessageHandlers;
//#endif
//#if (appInsights == true)
using BlazorApplicationInsights.Interfaces;
Expand All @@ -18,7 +19,6 @@ public partial class ClientAppCoordinator : AppComponentBase
{
//#if (signalr == true)
private HubConnection? hubConnection;
[AutoInject] private IServiceProvider serviceProvider = default!;
//#endif
//#if (notification == true)
[AutoInject] private IPushNotificationService pushNotificationService = default!;
Expand All @@ -28,7 +28,6 @@ public partial class ClientAppCoordinator : AppComponentBase
//#endif
[AutoInject] private Navigator navigator = default!;
[AutoInject] private IJSRuntime jsRuntime = default!;
[AutoInject] private Bit.Butil.Console console = default!;
[AutoInject] private IStorageService storageService = default!;
[AutoInject] private ILogger<ClientAppCoordinator> logger = default!;
[AutoInject] private AuthenticationManager authManager = default!;
Expand Down Expand Up @@ -75,8 +74,6 @@ await storageService.GetItem("Culture") ?? // 2- User settings
}

await SetupBodyClasses();

BrowserConsoleLoggerProvider.SetConsole(console);
}

await base.OnInitAsync();
Expand Down Expand Up @@ -146,8 +143,8 @@ private async Task ConnectSignalR()
// WebSockets should be enabled on services like IIS or Cloudflare CDN, offering significantly better performance.
options.HttpMessageHandlerFactory = signalrHttpMessageHandler =>
{
return serviceProvider.GetRequiredService<Func<HttpMessageHandler, HttpMessageHandler>>()
.Invoke(signalrHttpMessageHandler);
return serviceProvider.GetRequiredService<HttpMessageHandlersChainFactory>()
.Invoke(transportHandler: signalrHttpMessageHandler);
};
})
.Build();
Expand Down Expand Up @@ -240,4 +237,27 @@ protected override async ValueTask DisposeAsync(bool disposing)

await base.DisposeAsync(disposing);
}

[AutoInject]
private IServiceProvider serviceProvider
{
set => currentServiceProvider = value;
get => currentServiceProvider!;
}

private static IServiceProvider? currentServiceProvider;
public static IServiceProvider? CurrentServiceProvider
{
get
{
if (AppPlatform.IsBlazorHybridOrBrowser is false)
throw new InvalidOperationException($"{nameof(CurrentServiceProvider)} is only available in Blazor Hybrid or blazor web assembly.");

return currentServiceProvider;
}
private set
{
currentServiceProvider = value;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
using Boilerplate.Client.Core;
using System.Diagnostics.CodeAnalysis;
using Microsoft.AspNetCore.Components.WebAssembly.Services;
using Boilerplate.Client.Core.Components;
using Boilerplate.Client.Core.Services.HttpMessageHandlers;

namespace Microsoft.Extensions.DependencyInjection;
Expand Down Expand Up @@ -43,6 +44,7 @@ public static IServiceCollection AddClientCoreProjectServices(this IServiceColle
services.AddSessioned(sp => (AuthenticationManager)sp.GetRequiredService<AuthenticationStateProvider>());

services.AddSingleton(sp => configuration.Get<ClientCoreSettings>()!);
services.AddSingleton(_ => new CurrentScopeProvider(() => ClientAppCoordinator.CurrentServiceProvider));

services.AddOptions<ClientCoreSettings>()
.Bind(configuration)
Expand All @@ -55,12 +57,12 @@ public static IServiceCollection AddClientCoreProjectServices(this IServiceColle
// This code constructs a chain of HTTP message handlers. By default, it uses `HttpClientHandler`
// to send requests to the server. However, you can replace `HttpClientHandler` with other HTTP message
// handlers, such as ASP.NET Core's `HttpMessageHandler` from the Test Host, which is useful for integration tests.
services.AddScoped<Func<HttpMessageHandler, HttpMessageHandler>>(serviceProvider => underlyingHttpMessageHandler =>
services.AddScoped<HttpMessageHandlersChainFactory>(serviceProvider => transportHandler =>
{
var constructedHttpMessageHandler = ActivatorUtilities.CreateInstance<RequestHeadersDelegationHandler>(serviceProvider,
[ActivatorUtilities.CreateInstance<AuthDelegatingHandler>(serviceProvider,
[ActivatorUtilities.CreateInstance<RetryDelegatingHandler>(serviceProvider,
[ActivatorUtilities.CreateInstance<ExceptionDelegatingHandler>(serviceProvider, [underlyingHttpMessageHandler])])])]);
[ActivatorUtilities.CreateInstance<ExceptionDelegatingHandler>(serviceProvider, [transportHandler])])])]);
return constructedHttpMessageHandler;
});
services.AddScoped<AuthDelegatingHandler>();
Expand All @@ -69,8 +71,8 @@ public static IServiceCollection AddClientCoreProjectServices(this IServiceColle
services.AddScoped<RequestHeadersDelegationHandler>();
services.AddScoped(serviceProvider =>
{
var underlyingHttpMessageHandler = serviceProvider.GetRequiredService<HttpClientHandler>();
var constructedHttpMessageHandler = serviceProvider.GetRequiredService<Func<HttpMessageHandler, HttpMessageHandler>>().Invoke(underlyingHttpMessageHandler);
var transportHandler = serviceProvider.GetRequiredService<HttpClientHandler>();
var constructedHttpMessageHandler = serviceProvider.GetRequiredService<HttpMessageHandlersChainFactory>().Invoke(transportHandler);
return constructedHttpMessageHandler;
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ public static async ValueTask<DeviceInstallationDto> GetDeviceInstallation(this
/// </summary>
public static bool IsInitialized(this IJSRuntime jsRuntime)
{
if (jsRuntime is null)
return false;

var type = jsRuntime.GetType();

return type.Name switch
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,35 @@
namespace Microsoft.Extensions.Logging;
using Boilerplate.Client.Core.Services.DiagnosticLog;

namespace Microsoft.Extensions.Logging;

public static class ILoggingBuilderExtensions
{
public static ILoggingBuilder AddBrowserConsoleLogger(this ILoggingBuilder builder)
public static ILoggingBuilder AddDiagnosticLogger(this ILoggingBuilder builder)
{
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<ILoggerProvider, BrowserConsoleLoggerProvider>());
builder.Services.AddSessioned<DiagnosticLogger>();
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<ILoggerProvider, DiagnosticLoggerProvider>());

return builder;
}

public static ILoggingBuilder ConfigureLoggers(this ILoggingBuilder loggingBuilder)
{
loggingBuilder.ClearProviders();

if (AppEnvironment.IsDev())
{
loggingBuilder.AddDebug();
}

if (!AppPlatform.IsBrowser)
{
loggingBuilder.AddConsole();
// DiagnosticLogger is already logging in browser's console.
// But Console logger is still useful in Visual Studio's Device Log (Android, iOS) or BrowserStack etc.
}

loggingBuilder.AddDiagnosticLogger();

return loggingBuilder;
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using Boilerplate.Client.Core.Components;

namespace Boilerplate.Client.Core.Services.Contracts;

/// <summary>
/// Provides the current scope's <see cref="IServiceProvider"/>.
///
/// In different hosting environments, this delegate returns the `IServiceProvider` from:
///
/// - **Blazor Server, SSR, and Pre-rendering:** `HttpContextAccessor.HttpContext.RequestServices`
/// - **Blazor WebAssembly and Hybrid:** Gets it from <see cref="ClientAppCoordinator.CurrentServiceProvider"/>
///
/// The delegate may return `null` in the following scenarios:
///
/// - When there's no active `HttpContext` in backend environments.
/// - When the Routes.razor page is not loaded yet.
/// </summary>
/// <returns>The current scope's `IServiceProvider`, or `null` if not available.</returns>
public delegate IServiceProvider? CurrentScopeProvider();
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
using Microsoft.Extensions.Logging;

namespace Boilerplate.Client.Core.Services.DiagnosticLog;

public partial class DiagnosticLogger(CurrentScopeProvider scopeProvider) : ILogger, IDisposable
{
private ConcurrentQueue<IDictionary<string, object?>> states = new();

public string? CategoryName { get; set; }

public IDisposable? BeginScope<TState>(TState state)
where TState : notnull
{
if (state is IDictionary<string, object?> data)
{
data[nameof(CategoryName)] = CategoryName;
states.Enqueue(data);
}

return this;
}

public bool IsEnabled(LogLevel logLevel)
{
return logLevel != LogLevel.None;
}

public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
{
if (IsEnabled(logLevel) is false) return;

var message = formatter(state, exception);

states.TryDequeue(out var currentState);

var scope = scopeProvider.Invoke();

if (scope is null)
return;

// Store logs in the memory to be shown later.

var jsRuntime = scope.GetRequiredService<IJSRuntime>();

if (jsRuntime.IsInitialized() is false)
return;

var console = scope.GetRequiredService<Bit.Butil.Console>();

switch (logLevel)
{
case LogLevel.Trace:
case LogLevel.Debug:
console!.Log(message, $"{Environment.NewLine}Category:", CategoryName, $"{Environment.NewLine}State:", currentState);
break;
case LogLevel.Information:
console!.Info(message, $"{Environment.NewLine}Category:", CategoryName, $"{Environment.NewLine}State:", currentState);
break;
case LogLevel.Warning:
console!.Warn(message, $"{Environment.NewLine}Category:", CategoryName, $"{Environment.NewLine}State:", currentState);
break;
case LogLevel.Error:
case LogLevel.Critical:
console!.Error(message, $"{Environment.NewLine}Category:", CategoryName, $"{Environment.NewLine}State:", currentState);
break;
case LogLevel.None:
break;
default:
console!.Log(message, $"{Environment.NewLine}Category:", CategoryName, $"{Environment.NewLine}State:", currentState);
break;
}
}

public void Dispose()
{

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
//+:cnd:noEmit
using Microsoft.Extensions.Logging;

namespace Boilerplate.Client.Core.Services.DiagnosticLog;

// https://learn.microsoft.com/en-us/aspnet/core/blazor/hybrid/developer-tools

/// <summary>
/// Provides a custom logger that outputs log messages to the browser's console and allows for selective display of logs
/// within the application UI for enhanced diagnostics.
/// </summary>
[ProviderAlias("DevInsights")]
public partial class DiagnosticLoggerProvider : ILoggerProvider
{
[AutoInject] private CurrentScopeProvider scopeProvider = default!;

public ILogger CreateLogger(string categoryName)
{
return new DiagnosticLogger(scopeProvider)
{
CategoryName = categoryName
};
}

public void Dispose()
{

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,14 @@ protected virtual void Handle(Exception exception, Dictionary<string, object> pa
innerException = innerException.InnerException;
}

Logger.LogError(exception, exceptionMessageToLog);
if (exception is KnownException)
{
Logger.LogError(exception, exceptionMessageToLog);
}
else
{
Logger.LogCritical(exception, exceptionMessageToLog);
}
}

string exceptionMessageToShow = (exception as KnownException)?.Message ??
Expand Down
Loading

0 comments on commit 3d1ece7

Please sign in to comment.