Skip to content

Commit 40d5199

Browse files
committed
Add better logging.
1 parent af9bc02 commit 40d5199

File tree

2 files changed

+136
-13
lines changed

2 files changed

+136
-13
lines changed

src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/Redirections/CircularRedirection.razor

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,26 +13,39 @@
1313
{
1414
<h2>Unobserved Exceptions (for debugging):</h2>
1515
<ul>
16-
@foreach (var ex in _unobservedExceptions)
16+
@foreach (var exceptionDetail in _unobservedExceptions)
1717
{
18-
<li>@ex.ToString()</li>
18+
<li>
19+
<strong>Observed at:</strong> @exceptionDetail.ObservedAt.ToString("yyyy-MM-dd HH:mm:ss.fff") UTC<br/>
20+
<strong>Thread ID:</strong> @exceptionDetail.ObservedThreadId<br/>
21+
<strong>From Finalizer Thread:</strong> @exceptionDetail.IsFromFinalizerThread<br/>
22+
<strong>Exception:</strong> @exceptionDetail.Exception.ToString()<br/>
23+
<details>
24+
<summary>Detailed Exception Information</summary>
25+
<pre>@exceptionDetail.DetailedExceptionInfo</pre>
26+
</details>
27+
<details>
28+
<summary>Call Stack When Observed</summary>
29+
<pre>@exceptionDetail.ObservedCallStack</pre>
30+
</details>
31+
</li>
1932
}
2033
</ul>
2134
}
2235
}
2336

2437
@code {
2538
private bool _shouldStopRedirecting;
26-
private IReadOnlyCollection<Exception> _unobservedExceptions = Array.Empty<Exception>();
39+
private IReadOnlyCollection<UnobservedExceptionDetails> _unobservedExceptions = Array.Empty<UnobservedExceptionDetails>();
2740

2841
protected override async Task OnInitializedAsync()
2942
{
30-
int visits = Observer.GetCircularRedirectCount();
31-
if (visits == 0)
32-
{
33-
// make sure we start with clean logs
34-
Observer.Clear();
35-
}
43+
int visits = Observer.GetCircularRedirectCount();
44+
if (visits == 0)
45+
{
46+
// make sure we start with clean logs
47+
Observer.Clear();
48+
}
3649

3750
// Force GC collection to trigger finalizers - this is what causes the issue
3851
GC.Collect();
@@ -47,7 +60,7 @@
4760
else
4861
{
4962
_shouldStopRedirecting = true;
50-
_unobservedExceptions = Observer.GetExceptions();
63+
_unobservedExceptions = Observer.GetExceptionDetails();
5164
}
5265
}
5366
}

src/Components/test/testassets/Components.TestServer/UnobservedTaskExceptionObserver.cs

Lines changed: 113 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,134 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using System.Collections.Concurrent;
5+
using System.Diagnostics;
6+
using System.Globalization;
7+
using System.Text;
58
using System.Threading;
9+
using System.Linq;
610

711
namespace TestServer;
812

