Skip to content

Commit 5697cc1

Browse files
authored
Merge pull request #337 from Falco20019/dev
Avoid forcing the file to be closed and reopened every 30 minutes if the previous open was successful. Avoids issues with file hooks needing to reprocess the same file.
2 parents 5e3ff41 + 455a10f commit 5697cc1

File tree

4 files changed

+248
-47
lines changed

4 files changed

+248
-47
lines changed

src/Serilog.Sinks.File/Sinks/File/FileSink.cs

+10-2
Original file line numberDiff line numberDiff line change
@@ -91,8 +91,16 @@ internal FileSink(
9191

9292
if (hooks != null)
9393
{
94-
outputStream = hooks.OnFileOpened(path, outputStream, encoding) ??
95-
throw new InvalidOperationException($"The file lifecycle hook `{nameof(FileLifecycleHooks.OnFileOpened)}(...)` returned `null`.");
94+
try
95+
{
96+
outputStream = hooks.OnFileOpened(path, outputStream, encoding) ??
97+
throw new InvalidOperationException($"The file lifecycle hook `{nameof(FileLifecycleHooks.OnFileOpened)}(...)` returned `null`.");
98+
}
99+
catch
100+
{
101+
outputStream?.Dispose();
102+
throw;
103+
}
96104
}
97105

98106
_output = new StreamWriter(outputStream, encoding);

src/Serilog.Sinks.File/Sinks/File/RollingFileSink.cs

+71-43
Original file line numberDiff line numberDiff line change
@@ -83,16 +83,23 @@ public void Emit(LogEvent logEvent)
8383
{
8484
AlignCurrentFileTo(now, nextSequence: true);
8585
}
86+
87+
/* TODO: We REALLY should add this to avoid stuff become missing undetected.
88+
if (_currentFile == null)
89+
{
90+
SelfLog.WriteLine("Log event {0} was lost since it was not possible to open the file or create a new one.", logEvent.RenderMessage());
91+
}
92+
*/
8693
}
8794
}
8895

