-
Notifications
You must be signed in to change notification settings - Fork 10.4k
Log unhandled exceptions to custom logger #19606
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
Changes from all commits
8b22eec
6982f99
c3ed431
48600a6
9dab4c5
8f0c407
3e9b351
4a066fa
58127bb
db52af8
d0c40f4
e868067
288783b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,26 +2,170 @@ | |
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. | ||
|
||
using System; | ||
using System.Text; | ||
using Microsoft.Extensions.Logging; | ||
using Microsoft.JSInterop; | ||
|
||
namespace Microsoft.AspNetCore.Components.WebAssembly.Services | ||
{ | ||
internal class WebAssemblyConsoleLogger<T> : ILogger<T>, ILogger | ||
{ | ||
private static readonly string _loglevelPadding = ": "; | ||
private static readonly string _messagePadding; | ||
private static readonly string _newLineWithMessagePadding; | ||
private static readonly StringBuilder _logBuilder = new StringBuilder(); | ||
|
||
private readonly string _name; | ||
private readonly IJSInProcessRuntime _jsRuntime; | ||
|
||
static WebAssemblyConsoleLogger() | ||
{ | ||
var logLevelString = GetLogLevelString(LogLevel.Information); | ||
_messagePadding = new string(' ', logLevelString.Length + _loglevelPadding.Length); | ||
_newLineWithMessagePadding = Environment.NewLine + _messagePadding; | ||
} | ||
|
||
public WebAssemblyConsoleLogger(IJSRuntime jsRuntime) | ||
: this(string.Empty, (IJSInProcessRuntime)jsRuntime) // Cast for DI | ||
{ | ||
} | ||
|
||
public WebAssemblyConsoleLogger(string name, IJSInProcessRuntime jsRuntime) | ||
{ | ||
_name = name ?? throw new ArgumentNullException(nameof(name)); | ||
_jsRuntime = jsRuntime ?? throw new ArgumentNullException(nameof(jsRuntime)); | ||
} | ||
|
||
public IDisposable BeginScope<TState>(TState state) | ||
{ | ||
return NoOpDisposable.Instance; | ||
} | ||
|
||
public bool IsEnabled(LogLevel logLevel) | ||
{ | ||
return logLevel >= LogLevel.Warning; | ||
return logLevel >= LogLevel.Warning && logLevel != LogLevel.None; | ||
} | ||
|
||
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter) | ||
{ | ||
var formattedMessage = formatter(state, exception); | ||
Console.WriteLine($"[{logLevel}] {formattedMessage}"); | ||
if (!IsEnabled(logLevel)) | ||
{ | ||
return; | ||
} | ||
|
||
if (formatter == null) | ||
{ | ||
throw new ArgumentNullException(nameof(formatter)); | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It is a bit weird that the logger does this. Does the ConsoleLogger do the same thing? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would look at what we do in ASP.NET Core and strive for consistency with what is there. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good point. Changing as per #19606 (comment) |
||
|
||
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". | ||
_jsRuntime.InvokeVoid("console.debug", formattedMessage); | ||
break; | ||
case LogLevel.Information: | ||
_jsRuntime.InvokeVoid("console.info", formattedMessage); | ||
break; | ||
case LogLevel.Warning: | ||
_jsRuntime.InvokeVoid("console.warn", formattedMessage); | ||
break; | ||
case LogLevel.Error: | ||
_jsRuntime.InvokeVoid("console.error", formattedMessage); | ||
break; | ||
case LogLevel.Critical: | ||
// Writing to Console.Error is even more severe than calling console.error, | ||
// because it also causes the error UI (gold bar) to appear. We use Console.Error | ||
// as the signal for triggering that because it's what the underlying dotnet.wasm | ||
// runtime will do if it encounters a truly severe error outside the Blazor | ||
// code paths. | ||
Console.Error.WriteLine(formattedMessage); | ||
break; | ||
default: // LogLevel.None or invalid enum values | ||
Console.WriteLine(formattedMessage); | ||
break; | ||
} | ||
} | ||
finally | ||
{ | ||
_logBuilder.Clear(); | ||
} | ||
} | ||
} | ||
|
||
private 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 class NoOpDisposable : IDisposable | ||
|
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I removed the
WASM:
prefix because it serves no purpose. Developers who callConsole.WriteLine
should be able to choose exactly what string gets logged, without the framework enforcing an arbitrary prefix.