1
1
// Licensed to the .NET Foundation under one or more agreements.
2
2
// The .NET Foundation licenses this file to you under the MIT license.
3
3
4
+ using System . Diagnostics ;
5
+ using System . Globalization ;
4
6
using Aspire . Dashboard . ConsoleLogs ;
7
+ using Aspire . Dashboard . Extensions ;
5
8
using Aspire . Dashboard . Model ;
6
9
using Aspire . Dashboard . Utils ;
7
10
using Microsoft . AspNetCore . Components ;
@@ -14,30 +17,35 @@ namespace Aspire.Dashboard.Components;
14
17
/// </summary>
15
18
public sealed partial class LogViewer
16
19
{
17
- private readonly TaskCompletionSource _whenDomReady = new ( ) ;
18
20
private readonly CancellationSeries _cancellationSeries = new ( ) ;
19
- private IJSObjectReference ? _jsModule ;
21
+ private bool _convertTimestampsFromUtc ;
22
+ private bool _applicationChanged ;
20
23
21
24
[ Inject ]
22
25
public required BrowserTimeProvider TimeProvider { get ; init ; }
23
26
24
27
protected override async Task OnAfterRenderAsync ( bool firstRender )
25
28
{
29
+ if ( _applicationChanged )
30
+ {
31
+ await JS . InvokeVoidAsync ( "resetContinuousScrollPosition" ) ;
32
+ _applicationChanged = false ;
33
+ }
26
34
if ( firstRender )
27
35
{
28
- _jsModule ??= await JS . InvokeAsync < IJSObjectReference > ( "import" , "/Components/Controls/LogViewer.razor.js" ) ;
29
-
30
- _whenDomReady . TrySetResult ( ) ;
36
+ await JS . InvokeVoidAsync ( "initializeContinuousScroll" ) ;
31
37
}
32
38
}
33
39
34
- internal async Task SetLogSourceAsync ( IAsyncEnumerable < IReadOnlyList < ( string Content , bool IsErrorMessage ) > > batches , bool convertTimestampsFromUtc )
40
+ private readonly List < LogEntry > _logEntries = new ( ) ;
41
+ private int ? _baseLineNumber ;
42
+
43
+ internal async Task SetLogSourceAsync ( IAsyncEnumerable < IReadOnlyList < ResourceLogLine > > batches , bool convertTimestampsFromUtc )
35
44
{
36
- var cancellationToken = await _cancellationSeries . NextAsync ( ) ;
37
- var logParser = new LogParser ( TimeProvider , convertTimestampsFromUtc ) ;
45
+ _convertTimestampsFromUtc = convertTimestampsFromUtc ;
38
46
39
- // Ensure we are able to write to the DOM.
40
- await _whenDomReady . Task ;
47
+ var cancellationToken = await _cancellationSeries . NextAsync ( ) ;
48
+ var logParser = new LogParser ( ) ;
41
49
42
50
await foreach ( var batch in batches . WithCancellation ( cancellationToken ) )
43
51
{
@@ -46,33 +54,105 @@ internal async Task SetLogSourceAsync(IAsyncEnumerable<IReadOnlyList<(string Con
46
54
continue ;
47
55
}
48
56
49
- List < LogEntry > entries = new ( batch . Count ) ;
50
-
51
- foreach ( var ( content , isErrorOutput ) in batch )
57
+ foreach ( var ( lineNumber , content , isErrorOutput ) in batch )
52
58
{
53
- entries . Add ( logParser . CreateLogEntry ( content , isErrorOutput ) ) ;
59
+ // Keep track of the base line number to ensure that we can calculate the line number of each log entry.
60
+ // This becomes important when the total number of log entries exceeds the limit and is truncated.
61
+ if ( _baseLineNumber is null )
62
+ {
63
+ _baseLineNumber = lineNumber ;
64
+ }
65
+
66
+ InsertSorted ( _logEntries , logParser . CreateLogEntry ( content , isErrorOutput ) ) ;
54
67
}
55
68
56
- await _jsModule ! . InvokeVoidAsync ( "addLogEntries" , cancellationToken , entries ) ;
69
+ StateHasChanged ( ) ;
57
70
}
58
71
}
59
72
60
- internal async Task ClearLogsAsync ( CancellationToken cancellationToken = default )
73
+ private void InsertSorted ( List < LogEntry > logEntries , LogEntry logEntry )
61
74
{
62
- await _cancellationSeries . ClearAsync ( ) ;
75
+ if ( logEntry . ParentId != null )
76
+ {
77
+ // If we have a parent id, then we know we're on a non-timestamped line that is part
78
+ // of a multi-line log entry. We need to find the prior line from that entry
79
+ for ( var rowIndex = logEntries . Count - 1 ; rowIndex >= 0 ; rowIndex -- )
80
+ {
81
+ var current = logEntries [ rowIndex ] ;
63
82
64
- if ( _jsModule is not null )
83
+ if ( current . Id == logEntry . ParentId && logEntry . LineIndex - 1 == current . LineIndex )
84
+ {
85
+ InsertLogEntry ( logEntries , rowIndex + 1 , logEntry ) ;
86
+ return ;
87
+ }
88
+ }
89
+ }
90
+ else if ( logEntry . Timestamp != null )
65
91
{
66
- await _jsModule . InvokeVoidAsync ( "clearLogs" , cancellationToken ) ;
92
+ // Otherwise, if we have a timestamped line, we just need to find the prior line.
93
+ // Since the rows are always in order, as soon as we see a timestamp
94
+ // that is less than the one we're adding, we can insert it immediately after that
95
+ for ( var rowIndex = logEntries . Count - 1 ; rowIndex >= 0 ; rowIndex -- )
96
+ {
97
+ var current = logEntries [ rowIndex ] ;
98
+ var currentTimestamp = current . Timestamp ?? current . ParentTimestamp ;
99
+
100
+ if ( currentTimestamp != null && currentTimestamp < logEntry . Timestamp )
101
+ {
102
+ InsertLogEntry ( logEntries , rowIndex + 1 , logEntry ) ;
103
+ return ;
104
+ }
105
+ }
106
+ }
107
+
108
+ // If we didn't find a place to insert then append it to the end. This happens with the first entry, but
109
+ // could also happen if the logs don't have recognized timestamps.
110
+ InsertLogEntry ( logEntries , logEntries . Count , logEntry ) ;
111
+
112
+ void InsertLogEntry ( List < LogEntry > logEntries , int index , LogEntry logEntry )
113
+ {
114
+ // Set the line number of the log entry.
115
+ if ( index == 0 )
116
+ {
117
+ Debug . Assert ( _baseLineNumber != null , "Should be set before this method is run." ) ;
118
+ logEntry . LineNumber = _baseLineNumber . Value ;
119
+ }
120
+ else
121
+ {
122
+ logEntry . LineNumber = logEntries [ index - 1 ] . LineNumber + 1 ;
123
+ }
124
+
125
+ logEntries . Insert ( index , logEntry ) ;
126
+
127
+ // If a log entry isn't inserted at the end then update the line numbers of all subsequent entries.
128
+ for ( var i = index + 1 ; i < logEntries . Count ; i ++ )
129
+ {
130
+ logEntries [ i ] . LineNumber ++ ;
131
+ }
67
132
}
68
133
}
69
134
70
- public async ValueTask DisposeAsync ( )
135
+ private string GetDisplayTimestamp ( DateTimeOffset timestamp )
71
136
{
72
- _whenDomReady . TrySetCanceled ( ) ;
137
+ if ( _convertTimestampsFromUtc )
138
+ {
139
+ timestamp = TimeProvider . ToLocal ( timestamp ) ;
140
+ }
141
+
142
+ return timestamp . ToString ( KnownFormats . ConsoleLogsTimestampFormat , CultureInfo . InvariantCulture ) ;
143
+ }
73
144
145
+ internal async Task ClearLogsAsync ( )
146
+ {
74
147
await _cancellationSeries . ClearAsync ( ) ;
75
148
76
- await JSInteropHelpers . SafeDisposeAsync ( _jsModule ) ;
149
+ _applicationChanged = true ;
150
+ _logEntries . Clear ( ) ;
151
+ StateHasChanged ( ) ;
152
+ }
153
+
154
+ public async ValueTask DisposeAsync ( )
155
+ {
156
+ await _cancellationSeries . ClearAsync ( ) ;
77
157
}
78
158
}
0 commit comments