8996
void AlignCurrentFileTo(DateTime now, bool nextSequence = false)
9097
{
91-
if (!_nextCheckpoint.HasValue)
98+
if (_currentFile == null && !_nextCheckpoint.HasValue)
9299
{
93100
OpenFile(now);
94101
}
95-
else if (nextSequence || now >= _nextCheckpoint.Value)
102+
else if (nextSequence || (_nextCheckpoint.HasValue && now >= _nextCheckpoint.Value))
96103
{
97104
int? minSequence = null;
98105
if (nextSequence)
@@ -112,68 +119,89 @@ void OpenFile(DateTime now, int? minSequence = null)
112119
{
113120
var currentCheckpoint = _roller.GetCurrentCheckpoint(now);
114121

115-
// We only try periodically because repeated failures
116-
// to open log files REALLY slow an app down.
117-
_nextCheckpoint = _roller.GetNextCheckpoint(now) ?? now.AddMinutes(30);
122+
_nextCheckpoint = _roller.GetNextCheckpoint(now);
118123

119-
var existingFiles = Enumerable.Empty<string>();
120124
try
121125
{
122-
if (Directory.Exists(_roller.LogFileDirectory))
126+
var existingFiles = Enumerable.Empty<string>();
127+
try
128+
{
129+
if (Directory.Exists(_roller.LogFileDirectory))
130+
{
131+
// ReSharper disable once ConvertClosureToMethodGroup
132+
existingFiles = Directory.GetFiles(_roller.LogFileDirectory, _roller.DirectorySearchPattern)
133+
.Select(f => Path.GetFileName(f));
134+
}
135+
}
136+
catch (DirectoryNotFoundException)
123137
{
124-
// ReSharper disable once ConvertClosureToMethodGroup
125-
existingFiles = Directory.GetFiles(_roller.LogFileDirectory, _roller.DirectorySearchPattern)
126-
.Select(f => Path.GetFileName(f));
127138
}
128-
}
129-
catch (DirectoryNotFoundException) { }
130139

131-
var latestForThisCheckpoint = _roller
132-
.SelectMatches(existingFiles)
133-
.Where(m => m.DateTime == currentCheckpoint)
140+
var latestForThisCheckpoint = _roller
141+
.SelectMatches(existingFiles)
142+
.Where(m => m.DateTime == currentCheckpoint)
134143
#if ENUMERABLE_MAXBY
135144
.MaxBy(m => m.SequenceNumber);
136145
#else
137-
.OrderByDescending(m => m.SequenceNumber)
138-
.FirstOrDefault();
146+
.OrderByDescending(m => m.SequenceNumber)
147+
.FirstOrDefault();
139148
#endif
140149

141-
var sequence = latestForThisCheckpoint?.SequenceNumber;
142-
if (minSequence != null)
143-
{
144-
if (sequence == null || sequence.Value < minSequence.Value)
145-
sequence = minSequence;
146-
}
147-
148-
const int maxAttempts = 3;
149-
for (var attempt = 0; attempt < maxAttempts; attempt++)
150-
{
151-
_roller.GetLogFilePath(now, sequence, out var path);
150+
var sequence = latestForThisCheckpoint?.SequenceNumber;
151+
if (minSequence != null)
152+
{
153+
if (sequence == null || sequence.Value < minSequence.Value)
154+
sequence = minSequence;
155+
}
152156

153-
try
157+
const int maxAttempts = 3;
158+
for (var attempt = 0; attempt < maxAttempts; attempt++)
154159
{
155-
_currentFile = _shared ?
160+
_roller.GetLogFilePath(now, sequence, out var path);
161+
162+
try
163+
{
164+
_currentFile = _shared
165+
?
156166
#pragma warning disable 618
157-
new SharedFileSink(path, _textFormatter, _fileSizeLimitBytes, _encoding) :
167+
new SharedFileSink(path, _textFormatter, _fileSizeLimitBytes, _encoding)
168+
:
158169
#pragma warning restore 618
159-
new FileSink(path, _textFormatter, _fileSizeLimitBytes, _encoding, _buffered, _hooks);
170+
new FileSink(path, _textFormatter, _fileSizeLimitBytes, _encoding, _buffered, _hooks);
160171

161-
_currentFileSequence = sequence;
172+
_currentFileSequence = sequence;
173+
}
174+
catch (IOException ex)
175+
{
176+
if (IOErrors.IsLockedFile(ex))
177+
{
178+
SelfLog.WriteLine(
179+
"File target {0} was locked, attempting to open next in sequence (attempt {1})", path,
180+
attempt + 1);
181+
sequence = (sequence ?? 0) + 1;
182+
continue;
183+
}
184+
185+
throw;
186+
}
187+
188+
ApplyRetentionPolicy(path, now);
189+
return;
162190
}
163-
catch (IOException ex)
191+
}
192+
finally
193+
{
194+
if (_currentFile == null)
164195
{
165-
if (IOErrors.IsLockedFile(ex))
196+
// We only try periodically because repeated failures
197+
// to open log files REALLY slow an app down.
198+
// If the next checkpoint would be earlier, keep it!
199+
var retryCheckpoint = now.AddMinutes(30);
200+
if (_nextCheckpoint == null || retryCheckpoint < _nextCheckpoint)
166201
{
167-
SelfLog.WriteLine("File target {0} was locked, attempting to open next in sequence (attempt {1})", path, attempt + 1);
168-
sequence = (sequence ?? 0) + 1;
169-
continue;
202+
_nextCheckpoint = retryCheckpoint;
170203
}
171-
172-
throw;
173204
}
174-
175-
ApplyRetentionPolicy(path, now);
176-
return;
177205
}
178206
}
179207

test/Serilog.Sinks.File.Tests/RollingFileSinkTests.cs

+131-2
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,25 @@
44
using Serilog.Sinks.File.Tests.Support;
55
using Serilog.Configuration;
66
using Serilog.Core;
7+
using Serilog.Debugging;
8+
using Xunit.Abstractions;
79

810
namespace Serilog.Sinks.File.Tests;
911

10-
public class RollingFileSinkTests
12+
public class RollingFileSinkTests : IDisposable
1113
{
14+
private readonly ITestOutputHelper _testOutputHelper;
15+
16+
public RollingFileSinkTests(ITestOutputHelper testOutputHelper)
17+
{
18+
_testOutputHelper = testOutputHelper;
19+
}
20+
21+
public void Dispose()
22+
{
23+
SelfLog.Disable();
24+
}
25+
1226
[Fact]
1327
public void LogEventsAreEmittedToTheFileNamedAccordingToTheEventTimestamp()
1428
{
@@ -145,6 +159,116 @@ public void WhenRetentionCountAndArchivingHookIsSetOldFilesAreCopiedAndOriginalD
145159
});
146160
}
147161

162+
[Fact]
163+
public void WhenFirstOpeningFailedWithLockRetryDelayedUntilNextCheckpoint()
164+
{
165+
var fileName = Some.String() + ".txt";
166+
using var temp = new TempFolder();
167+
using var log = new LoggerConfiguration()
168+
.WriteTo.File(Path.Combine(temp.Path, fileName), rollOnFileSizeLimit: true, fileSizeLimitBytes: 1, rollingInterval: RollingInterval.Minute, hooks: new FailOpeningHook(true, 2, 3, 4))
169+
.CreateLogger();
170+
LogEvent e1 = Some.InformationEvent(new DateTime(2012, 10, 28)),
171+
e2 = Some.InformationEvent(e1.Timestamp.AddSeconds(1)),
172+
e3 = Some.InformationEvent(e1.Timestamp.AddMinutes(5)),
173+
e4 = Some.InformationEvent(e1.Timestamp.AddMinutes(31));
174+
LogEvent[] logEvents = new[] { e1, e2, e3, e4 };
175+
176+
foreach (var logEvent in logEvents)
177+
{
178+
Clock.SetTestDateTimeNow(logEvent.Timestamp.DateTime);
179+
log.Write(logEvent);
180+
}
181+
182+
var files = Directory.GetFiles(temp.Path)
183+
.OrderBy(p => p, StringComparer.OrdinalIgnoreCase)
184+
.ToArray();
185+
var pattern = "yyyyMMddHHmm";
186+
187+
Assert.Equal(6, files.Length);
188+
// Successful write of e1:
189+
Assert.True(files[0].EndsWith(ExpectedFileName(fileName, e1.Timestamp, pattern)), files[0]);
190+
// Failing writes for e2, will be dropped and logged to SelfLog:
191+
Assert.True(files[1].EndsWith("_001.txt"), files[1]);
192+
Assert.True(files[2].EndsWith("_002.txt"), files[2]);
193+
Assert.True(files[3].EndsWith("_003.txt"), files[3]);
194+
// Successful write of e3:
195+
Assert.True(files[4].EndsWith(ExpectedFileName(fileName, e3.Timestamp, pattern)), files[4]);
196+
// Successful write of e4:
197+
Assert.True(files[5].EndsWith(ExpectedFileName(fileName, e4.Timestamp, pattern)), files[5]);
198+
}
199+
200+
[Fact]
201+
public void WhenFirstOpeningFailedWithLockRetryDelayed30Minutes()
202+
{
203+
var fileName = Some.String() + ".txt";
204+
using var temp = new TempFolder();
205+
using var log = new LoggerConfiguration()
206+
.WriteTo.File(Path.Combine(temp.Path, fileName), rollOnFileSizeLimit: true, fileSizeLimitBytes: 1, rollingInterval: RollingInterval.Hour, hooks: new FailOpeningHook(true, 2, 3, 4))
207+
.CreateLogger();
208+
LogEvent e1 = Some.InformationEvent(new DateTime(2012, 10, 28)),
209+
e2 = Some.InformationEvent(e1.Timestamp.AddSeconds(1)),
210+
e3 = Some.InformationEvent(e1.Timestamp.AddMinutes(5)),
211+
e4 = Some.InformationEvent(e1.Timestamp.AddMinutes(31));
212+
LogEvent[] logEvents = new[] { e1, e2, e3, e4 };
213+
214+
SelfLog.Enable(_testOutputHelper.WriteLine);
215+
foreach (var logEvent in logEvents)
216+
{
217+
Clock.SetTestDateTimeNow(logEvent.Timestamp.DateTime);
218+
log.Write(logEvent);
219+
}
220+
221+
var files = Directory.GetFiles(temp.Path)
222+
.OrderBy(p => p, StringComparer.OrdinalIgnoreCase)
223+
.ToArray();
224+
var pattern = "yyyyMMddHH";
225+
226+
Assert.Equal(4, files.Length);
227+
// Successful write of e1:
228+
Assert.True(files[0].EndsWith(ExpectedFileName(fileName, e1.Timestamp, pattern)), files[0]);
229+
// Failing writes for e2, will be dropped and logged to SelfLog; on lock it will try it three times:
230+
Assert.True(files[1].EndsWith("_001.txt"), files[1]);
231+
Assert.True(files[2].EndsWith("_002.txt"), files[2]);
232+
/* e3 will be dropped and logged to SelfLog without new file as it's in the 30 minutes cooldown and roller only starts on next hour! */
233+
// Successful write of e4, the third file will be retried after failing initially:
234+
Assert.True(files[3].EndsWith("_003.txt"), files[3]);
235+
}
236+
237+
[Fact]
238+
public void WhenFirstOpeningFailedWithoutLockRetryDelayed30Minutes()
239+
{
240+
var fileName = Some.String() + ".txt";
241+
using var temp = new TempFolder();
242+
using var log = new LoggerConfiguration()
243+
.WriteTo.File(Path.Combine(temp.Path, fileName), rollOnFileSizeLimit: true, fileSizeLimitBytes: 1, rollingInterval: RollingInterval.Hour, hooks: new FailOpeningHook(false, 2))
244+
.CreateLogger();
245+
LogEvent e1 = Some.InformationEvent(new DateTime(2012, 10, 28)),
246+
e2 = Some.InformationEvent(e1.Timestamp.AddSeconds(1)),
247+
e3 = Some.InformationEvent(e1.Timestamp.AddMinutes(5)),
248+
e4 = Some.InformationEvent(e1.Timestamp.AddMinutes(31));
249+
LogEvent[] logEvents = new[] { e1, e2, e3, e4 };
250+
251+
SelfLog.Enable(_testOutputHelper.WriteLine);
252+
foreach (var logEvent in logEvents)
253+
{
254+
Clock.SetTestDateTimeNow(logEvent.Timestamp.DateTime);
255+
log.Write(logEvent);
256+
}
257+
258+
var files = Directory.GetFiles(temp.Path)
259+
.OrderBy(p => p, StringComparer.OrdinalIgnoreCase)
260+
.ToArray();
261+
var pattern = "yyyyMMddHH";
262+
263+
Assert.Equal(2, files.Length);
264+
// Successful write of e1:
265+
Assert.True(files[0].EndsWith(ExpectedFileName(fileName, e1.Timestamp, pattern)), files[0]);
266+
/* Failing writes for e2, will be dropped and logged to SelfLog; on non-lock it will try it once */
267+
/* e3 will be dropped and logged to SelfLog without new file as it's in the 30 minutes cooldown and roller only starts on next hour! */
268+
// Successful write of e4, the file will be retried after failing initially:
269+
Assert.True(files[1].EndsWith("_001.txt"), files[1]);
270+
}
271+
148272
[Fact]
149273
public void WhenSizeLimitIsBreachedNewFilesCreated()
150274
{
@@ -279,7 +403,7 @@ static void TestRollingEventSequence(
279403
Clock.SetTestDateTimeNow(@event.Timestamp.DateTime);
280404
log.Write(@event);
281405

282-
var expected = pathFormat.Replace(".txt", @event.Timestamp.ToString("yyyyMMdd") + ".txt");
406+
var expected = ExpectedFileName(pathFormat, @event.Timestamp, "yyyyMMdd");
283407
Assert.True(System.IO.File.Exists(expected));
284408

285409
verified.Add(expected);
@@ -292,4 +416,9 @@ static void TestRollingEventSequence(
292416
Directory.Delete(folder, true);
293417
}
294418
}
419+
420+
static string ExpectedFileName(string fileName, DateTimeOffset timestamp, string pattern)
421+
{
422+
return fileName.Replace(".txt", timestamp.ToString(pattern) + ".txt");
423+
}
295424
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
using System.Text;
2+
3+
namespace Serilog.Sinks.File.Tests.Support;
4+
5+
/// <inheritdoc />
6+
/// <summary>
7+
/// Demonstrates the use of <seealso cref="T:Serilog.FileLifecycleHooks" />, by failing to open for the given amount of times.
8+
/// </summary>
9+
class FailOpeningHook : FileLifecycleHooks
10+
{
11+
readonly bool _asFileLocked;
12+
readonly int[] _failingInstances;
13+
14+
public int TimesOpened { get; private set; }
15+
16+
public FailOpeningHook(bool asFileLocked, params int[] failingInstances)
17+
{
18+
_asFileLocked = asFileLocked;
19+
_failingInstances = failingInstances;
20+
}
21+
22+
public override Stream OnFileOpened(string path, Stream stream, Encoding encoding)
23+
{
24+
TimesOpened++;
25+
if (_failingInstances.Contains(TimesOpened))
26+
{
27+
var message = $"We failed on try {TimesOpened}, the file was locked: {_asFileLocked}";
28+
29+
throw _asFileLocked
30+
? new IOException(message)
31+
: new Exception(message);
32+
}
33+
34+
return base.OnFileOpened(path, stream, encoding);
35+
}
36+
}

0 commit comments

Comments
 (0)