@@ -34,6 +34,7 @@ public class FileLogWriter : LogWriter
3434 private readonly FileOptions _fileOptions ;
3535 private readonly bool _flushToDisk ;
3636 private readonly FileArchiveTimestampMode _archiveTimestampMode ;
37+ private readonly Func < FileArchiveFileNameContext , string > ? _archiveFileNameFormatter ;
3738 private readonly FileLogWriterFailureMode _failureMode ;
3839 private readonly Action < FileLogWriterFailureContext > ? _failureHandler ;
3940 private readonly int _retryCount ;
@@ -112,6 +113,7 @@ public FileLogWriter(FileLogWriterOptions options)
112113 _fileOptions = options . FileOptions ;
113114 _flushToDisk = options . FlushToDisk ;
114115 _archiveTimestampMode = options . ArchiveTimestampMode ;
116+ _archiveFileNameFormatter = options . ArchiveFileNameFormatter ;
115117 _failureMode = options . FailureMode ;
116118 _failureHandler = options . FailureHandler ;
117119 _retryCount = options . RetryCount ;
@@ -389,11 +391,22 @@ private bool TryMoveCurrentFileToArchive(DateTime timestamp)
389391 _ => throw new ArgumentOutOfRangeException ( nameof ( _archiveTimestampMode ) , _archiveTimestampMode , null )
390392 } ;
391393
392- var stamp = archiveTimestamp . ToString ( "yyyyMMddHHmmssfff" , CultureInfo . InvariantCulture ) ;
393394 var sequence = 0L ;
395+ var attemptedFileNames = new HashSet < string > ( StringComparer . Ordinal ) ;
394396 while ( true )
395397 {
396- var archivePath = GetArchivePath ( stamp , sequence ) ;
398+ var archiveFileName = GetArchiveFileName ( archiveTimestamp , sequence ) ;
399+ if ( ! attemptedFileNames . Add ( archiveFileName ) )
400+ {
401+ archiveFileName = AppendSequenceSuffix ( archiveFileName , sequence ) ;
402+ if ( ! attemptedFileNames . Add ( archiveFileName ) )
403+ {
404+ sequence ++ ;
405+ continue ;
406+ }
407+ }
408+
409+ var archivePath = Path . Combine ( _directoryPath , archiveFileName ) ;
397410 try
398411 {
399412 File . Move ( _filePath , archivePath , overwrite : false ) ;
@@ -412,12 +425,35 @@ private bool TryMoveCurrentFileToArchive(DateTime timestamp)
412425 }
413426 }
414427
415- private string GetArchivePath ( string stamp , long sequence )
428+ private string GetArchiveFileName ( DateTime archiveTimestamp , long sequence )
429+ {
430+ var context = new FileArchiveFileNameContext ( _archiveFileNamePrefix , _archiveFileExtension , archiveTimestamp , sequence ) ;
431+ var formatter = _archiveFileNameFormatter ;
432+ var archiveFileName = formatter is null
433+ ? FileArchiveFileNameFormatters . Compact ( context )
434+ : formatter ( context ) ;
435+
436+ if ( string . IsNullOrWhiteSpace ( archiveFileName ) )
437+ {
438+ throw new InvalidOperationException ( "ArchiveFileNameFormatter returned null, empty, or whitespace." ) ;
439+ }
440+
441+ if ( ! string . Equals ( Path . GetFileName ( archiveFileName ) , archiveFileName , StringComparison . Ordinal ) )
442+ {
443+ throw new InvalidOperationException ( "ArchiveFileNameFormatter must return a file name, not a path." ) ;
444+ }
445+
446+ return archiveFileName ;
447+ }
448+
449+ private static string AppendSequenceSuffix ( string archiveFileName , long sequence )
416450 {
417- var suffix = sequence == 0
418- ? stamp
419- : $ "{ stamp } .{ sequence . ToString ( CultureInfo . InvariantCulture ) } ";
420- return Path . Combine ( _directoryPath , $ "{ _archiveFileNamePrefix } .{ suffix } { _archiveFileExtension } ") ;
451+ var extension = Path . GetExtension ( archiveFileName ) ;
452+ var extensionLength = extension . Length ;
453+ var baseFileName = extensionLength == 0
454+ ? archiveFileName
455+ : archiveFileName . Substring ( 0 , archiveFileName . Length - extensionLength ) ;
456+ return $ "{ baseFileName } .{ sequence . ToString ( CultureInfo . InvariantCulture ) } { extension } ";
421457 }
422458
423459 private void ApplyRetentionPolicy ( )
0 commit comments