13+
/// <summary>
14+
/// Represents detailed information about an unobserved task exception, including the original call stack.
15+
/// </summary>
16+
public class UnobservedExceptionDetails
17+
{
18+
/// <summary>
19+
/// The original exception that was unobserved.
20+
/// </summary>
21+
public Exception Exception { get; init; }
22+
23+
/// <summary>
24+
/// The timestamp when the exception was observed.
25+
/// </summary>
26+
public DateTime ObservedAt { get; init; }
27+
28+
/// <summary>
29+
/// The current call stack when the exception was observed (may show finalizer thread).
30+
/// </summary>
31+
public string ObservedCallStack { get; init; }
32+
33+
/// <summary>
34+
/// Detailed breakdown of inner exceptions and their stack traces.
35+
/// </summary>
36+
public string DetailedExceptionInfo { get; init; }
37+
38+
/// <summary>
39+
/// The managed thread ID where the exception was observed.
40+
/// </summary>
41+
public int ObservedThreadId { get; init; }
42+
43+
/// <summary>
44+
/// Whether this exception was observed on the finalizer thread.
45+
/// </summary>
46+
public bool IsFromFinalizerThread { get; init; }
47+
48+
public UnobservedExceptionDetails(Exception exception)
49+
{
50+
Exception = exception;
51+
ObservedAt = DateTime.UtcNow;
52+
ObservedCallStack = Environment.StackTrace;
53+
DetailedExceptionInfo = BuildDetailedExceptionInfo(exception);
54+
ObservedThreadId = Environment.CurrentManagedThreadId;
55+
IsFromFinalizerThread = Thread.CurrentThread.IsThreadPoolThread && Thread.CurrentThread.IsBackground;
56+
}
57+
58+
private static string BuildDetailedExceptionInfo(Exception exception)
59+
{
60+
var sb = new StringBuilder();
61+
var currentException = exception;
62+
var depth = 0;
63+
64+
while (currentException is not null)
65+
{
66+
var indent = new string(' ', depth * 2);
67+
sb.AppendLine(CultureInfo.InvariantCulture, $"{indent}Exception Type: {currentException.GetType().FullName}");
68+
sb.AppendLine(CultureInfo.InvariantCulture, $"{indent}Message: {currentException.Message}");
69+
70+
if (currentException.Data.Count > 0)
71+
{
72+
sb.AppendLine(CultureInfo.InvariantCulture, $"{indent}Data:");
73+
foreach (var key in currentException.Data.Keys)
74+
{
75+
sb.AppendLine(CultureInfo.InvariantCulture, $"{indent} {key}: {currentException.Data[key]}");
76+
}
77+
}
78+
79+
if (!string.IsNullOrEmpty(currentException.StackTrace))
80+
{
81+
sb.AppendLine(CultureInfo.InvariantCulture, $"{indent}Stack Trace:");
82+
sb.AppendLine(currentException.StackTrace);
83+
}
84+
85+
// Handle AggregateException specially to extract all inner exceptions
86+
if (currentException is AggregateException aggregateException)
87+
{
88+
sb.AppendLine(CultureInfo.InvariantCulture, $"{indent}Aggregate Exception contains {aggregateException.InnerExceptions.Count} inner exceptions:");
89+
for (int i = 0; i < aggregateException.InnerExceptions.Count; i++)
90+
{
91+
sb.AppendLine(CultureInfo.InvariantCulture, $"{indent} Inner Exception {i + 1}:");
92+
sb.AppendLine(BuildDetailedExceptionInfo(aggregateException.InnerExceptions[i]));
93+
}
94+
break; // Don't process InnerException for AggregateException as we've handled all inner exceptions
95+
}
96+
97+
currentException = currentException.InnerException;
98+
depth++;
99+
100+
if (currentException is not null)
101+
{
102+
sb.AppendLine(CultureInfo.InvariantCulture, $"{indent}--- Inner Exception ---");
103+
}
104+
}
105+
106+
return sb.ToString();
107+
}
108+
}
109+
9110
public class UnobservedTaskExceptionObserver
10111
{
11-
private readonly ConcurrentQueue<Exception> _exceptions = new();
112+
private readonly ConcurrentQueue<UnobservedExceptionDetails> _exceptions = new();
12113
private int _circularRedirectCount;
13114

14115
public void OnUnobservedTaskException(object sender, UnobservedTaskExceptionEventArgs e)
15116
{
16-
_exceptions.Enqueue(e.Exception);
117+
var details = new UnobservedExceptionDetails(e.Exception);
118+
_exceptions.Enqueue(details);
17119
e.SetObserved(); // Mark as observed to prevent the process from crashing during tests
18120
}
19121

20122
public bool HasExceptions => !_exceptions.IsEmpty;
21123

22-
public IReadOnlyCollection<Exception> GetExceptions() => _exceptions.ToArray();
124+
/// <summary>
125+
/// Gets the detailed exception information including original call stacks.
126+
/// </summary>
127+
public IReadOnlyCollection<UnobservedExceptionDetails> GetExceptionDetails() => _exceptions.ToArray();
128+
129+
/// <summary>
130+
/// Gets the raw exceptions for backward compatibility.
131+
/// </summary>
132+
public IReadOnlyCollection<Exception> GetExceptions() => _exceptions.ToArray().Select(d => d.Exception).ToList();
23133

24134
public void Clear()
25135
{

0 commit comments

Comments
 (0)