| File: Services\WebAssemblyConsoleLogger.cs | Web Access |
| Project: src\aspnetcore\src\Components\WebAssembly\WebAssembly\src\Microsoft.AspNetCore.Components.WebAssembly.csproj (Microsoft.AspNetCore.Components.WebAssembly) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; using System.Runtime.InteropServices.JavaScript; using System.Text; using Microsoft.Extensions.Logging; using Microsoft.JSInterop; using Microsoft.JSInterop.WebAssembly; namespace Microsoft.AspNetCore.Components.WebAssembly.Services; internal sealed class WebAssemblyConsoleLogger<T> : ILogger<T>, ILogger { private const string _loglevelPadding = ": "; private static readonly string _messagePadding = new(' ', GetLogLevelString(LogLevel.Information).Length + _loglevelPadding.Length); private static readonly string _newLineWithMessagePadding = Environment.NewLine + _messagePadding; private static readonly StringBuilder _logBuilder = new StringBuilder(); private readonly string _name; private readonly WebAssemblyJSRuntime _jsRuntime; public WebAssemblyConsoleLogger(IJSRuntime jsRuntime) : this(string.Empty, (WebAssemblyJSRuntime)jsRuntime) // Cast for DI { } public WebAssemblyConsoleLogger(string name, WebAssemblyJSRuntime jsRuntime) { _name = name ?? throw new ArgumentNullException(nameof(name)); _jsRuntime = jsRuntime ?? throw new ArgumentNullException(nameof(jsRuntime)); } public IDisposable? BeginScope<TState>(TState state) where TState : notnull { return NoOpDisposable.Instance; } 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)) { return; } ArgumentNullException.ThrowIfNull(formatter); var message = formatter(state, exception); if (!string.IsNullOrEmpty(message) || exception != null) { WriteMessage(logLevel, _name, eventId.Id, message, exception); } } private void WriteMessage(LogLevel logLevel, string logName, int eventId, string message, Exception? exception) { lock (_logBuilder) { try { CreateDefaultLogMessage(_logBuilder, logLevel, logName, eventId, message, exception); var formattedMessage = _logBuilder.ToString(); switch (logLevel) { case LogLevel.Trace: case LogLevel.Debug: // Although https://console.spec.whatwg.org/#loglevel-severity claims that // "console.debug" and "console.log" are synonyms, that doesn't match the // behavior of browsers in the real world. Chromium only displays "debug" // messages if you enable "Verbose" in the filter dropdown (which is off // by default). As such "console.debug" is the best choice for messages // with a lower severity level than "Information". ConsoleLoggerInterop.ConsoleDebug(formattedMessage); break; case LogLevel.Information: ConsoleLoggerInterop.ConsoleInfo(formattedMessage); break; case LogLevel.Warning: ConsoleLoggerInterop.ConsoleWarn(formattedMessage); break; case LogLevel.Error: ConsoleLoggerInterop.ConsoleError(formattedMessage); break; case LogLevel.Critical: ConsoleLoggerInterop.DotNetCriticalError(formattedMessage); break; default: // invalid enum values Debug.Assert(logLevel != LogLevel.None, "This method is never called with LogLevel.None."); _jsRuntime.InvokeVoid("console.log", formattedMessage); break; } } finally { _logBuilder.Clear(); } } } private static void CreateDefaultLogMessage(StringBuilder logBuilder, LogLevel logLevel, string logName, int eventId, string message, Exception? exception) { logBuilder.Append(GetLogLevelString(logLevel)); logBuilder.Append(_loglevelPadding); logBuilder.Append(logName); logBuilder.Append('['); logBuilder.Append(eventId); logBuilder.Append(']'); if (!string.IsNullOrEmpty(message)) { // message logBuilder.AppendLine(); logBuilder.Append(_messagePadding); var len = logBuilder.Length; logBuilder.Append(message); logBuilder.Replace(Environment.NewLine, _newLineWithMessagePadding, len, message.Length); } // Example: // System.InvalidOperationException // at Namespace.Class.Function() in File:line X if (exception != null) { // exception message logBuilder.AppendLine(); logBuilder.Append(exception.ToString()); } } private static string GetLogLevelString(LogLevel logLevel) { switch (logLevel) { case LogLevel.Trace: return "trce"; case LogLevel.Debug: return "dbug"; case LogLevel.Information: return "info"; case LogLevel.Warning: return "warn"; case LogLevel.Error: return "fail"; case LogLevel.Critical: return "crit"; default: throw new ArgumentOutOfRangeException(nameof(logLevel)); } } private sealed class NoOpDisposable : IDisposable { public static NoOpDisposable Instance = new NoOpDisposable(); public void Dispose() { } } } internal static partial class ConsoleLoggerInterop { [JSImport("globalThis.console.debug")] public static partial void ConsoleDebug(string message); [JSImport("globalThis.console.info")] public static partial void ConsoleInfo(string message); [JSImport("globalThis.console.warn")] public static partial void ConsoleWarn(string message); [JSImport("globalThis.console.error")] public static partial void ConsoleError(string message); [JSImport("Blazor._internal.dotNetCriticalError", "blazor-internal")] public static partial void DotNetCriticalError(string message); }