11// Licensed to the .NET Foundation under one or more agreements.
22// The .NET Foundation licenses this file to you under the MIT license.
33
4+ using System . Diagnostics ;
5+ using System . Globalization ;
46using Aspire . Dashboard . ConsoleLogs ;
7+ using Aspire . Dashboard . Extensions ;
58using Aspire . Dashboard . Model ;
69using Aspire . Dashboard . Utils ;
710using Microsoft . AspNetCore . Components ;
@@ -14,30 +17,35 @@ namespace Aspire.Dashboard.Components;
1417/// </summary>
1518public sealed partial class LogViewer
1619{
17- private readonly TaskCompletionSource _whenDomReady = new ( ) ;
1820 private readonly CancellationSeries _cancellationSeries = new ( ) ;
19- private IJSObjectReference ? _jsModule ;
21+ private bool _convertTimestampsFromUtc ;
22+ private bool _applicationChanged ;
2023
2124 [ Inject ]
2225 public required BrowserTimeProvider TimeProvider { get ; init ; }
2326
2427 protected override async Task OnAfterRenderAsync ( bool firstRender )
2528 {
29+ if ( _applicationChanged )
30+ {
31+ await JS . InvokeVoidAsync ( "resetContinuousScrollPosition" ) ;
32+ _applicationChanged = false ;
33+ }
2634 if ( firstRender )
2735 {
28- _jsModule ??= await JS . InvokeAsync < IJSObjectReference > ( "import" , "/Components/Controls/LogViewer.razor.js" ) ;
29-
30- _whenDomReady . TrySetResult ( ) ;
36+ await JS . InvokeVoidAsync ( "initializeContinuousScroll" ) ;
3137 }
3238 }
3339
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 )
3544 {
36- var cancellationToken = await _cancellationSeries . NextAsync ( ) ;
37- var logParser = new LogParser ( TimeProvider , convertTimestampsFromUtc ) ;
45+ _convertTimestampsFromUtc = convertTimestampsFromUtc ;
3846
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 ( ) ;
4149
4250 await foreach ( var batch in batches . WithCancellation ( cancellationToken ) )
4351 {
@@ -46,33 +54,105 @@ internal async Task SetLogSourceAsync(IAsyncEnumerable<IReadOnlyList<(string Con
4654 continue ;
4755 }
4856
49- List < LogEntry > entries = new ( batch . Count ) ;
50-
51- foreach ( var ( content , isErrorOutput ) in batch )
57+ foreach ( var ( lineNumber , content , isErrorOutput ) in batch )
5258 {
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 ) ) ;
5467 }
5568
56- await _jsModule ! . InvokeVoidAsync ( "addLogEntries" , cancellationToken , entries ) ;
69+ StateHasChanged ( ) ;
5770 }
5871 }
5972
60- internal async Task ClearLogsAsync ( CancellationToken cancellationToken = default )
73+ private void InsertSorted ( List < LogEntry > logEntries , LogEntry logEntry )
6174 {
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 ] ;
6382
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 )
6591 {
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+ }
67132 }
68133 }
69134
70- public async ValueTask DisposeAsync ( )
135+ private string GetDisplayTimestamp ( DateTimeOffset timestamp )
71136 {
72- _whenDomReady . TrySetCanceled ( ) ;
137+ if ( _convertTimestampsFromUtc )
138+ {
139+ timestamp = TimeProvider . ToLocal ( timestamp ) ;
140+ }
141+
142+ return timestamp . ToString ( KnownFormats . ConsoleLogsTimestampFormat , CultureInfo . InvariantCulture ) ;
143+ }
73144
145+ internal async Task ClearLogsAsync ( )
146+ {
74147 await _cancellationSeries . ClearAsync ( ) ;
75148
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 ( ) ;
77157 }
78158}
0 commit comments