Skip to content

Commit 9343d2b

Browse files
Log unhandled exceptions to custom logger (#19606)
1 parent 343816f commit 9343d2b

File tree

13 files changed

+451
-22
lines changed

13 files changed

+451
-22
lines changed

src/Components/Web.JS/dist/Release/blazor.server.js

Lines changed: 10 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Components/Web.JS/dist/Release/blazor.webassembly.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Components/Web.JS/src/Platform/Mono/MonoPlatform.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -160,10 +160,10 @@ function createEmscriptenModuleInstance(resourceLoader: WebAssemblyResourceLoade
160160
const module = {} as typeof Module;
161161
const suppressMessages = ['DEBUGGING ENABLED'];
162162

163-
module.print = line => (suppressMessages.indexOf(line) < 0 && console.log(`WASM: ${line}`));
163+
module.print = line => (suppressMessages.indexOf(line) < 0 && console.log(line));
164164

165165
module.printErr = line => {
166-
console.error(`WASM: ${line}`);
166+
console.error(line);
167167
showErrorNotification();
168168
};
169169
module.preRun = [];

src/Components/WebAssembly/WebAssembly/src/Rendering/WebAssemblyRenderer.cs

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Rendering
1616
/// </summary>
1717
internal class WebAssemblyRenderer : Renderer
1818
{
19+
private readonly ILogger _logger;
1920
private readonly int _webAssemblyRendererId;
2021

2122
private bool isDispatchingEvent;
@@ -31,6 +32,7 @@ public WebAssemblyRenderer(IServiceProvider serviceProvider, ILoggerFactory logg
3132
{
3233
// The WebAssembly renderer registers and unregisters itself with the static registry
3334
_webAssemblyRendererId = RendererRegistry.Add(this);
35+
_logger = loggerFactory.CreateLogger<WebAssemblyRenderer>();
3436
}
3537

3638
public override Dispatcher Dispatcher => NullDispatcher.Instance;
@@ -101,17 +103,16 @@ protected override Task UpdateDisplayAsync(in RenderBatch batch)
101103
/// <inheritdoc />
102104
protected override void HandleException(Exception exception)
103105
{
104-
Console.Error.WriteLine($"Unhandled exception rendering component:");
105106
if (exception is AggregateException aggregateException)
106107
{
107108
foreach (var innerException in aggregateException.Flatten().InnerExceptions)
108109
{
109-
Console.Error.WriteLine(innerException);
110+
Log.UnhandledExceptionRenderingComponent(_logger, innerException);
110111
}
111112
}
112113
else
113114
{
114-
Console.Error.WriteLine(exception);
115+
Log.UnhandledExceptionRenderingComponent(_logger, exception);
115116
}
116117
}
117118

@@ -192,5 +193,31 @@ public IncomingEventInfo(ulong eventHandlerId, EventFieldInfo eventFieldInfo, Ev
192193
TaskCompletionSource = new TaskCompletionSource<object>();
193194
}
194195
}
196+
197+
private static class Log
198+
{
199+
private static readonly Action<ILogger, string, Exception> _unhandledExceptionRenderingComponent;
200+
201+
private static class EventIds
202+
{
203+
public static readonly EventId UnhandledExceptionRenderingComponent = new EventId(100, "ExceptionRenderingComponent");
204+
}
205+
206+
static Log()
207+
{
208+
_unhandledExceptionRenderingComponent = LoggerMessage.Define<string>(
209+
LogLevel.Critical,
210+
EventIds.UnhandledExceptionRenderingComponent,
211+
"Unhandled exception rendering component: {Message}");
212+
}
213+
214+
public static void UnhandledExceptionRenderingComponent(ILogger logger, Exception exception)
215+
{
216+
_unhandledExceptionRenderingComponent(
217+
logger,
218+
exception.Message,
219+
exception);
220+
}
221+
}
195222
}
196223
}

src/Components/WebAssembly/WebAssembly/src/Services/WebAssemblyConsoleLogger.cs

Lines changed: 147 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,26 +2,170 @@
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

44
using System;
5+
using System.Text;
56
using Microsoft.Extensions.Logging;
7+
using Microsoft.JSInterop;
68

79
namespace Microsoft.AspNetCore.Components.WebAssembly.Services
810
{
911
internal class WebAssemblyConsoleLogger<T> : ILogger<T>, ILogger
1012
{
13+
private static readonly string _loglevelPadding = ": ";
14+
private static readonly string _messagePadding;
15+
private static readonly string _newLineWithMessagePadding;
16+
private static readonly StringBuilder _logBuilder = new StringBuilder();
17+
18+
private readonly string _name;
19+
private readonly IJSInProcessRuntime _jsRuntime;
20+
21+
static WebAssemblyConsoleLogger()
22+
{
23+
var logLevelString = GetLogLevelString(LogLevel.Information);
24+
_messagePadding = new string(' ', logLevelString.Length + _loglevelPadding.Length);
25+
_newLineWithMessagePadding = Environment.NewLine + _messagePadding;
26+
}
27+
28+
public WebAssemblyConsoleLogger(IJSRuntime jsRuntime)
29+
: this(string.Empty, (IJSInProcessRuntime)jsRuntime) // Cast for DI
30+
{
31+
}
32+
33+
public WebAssemblyConsoleLogger(string name, IJSInProcessRuntime jsRuntime)
34+
{
35+
_name = name ?? throw new ArgumentNullException(nameof(name));
36+
_jsRuntime = jsRuntime ?? throw new ArgumentNullException(nameof(jsRuntime));
37+
}
38+
1139
public IDisposable BeginScope<TState>(TState state)
1240
{
1341
return NoOpDisposable.Instance;
1442
}
1543

1644
public bool IsEnabled(LogLevel logLevel)
1745
{
18-
return logLevel >= LogLevel.Warning;
46+
return logLevel >= LogLevel.Warning && logLevel != LogLevel.None;
1947
}
2048

2149
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
2250
{
23-
var formattedMessage = formatter(state, exception);
24-
Console.WriteLine($"[{logLevel}] {formattedMessage}");
51+
if (!IsEnabled(logLevel))
52+
{
53+
return;
54+
}
55+
56+
if (formatter == null)
57+
{
58+
throw new ArgumentNullException(nameof(formatter));
59+
}
60+
61+
var message = formatter(state, exception);
62+
63+
if (!string.IsNullOrEmpty(message) || exception != null)
64+
{
65+
WriteMessage(logLevel, _name, eventId.Id, message, exception);
66+
}
67+
}
68+
69+
private void WriteMessage(LogLevel logLevel, string logName, int eventId, string message, Exception exception)
70+
{
71+
lock (_logBuilder)
72+
{
73+
try
74+
{
75+
CreateDefaultLogMessage(_logBuilder, logLevel, logName, eventId, message, exception);
76+
var formattedMessage = _logBuilder.ToString();
77+
78+
switch (logLevel)
79+
{
80+
case LogLevel.Trace:
81+
case LogLevel.Debug:
82+
// Although https://console.spec.whatwg.org/#loglevel-severity claims that
83+
// "console.debug" and "console.log" are synonyms, that doesn't match the
84+
// behavior of browsers in the real world. Chromium only displays "debug"
85+
// messages if you enable "Verbose" in the filter dropdown (which is off
86+
// by default). As such "console.debug" is the best choice for messages
87+
// with a lower severity level than "Information".
88+
_jsRuntime.InvokeVoid("console.debug", formattedMessage);
89+
break;
90+
case LogLevel.Information:
91+
_jsRuntime.InvokeVoid("console.info", formattedMessage);
92+
break;
93+
case LogLevel.Warning:
94+
_jsRuntime.InvokeVoid("console.warn", formattedMessage);
95+
break;
96+
case LogLevel.Error:
97+
_jsRuntime.InvokeVoid("console.error", formattedMessage);
98+
break;
99+
case LogLevel.Critical:
100+
// Writing to Console.Error is even more severe than calling console.error,
101+
// because it also causes the error UI (gold bar) to appear. We use Console.Error
102+
// as the signal for triggering that because it's what the underlying dotnet.wasm
103+
// runtime will do if it encounters a truly severe error outside the Blazor
104+
// code paths.
105+
Console.Error.WriteLine(formattedMessage);
106+
break;
107+
default: // LogLevel.None or invalid enum values
108+
Console.WriteLine(formattedMessage);
109+
break;
110+
}
111+
}
112+
finally
113+
{
114+
_logBuilder.Clear();
115+
}
116+
}
117+
}
118+
119+
private void CreateDefaultLogMessage(StringBuilder logBuilder, LogLevel logLevel, string logName, int eventId, string message, Exception exception)
120+
{
121+
logBuilder.Append(GetLogLevelString(logLevel));
122+
logBuilder.Append(_loglevelPadding);
123+
logBuilder.Append(logName);
124+
logBuilder.Append("[");
125+
logBuilder.Append(eventId);
126+
logBuilder.Append("]");
127+
128+
if (!string.IsNullOrEmpty(message))
129+
{
130+
// message
131+
logBuilder.AppendLine();
132+
logBuilder.Append(_messagePadding);
133+
134+
var len = logBuilder.Length;
135+
logBuilder.Append(message);
136+
logBuilder.Replace(Environment.NewLine, _newLineWithMessagePadding, len, message.Length);
137+
}
138+
139+
// Example:
140+
// System.InvalidOperationException
141+
// at Namespace.Class.Function() in File:line X
142+
if (exception != null)
143+
{
144+
// exception message
145+
logBuilder.AppendLine();
146+
logBuilder.Append(exception.ToString());
147+
}
148+
}
149+
150+
private static string GetLogLevelString(LogLevel logLevel)
151+
{
152+
switch (logLevel)
153+
{
154+
case LogLevel.Trace:
155+
return "trce";
156+
case LogLevel.Debug:
157+
return "dbug";
158+
case LogLevel.Information:
159+
return "info";
160+
case LogLevel.Warning:
161+
return "warn";
162+
case LogLevel.Error:
163+
return "fail";
164+
case LogLevel.Critical:
165+
return "crit";
166+
default:
167+
throw new ArgumentOutOfRangeException(nameof(logLevel));
168+
}
25169
}
26170

27171
private class NoOpDisposable : IDisposable

src/Components/WebAssembly/WebAssembly/src/Services/WebAssemblyLoggerFactory.cs

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,28 @@
11
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

4+
using System;
5+
using Microsoft.Extensions.DependencyInjection;
46
using Microsoft.Extensions.Logging;
7+
using Microsoft.JSInterop;
58

69
namespace Microsoft.AspNetCore.Components.WebAssembly.Services
710
{
811
internal class WebAssemblyLoggerFactory : ILoggerFactory
912
{
10-
public void AddProvider(ILoggerProvider provider)
13+
private readonly IJSInProcessRuntime _jsRuntime;
14+
15+
public WebAssemblyLoggerFactory(IServiceProvider services)
1116
{
12-
// No-op
17+
_jsRuntime = (IJSInProcessRuntime)services.GetRequiredService<IJSRuntime>();
1318
}
1419

20+
// We might implement this in the future, but it's not required currently
21+
public void AddProvider(ILoggerProvider provider)
22+
=> throw new NotSupportedException();
23+
1524
public ILogger CreateLogger(string categoryName)
16-
=> new WebAssemblyConsoleLogger<object>();
25+
=> new WebAssemblyConsoleLogger<object>(categoryName, _jsRuntime);
1726

1827
public void Dispose()
1928
{

src/Components/test/E2ETest/Tests/ErrorNotificationTest.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ protected override void InitializeAsyncCore()
2929
Navigate(ServerPathBase, noReload: _serverFixture.ExecutionMode == ExecutionMode.Client);
3030
Browser.MountTestComponent<ErrorComponent>();
3131
Browser.Exists(By.Id("blazor-error-ui"));
32-
Browser.Exists(By.TagName("button"));
32+
Browser.Exists(By.Id("throw-simple-exception"));
3333
}
3434

3535
[Fact]
@@ -38,7 +38,7 @@ public void ShowsErrorNotification_OnError_Dismiss()
3838
var errorUi = Browser.FindElement(By.Id("blazor-error-ui"));
3939
Assert.Equal("none", errorUi.GetCssValue("display"));
4040

41-
var causeErrorButton = Browser.FindElement(By.TagName("button"));
41+
var causeErrorButton = Browser.FindElement(By.Id("throw-simple-exception"));
4242
causeErrorButton.Click();
4343

4444
Browser.Exists(By.CssSelector("#blazor-error-ui[style='display: block;']"), TimeSpan.FromSeconds(10));
@@ -52,7 +52,7 @@ public void ShowsErrorNotification_OnError_Dismiss()
5252
[Fact]
5353
public void ShowsErrorNotification_OnError_Reload()
5454
{
55-
var causeErrorButton = Browser.Exists(By.TagName("button"));
55+
var causeErrorButton = Browser.Exists(By.Id("throw-simple-exception"));
5656
var errorUi = Browser.FindElement(By.Id("blazor-error-ui"));
5757
Assert.Equal("none", errorUi.GetCssValue("display"));
5858

0 commit comments

Comments
 (0)