@@ -20,6 +20,7 @@ public class ResourceLoggerService
20
20
internal TimeProvider TimeProvider { get ; set ; } = TimeProvider . System ;
21
21
22
22
private readonly ConcurrentDictionary < string , ResourceLoggerState > _loggers = new ( ) ;
23
+ private IConsoleLogsService _consoleLogsService = new FakeConsoleLogsService ( ) ;
23
24
private Action < ( string , ResourceLoggerState ) > ? _loggerAdded ;
24
25
private event Action < ( string , ResourceLoggerState ) > LoggerAdded
25
26
{
@@ -146,6 +147,26 @@ internal Action<LogEntry> GetInternalLogger(string resourceName)
146
147
return ( logEntry ) => state . AddLog ( logEntry , inMemorySource : false ) ;
147
148
}
148
149
150
+ /// <summary>
151
+ /// Get all logs for a resource. This will return all logs that have been written to the log stream for the resource and then complete.
152
+ /// </summary>
153
+ /// <param name="resource">The resource to get all logs for.</param>
154
+ /// <returns>An async enumerable that returns all logs that have been written to the log stream and then completes.</returns>
155
+ public IAsyncEnumerable < IReadOnlyList < LogLine > > GetAllAsync ( IResource resource )
156
+ {
157
+ ArgumentNullException . ThrowIfNull ( resource ) ;
158
+
159
+ var resourceNames = resource . GetResolvedResourceNames ( ) ;
160
+ if ( resourceNames . Length > 1 )
161
+ {
162
+ return CombineMultipleAsync ( resourceNames , GetAllAsync ) ;
163
+ }
164
+ else
165
+ {
166
+ return GetAllAsync ( resourceNames [ 0 ] ) ;
167
+ }
168
+ }
169
+
149
170
/// <summary>
150
171
/// Watch for changes to the log stream for a resource.
151
172
/// </summary>
@@ -158,43 +179,24 @@ public IAsyncEnumerable<IReadOnlyList<LogLine>> WatchAsync(IResource resource)
158
179
var resourceNames = resource . GetResolvedResourceNames ( ) ;
159
180
if ( resourceNames . Length > 1 )
160
181
{
161
- return WatchMultipleAsync ( resourceNames , WatchAsync ) ;
182
+ return CombineMultipleAsync ( resourceNames , WatchAsync ) ;
162
183
}
163
184
else
164
185
{
165
186
return WatchAsync ( resourceNames [ 0 ] ) ;
166
187
}
188
+ }
167
189
168
- static async IAsyncEnumerable < IReadOnlyList < LogLine > > WatchMultipleAsync ( string [ ] resourceNames , Func < string , IAsyncEnumerable < IReadOnlyList < LogLine > > > watch )
169
- {
170
- var channel = Channel . CreateUnbounded < IReadOnlyList < LogLine > > ( ) ;
171
- var readTasks = resourceNames . Select ( async ( name ) =>
172
- {
173
- await foreach ( var logLines in watch ( name ) . ConfigureAwait ( false ) )
174
- {
175
- channel . Writer . TryWrite ( logLines ) ;
176
- }
177
- } ) ;
178
-
179
- var completionTask = Task . Run ( async ( ) =>
180
- {
181
- try
182
- {
183
- await Task . WhenAll ( readTasks ) . ConfigureAwait ( false ) ;
184
- }
185
- finally
186
- {
187
- channel . Writer . Complete ( ) ;
188
- }
189
- } ) ;
190
-
191
- await foreach ( var item in channel . Reader . ReadAllAsync ( ) . ConfigureAwait ( false ) )
192
- {
193
- yield return item ;
194
- }
190
+ /// <summary>
191
+ /// Get all logs for a resource. This will return all logs that have been written to the log stream for the resource and then complete.
192
+ /// </summary>
193
+ /// <param name="resourceName">The resource name</param>
194
+ /// <returns>An async enumerable that returns all logs that have been written to the log stream and then completes.</returns>
195
+ public IAsyncEnumerable < IReadOnlyList < LogLine > > GetAllAsync ( string resourceName )
196
+ {
197
+ ArgumentNullException . ThrowIfNull ( resourceName ) ;
195
198
196
- await completionTask . ConfigureAwait ( false ) ;
197
- }
199
+ return GetResourceLoggerState ( resourceName ) . GetAllAsync ( ) ;
198
200
}
199
201
200
202
/// <summary>
@@ -290,11 +292,42 @@ public void ClearBacklog(string resourceName)
290
292
}
291
293
}
292
294
295
+ private static async IAsyncEnumerable < IReadOnlyList < LogLine > > CombineMultipleAsync ( string [ ] resourceNames , Func < string , IAsyncEnumerable < IReadOnlyList < LogLine > > > fetch )
296
+ {
297
+ var channel = Channel . CreateUnbounded < IReadOnlyList < LogLine > > ( ) ;
298
+ var readTasks = resourceNames . Select ( async ( name ) =>
299
+ {
300
+ await foreach ( var logLines in fetch ( name ) . ConfigureAwait ( false ) )
301
+ {
302
+ channel . Writer . TryWrite ( logLines ) ;
303
+ }
304
+ } ) ;
305
+
306
+ var completionTask = Task . Run ( async ( ) =>
307
+ {
308
+ try
309
+ {
310
+ await Task . WhenAll ( readTasks ) . ConfigureAwait ( false ) ;
311
+ }
312
+ finally
313
+ {
314
+ channel . Writer . Complete ( ) ;
315
+ }
316
+ } ) ;
317
+
318
+ await foreach ( var item in channel . Reader . ReadAllAsync ( ) . ConfigureAwait ( false ) )
319
+ {
320
+ yield return item ;
321
+ }
322
+
323
+ await completionTask . ConfigureAwait ( false ) ;
324
+ }
325
+
293
326
// Internal for testing.
294
327
internal ResourceLoggerState GetResourceLoggerState ( string resourceName ) =>
295
328
_loggers . GetOrAdd ( resourceName , ( name , context ) =>
296
329
{
297
- var state = new ResourceLoggerState ( TimeProvider ) ;
330
+ var state = new ResourceLoggerState ( name , TimeProvider , _consoleLogsService ) ;
298
331
context . _loggerAdded ? . Invoke ( ( name , state ) ) ;
299
332
return state ;
300
333
} ,
@@ -314,15 +347,19 @@ internal sealed class ResourceLoggerState
314
347
315
348
private readonly CircularBuffer < LogEntry > _inMemoryEntries = new ( MaxLogCount ) ;
316
349
private readonly LogEntries _backlog = new ( MaxLogCount ) { BaseLineNumber = 0 } ;
350
+ private readonly string _name ;
317
351
private readonly TimeProvider _timeProvider ;
352
+ private readonly IConsoleLogsService _consoleLogsService ;
318
353
319
354
/// <summary>
320
355
/// Creates a new <see cref="ResourceLoggerState"/>.
321
356
/// </summary>
322
- public ResourceLoggerState ( TimeProvider timeProvider )
357
+ public ResourceLoggerState ( string name , TimeProvider timeProvider , IConsoleLogsService consoleLogsService )
323
358
{
324
359
_logger = new ResourceLogger ( this ) ;
360
+ _name = name ;
325
361
_timeProvider = timeProvider ;
362
+ _consoleLogsService = consoleLogsService ;
326
363
}
327
364
328
365
private Action < bool > ? _onSubscribersChanged ;
@@ -353,6 +390,25 @@ public event Action<bool> OnSubscribersChanged
353
390
}
354
391
}
355
392
393
+ public async IAsyncEnumerable < IReadOnlyList < LogLine > > GetAllAsync ( [ EnumeratorCancellation ] CancellationToken cancellationToken = default )
394
+ {
395
+ var consoleLogsEnumerable = _consoleLogsService . GetAllLogsAsync ( _name , cancellationToken ) ;
396
+
397
+ List < LogEntry > inMemoryEntries ;
398
+ lock ( _lock )
399
+ {
400
+ inMemoryEntries = _inMemoryEntries . ToList ( ) ;
401
+ }
402
+
403
+ var lineNumber = 0 ;
404
+ yield return CreateLogLines ( ref lineNumber , inMemoryEntries ) ;
405
+
406
+ await foreach ( var item in consoleLogsEnumerable . ConfigureAwait ( false ) )
407
+ {
408
+ yield return CreateLogLines ( ref lineNumber , item ) ;
409
+ }
410
+ }
411
+
356
412
/// <summary>
357
413
/// Watch for changes to the log stream for a resource.
358
414
/// </summary>
@@ -408,25 +464,6 @@ public async IAsyncEnumerable<IReadOnlyList<LogLine>> WatchAsync([EnumeratorCanc
408
464
channel . Writer . TryComplete ( ) ;
409
465
}
410
466
}
411
-
412
- static LogLine [ ] CreateLogLines ( ref int lineNumber , IReadOnlyList < LogEntry > entries )
413
- {
414
- var logs = new LogLine [ entries . Count ] ;
415
- for ( var i = 0 ; i < entries . Count ; i ++ )
416
- {
417
- var entry = entries [ i ] ;
418
- var content = entry . Content ?? string . Empty ;
419
- if ( entry . Timestamp != null )
420
- {
421
- content = entry . Timestamp . Value . ToString ( KnownFormats . ConsoleLogsTimestampFormat , CultureInfo . InvariantCulture ) + " " + content ;
422
- }
423
-
424
- logs [ i ] = new LogLine ( lineNumber , content , entry . Type == LogEntryType . Error ) ;
425
- lineNumber ++ ;
426
- }
427
-
428
- return logs ;
429
- }
430
467
}
431
468
432
469
private bool HasSubscribers
@@ -554,6 +591,38 @@ public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Except
554
591
}
555
592
}
556
593
}
594
+
595
+ private static LogLine [ ] CreateLogLines ( ref int lineNumber , IReadOnlyList < LogEntry > entries )
596
+ {
597
+ var logs = new LogLine [ entries . Count ] ;
598
+ for ( var i = 0 ; i < entries . Count ; i ++ )
599
+ {
600
+ var entry = entries [ i ] ;
601
+ var content = entry . Content ?? string . Empty ;
602
+ if ( entry . Timestamp != null )
603
+ {
604
+ content = entry . Timestamp . Value . ToString ( KnownFormats . ConsoleLogsTimestampFormat , CultureInfo . InvariantCulture ) + " " + content ;
605
+ }
606
+
607
+ logs [ i ] = new LogLine ( lineNumber , content , entry . Type == LogEntryType . Error ) ;
608
+ lineNumber ++ ;
609
+ }
610
+
611
+ return logs ;
612
+ }
613
+
614
+ internal void SetConsoleLogsService ( IConsoleLogsService consoleLogsService )
615
+ {
616
+ _consoleLogsService = consoleLogsService ;
617
+ }
618
+
619
+ private sealed class FakeConsoleLogsService : IConsoleLogsService
620
+ {
621
+ public IAsyncEnumerable < IReadOnlyList < LogEntry > > GetAllLogsAsync ( string resourceName , CancellationToken cancellationToken )
622
+ {
623
+ throw new InvalidOperationException ( $ "Getting all logs requires the { nameof ( ResourceLoggerService ) } instance created by DI.") ;
624
+ }
625
+ }
557
626
}
558
627
559
628
/// <summary>
0 commit comments