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 . Collections . Concurrent ;
45using Microsoft . AspNetCore . Testing ;
56using Microsoft . Extensions . Hosting . Internal ;
67using Microsoft . Extensions . Logging . Abstractions ;
8+ using Microsoft . Extensions . Logging . Testing ;
79
810namespace Microsoft . AspNetCore . HttpLogging ;
911
@@ -13,6 +15,7 @@ public class FileLoggerProcessorTests
1315 private string _messageOne = "Message one" ;
1416 private string _messageTwo = "Message two" ;
1517 private string _messageThree = "Message three" ;
18+ private string _messageFour = "Message four" ;
1619
1720 private DateTime _today = new DateTime ( 2021 , 01 , 01 , 12 , 00 , 00 ) ;
1821
@@ -200,6 +203,66 @@ public async Task RespectsMaxFileCount()
200203 }
201204 }
202205
206+ [ Fact ]
207+ public async Task StopsLoggingAfter10000Files ( )
208+ {
209+ var path = Path . Combine ( TempPath , Path . GetRandomFileName ( ) ) ;
210+ Directory . CreateDirectory ( path ) ;
211+ var mockSystemDateTime = new MockSystemDateTime
212+ {
213+ Now = _today
214+ } ;
215+
216+ try
217+ {
218+ string lastFileName ;
219+ var options = new W3CLoggerOptions ( )
220+ {
221+ LogDirectory = path ,
222+ FileSizeLimit = 5 ,
223+ RetainedFileCountLimit = 10000
224+ } ;
225+ var testSink = new TestSink ( ) ;
226+ var testLogger = new TestLoggerFactory ( testSink , enabled : true ) ;
227+ await using ( var logger = new FileLoggerProcessor ( new OptionsWrapperMonitor < W3CLoggerOptions > ( options ) , new HostingEnvironment ( ) , testLogger ) )
228+ {
229+ logger . SystemDateTime = mockSystemDateTime ;
230+ for ( int i = 0 ; i < 10000 ; i ++ )
231+ {
232+ logger . EnqueueMessage ( _messageOne ) ;
233+ }
234+ lastFileName = Path . Combine ( path , FormattableString . Invariant ( $ "{ options . FileName } { _today . Year : 0000} { _today . Month : 00} { _today . Day : 00} .9999.txt") ) ;
235+ await WaitForFile ( lastFileName , _messageOne . Length ) . DefaultTimeout ( ) ;
236+
237+ // directory is full, no warnings yet
238+ Assert . Equal ( 0 , testSink . Writes . Count ) ;
239+
240+ logger . EnqueueMessage ( _messageOne ) ;
241+ await WaitForCondition ( ( ) => testSink . Writes . FirstOrDefault ( ) ? . EventId . Name == "MaxFilesReached" ) . DefaultTimeout ( ) ;
242+ }
243+
244+ Assert . Equal ( 10000 , new DirectoryInfo ( path )
245+ . GetFiles ( )
246+ . ToArray ( ) . Length ) ;
247+
248+ // restarting the logger should do nothing since the folder is still full
249+ var testSink2 = new TestSink ( ) ;
250+ var testLogger2 = new TestLoggerFactory ( testSink2 , enabled : true ) ;
251+ await using ( var logger = new FileLoggerProcessor ( new OptionsWrapperMonitor < W3CLoggerOptions > ( options ) , new HostingEnvironment ( ) , testLogger2 ) )
252+ {
253+ Assert . Equal ( 0 , testSink2 . Writes . Count ) ;
254+
255+ logger . SystemDateTime = mockSystemDateTime ;
256+ logger . EnqueueMessage ( _messageOne ) ;
257+ await WaitForCondition ( ( ) => testSink2 . Writes . FirstOrDefault ( ) ? . EventId . Name == "MaxFilesReached" ) . DefaultTimeout ( ) ;
258+ }
259+ }
260+ finally
261+ {
262+ Helpers . DisposeDirectory ( path ) ;
263+ }
264+ }
265+
203266 [ Fact ]
204267 public async Task InstancesWriteToSameDirectory ( )
205268 {
@@ -349,6 +412,72 @@ public async Task WritesToNewFileOnNewInstance()
349412 Helpers . DisposeDirectory ( path ) ;
350413 }
351414 }
415+ [ Fact ]
416+ public async Task RollsTextFilesWhenFirstLogOfDayIsMissing ( )
417+ {
418+ var mockSystemDateTime = new MockSystemDateTime
419+ {
420+ Now = _today
421+ } ;
422+
423+ var path = Path . Combine ( TempPath , Path . GetRandomFileName ( ) ) ;
424+ Directory . CreateDirectory ( path ) ;
425+
426+ try
427+ {
428+ var options = new W3CLoggerOptions ( )
429+ {
430+ LogDirectory = path ,
431+ FileSizeLimit = 5 ,
432+ RetainedFileCountLimit = 2 ,
433+ } ;
434+ var fileName1 = Path . Combine ( path , FormattableString . Invariant ( $ "{ options . FileName } { _today . Year : 0000} { _today . Month : 00} { _today . Day : 00} .0000.txt") ) ;
435+ var fileName2 = Path . Combine ( path , FormattableString . Invariant ( $ "{ options . FileName } { _today . Year : 0000} { _today . Month : 00} { _today . Day : 00} .0001.txt") ) ;
436+ var fileName3 = Path . Combine ( path , FormattableString . Invariant ( $ "{ options . FileName } { _today . Year : 0000} { _today . Month : 00} { _today . Day : 00} .0002.txt") ) ;
437+ var fileName4 = Path . Combine ( path , FormattableString . Invariant ( $ "{ options . FileName } { _today . Year : 0000} { _today . Month : 00} { _today . Day : 00} .0003.txt") ) ;
438+
439+ await using ( var logger = new FileLoggerProcessor ( new OptionsWrapperMonitor < W3CLoggerOptions > ( options ) , new HostingEnvironment ( ) , NullLoggerFactory . Instance ) )
440+ {
441+ logger . SystemDateTime = mockSystemDateTime ;
442+ logger . EnqueueMessage ( _messageOne ) ;
443+ logger . EnqueueMessage ( _messageTwo ) ;
444+ logger . EnqueueMessage ( _messageThree ) ;
445+ // Pause for a bit before disposing so logger can finish logging
446+ await WaitForFile ( fileName3 , _messageThree . Length ) . DefaultTimeout ( ) ;
447+ }
448+
449+ // Even with a big enough FileSizeLimit, we still won't try to write to files from a previous instance.
450+ options . FileSizeLimit = 10000 ;
451+
452+ await using ( var logger = new FileLoggerProcessor ( new OptionsWrapperMonitor < W3CLoggerOptions > ( options ) , new HostingEnvironment ( ) , NullLoggerFactory . Instance ) )
453+ {
454+ logger . SystemDateTime = mockSystemDateTime ;
455+ logger . EnqueueMessage ( _messageFour ) ;
456+ // Pause for a bit before disposing so logger can finish logging
457+ await WaitForFile ( fileName4 , _messageFour . Length ) . DefaultTimeout ( ) ;
458+ }
459+
460+ var actualFiles = new DirectoryInfo ( path )
461+ . GetFiles ( )
462+ . Select ( f => f . Name )
463+ . OrderBy ( f => f )
464+ . ToArray ( ) ;
465+
466+ Assert . Equal ( 2 , actualFiles . Length ) ;
467+
468+ Assert . False ( File . Exists ( fileName1 ) ) ;
469+ Assert . False ( File . Exists ( fileName2 ) ) ;
470+ Assert . True ( File . Exists ( fileName3 ) ) ;
471+ Assert . True ( File . Exists ( fileName4 ) ) ;
472+
473+ Assert . Equal ( _messageThree + Environment . NewLine , File . ReadAllText ( fileName3 ) ) ;
474+ Assert . Equal ( _messageFour + Environment . NewLine , File . ReadAllText ( fileName4 ) ) ;
475+ }
476+ finally
477+ {
478+ Helpers . DisposeDirectory ( path ) ;
479+ }
480+ }
352481
353482 [ Fact ]
354483 public async Task WritesToNewFileOnOptionsChange ( )
@@ -428,6 +557,14 @@ private async Task WaitForFile(string fileName, int length)
428557 }
429558 }
430559
560+ private async Task WaitForCondition ( Func < bool > waitForLog )
561+ {
562+ while ( ! waitForLog ( ) )
563+ {
564+ await Task . Delay ( 10 ) ;
565+ }
566+ }
567+
431568 private async Task WaitForRoll ( string fileName )
432569 {
433570 while ( File . Exists ( fileName ) )
0 commit comments