Skip to content

Commit c71a023

Browse files
Determine next log file number from directory on first run (#41375)
1 parent 47f44c9 commit c71a023

File tree

2 files changed

+169
-16
lines changed

2 files changed

+169
-16
lines changed

src/Middleware/HttpLogging/src/FileLoggerProcessor.cs

Lines changed: 32 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -152,23 +152,28 @@ private async Task WriteMessagesAsync(List<string> messages, CancellationToken c
152152
{
153153
// Files are written up to _maxFileSize before rolling to a new file
154154
DateTime today = SystemDateTime.Now;
155+
156+
if (!TryCreateDirectory())
157+
{
158+
// return early if we fail to create the directory
159+
return;
160+
}
161+
155162
var fullName = GetFullName(today);
156163
// Don't write to an incomplete file left around by a previous FileLoggerProcessor
157164
if (_firstFile)
158165
{
159-
while (File.Exists(fullName))
166+
_fileNumber = GetFirstFileCount(today);
167+
fullName = GetFullName(today);
168+
if (_fileNumber >= W3CLoggerOptions.MaxFileCount)
160169
{
161-
_fileNumber++;
162-
if (_fileNumber >= W3CLoggerOptions.MaxFileCount)
163-
{
164-
_maxFilesReached = true;
165-
// Return early if log directory is already full
166-
Log.MaxFilesReached(_logger);
167-
return;
168-
}
169-
fullName = GetFullName(today);
170+
_maxFilesReached = true;
171+
// Return early if log directory is already full
172+
Log.MaxFilesReached(_logger);
173+
return;
170174
}
171175
}
176+
172177
_firstFile = false;
173178
if (_maxFilesReached)
174179
{
@@ -178,12 +183,6 @@ private async Task WriteMessagesAsync(List<string> messages, CancellationToken c
178183
return;
179184
}
180185
var fileInfo = new FileInfo(fullName);
181-
182-
if (!TryCreateDirectory())
183-
{
184-
// return early if we fail to create the directory
185-
return;
186-
}
187186
var streamWriter = GetStreamWriter(fullName);
188187

189188
try
@@ -296,6 +295,23 @@ public async ValueTask DisposeAsync()
296295
await _outputTask;
297296
}
298297

298+
private int GetFirstFileCount(DateTime date)
299+
{
300+
lock (_pathLock)
301+
{
302+
var searchString = FormattableString.Invariant($"{_fileName}{date.Year:0000}{date.Month:00}{date.Day:00}.*.txt");
303+
var files = new DirectoryInfo(_path)
304+
.GetFiles(searchString);
305+
306+
return files.Length == 0
307+
? 0
308+
: files
309+
.Max(x => int.TryParse(x.Name.Split('.').ElementAtOrDefault(Index.FromEnd(2)), out var parsed)
310+
? parsed + 1
311+
: 0);
312+
}
313+
}
314+
299315
private string GetFullName(DateTime date)
300316
{
301317
lock (_pathLock)

src/Middleware/HttpLogging/test/FileLoggerProcessorTests.cs

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
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;
45
using Microsoft.AspNetCore.Testing;
56
using Microsoft.Extensions.Hosting.Internal;
67
using Microsoft.Extensions.Logging.Abstractions;
8+
using Microsoft.Extensions.Logging.Testing;
79

810
namespace 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

Comments
 (0)