Skip to content

Commit 6bccc72

Browse files
committed
Add trimFileSizeBytes option
1 parent f18cb0e commit 6bccc72

File tree

3 files changed

+74
-16
lines changed

3 files changed

+74
-16
lines changed

src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs

+18-8
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ public static class FileLoggerConfigurationExtensions
5050
/// <param name="fileSizeLimitBytes">The approximate maximum size, in bytes, to which a log file will be allowed to grow.
5151
/// For unrestricted growth, pass null. The default is 1 GB. To avoid writing partial events, the last event within the limit
5252
/// will be written in full even if it exceeds the limit.</param>
53+
/// <param name="trimFileSizeBytes">The size a log file will trimmed to after it reaches fileSizeLimitBytes</param>
5354
/// <param name="buffered">Indicates if flushing to the output file can be buffered or not. The default
5455
/// is false.</param>
5556
/// <param name="shared">Allow the log file to be shared by multiple processes. The default is false.</param>
@@ -64,13 +65,14 @@ public static LoggerConfiguration File(
6465
string outputTemplate,
6566
IFormatProvider formatProvider,
6667
long? fileSizeLimitBytes,
68+
int? trimFileSizeBytes,
6769
LoggingLevelSwitch levelSwitch,
6870
bool buffered,
6971
bool shared,
7072
TimeSpan? flushToDiskInterval)
7173
{
7274
// ReSharper disable once RedundantArgumentDefaultValue
73-
return File(sinkConfiguration, path, restrictedToMinimumLevel, outputTemplate, formatProvider, fileSizeLimitBytes,
75+
return File(sinkConfiguration, path, restrictedToMinimumLevel, outputTemplate, formatProvider, fileSizeLimitBytes, trimFileSizeBytes,
7476
levelSwitch, buffered, shared, flushToDiskInterval, RollingInterval.Infinite, false,
7577
null, null);
7678
}
@@ -81,7 +83,7 @@ public static LoggerConfiguration File(
8183
/// <param name="sinkConfiguration">Logger sink configuration.</param>
8284
/// <param name="formatter">A formatter, such as <see cref="JsonFormatter"/>, to convert the log events into
8385
/// text for the file. If control of regular text formatting is required, use the other
84-
/// overload of <see cref="File(LoggerSinkConfiguration, string, LogEventLevel, string, IFormatProvider, long?, LoggingLevelSwitch, bool, bool, TimeSpan?)"/>
86+
/// overload of <see cref="File(LoggerSinkConfiguration, string, LogEventLevel, string, IFormatProvider, long?, int?, LoggingLevelSwitch, bool, bool, TimeSpan?)"/>
8587
/// and specify the outputTemplate parameter instead.
8688
/// </param>
8789
/// <param name="path">Path to the file.</param>
@@ -92,6 +94,7 @@ public static LoggerConfiguration File(
9294
/// <param name="fileSizeLimitBytes">The approximate maximum size, in bytes, to which a log file will be allowed to grow.
9395
/// For unrestricted growth, pass null. The default is 1 GB. To avoid writing partial events, the last event within the limit
9496
/// will be written in full even if it exceeds the limit.</param>
97+
/// <param name="trimFileSizeBytes">The size a log file will trimmed to after it reaches fileSizeLimitBytes</param>
9598
/// <param name="buffered">Indicates if flushing to the output file can be buffered or not. The default
9699
/// is false.</param>
97100
/// <param name="shared">Allow the log file to be shared by multiple processes. The default is false.</param>
@@ -105,13 +108,14 @@ public static LoggerConfiguration File(
105108
string path,
106109
LogEventLevel restrictedToMinimumLevel,
107110
long? fileSizeLimitBytes,
111+
int? trimFileSizeBytes,
108112
LoggingLevelSwitch levelSwitch,
109113
bool buffered,
110114
bool shared,
111115
TimeSpan? flushToDiskInterval)
112116
{
113117
// ReSharper disable once RedundantArgumentDefaultValue
114-
return File(sinkConfiguration, formatter, path, restrictedToMinimumLevel, fileSizeLimitBytes, levelSwitch,
118+
return File(sinkConfiguration, formatter, path, restrictedToMinimumLevel, fileSizeLimitBytes, trimFileSizeBytes, levelSwitch,
115119
buffered, shared, flushToDiskInterval, RollingInterval.Infinite, false, null, null);
116120
}
117121

@@ -130,6 +134,7 @@ public static LoggerConfiguration File(
130134
/// <param name="fileSizeLimitBytes">The approximate maximum size, in bytes, to which a log file will be allowed to grow.
131135
/// For unrestricted growth, pass null. The default is 1 GB. To avoid writing partial events, the last event within the limit
132136
/// will be written in full even if it exceeds the limit.</param>
137+
/// <param name="trimFileSizeBytes">The size a log file will trimmed to after it reaches fileSizeLimitBytes</param>
133138
/// <param name="buffered">Indicates if flushing to the output file can be buffered or not. The default
134139
/// is false.</param>
135140
/// <param name="shared">Allow the log file to be shared by multiple processes. The default is false.</param>
@@ -149,6 +154,7 @@ public static LoggerConfiguration File(
149154
string outputTemplate = DefaultOutputTemplate,
150155
IFormatProvider formatProvider = null,
151156
long? fileSizeLimitBytes = DefaultFileSizeLimitBytes,
157+
int? trimFileSizeBytes = null,
152158
LoggingLevelSwitch levelSwitch = null,
153159
bool buffered = false,
154160
bool shared = false,
@@ -163,7 +169,7 @@ public static LoggerConfiguration File(
163169
if (outputTemplate == null) throw new ArgumentNullException(nameof(outputTemplate));
164170

165171
var formatter = new MessageTemplateTextFormatter(outputTemplate, formatProvider);
166-
return File(sinkConfiguration, formatter, path, restrictedToMinimumLevel, fileSizeLimitBytes,
172+
return File(sinkConfiguration, formatter, path, restrictedToMinimumLevel, fileSizeLimitBytes, trimFileSizeBytes,
167173
levelSwitch, buffered, shared, flushToDiskInterval,
168174
rollingInterval, rollOnFileSizeLimit, retainedFileCountLimit, encoding);
169175
}
@@ -174,7 +180,7 @@ public static LoggerConfiguration File(
174180
/// <param name="sinkConfiguration">Logger sink configuration.</param>
175181
/// <param name="formatter">A formatter, such as <see cref="JsonFormatter"/>, to convert the log events into
176182
/// text for the file. If control of regular text formatting is required, use the other
177-
/// overload of <see cref="File(LoggerSinkConfiguration, string, LogEventLevel, string, IFormatProvider, long?, LoggingLevelSwitch, bool, bool, TimeSpan?, RollingInterval, bool, int?, Encoding)"/>
183+
/// overload of <see cref="File(LoggerSinkConfiguration, string, LogEventLevel, string, IFormatProvider, long?, int?, LoggingLevelSwitch, bool, bool, TimeSpan?, RollingInterval, bool, int?, Encoding)"/>
178184
/// and specify the outputTemplate parameter instead.
179185
/// </param>
180186
/// <param name="path">Path to the file.</param>
@@ -185,6 +191,7 @@ public static LoggerConfiguration File(
185191
/// <param name="fileSizeLimitBytes">The approximate maximum size, in bytes, to which a log file will be allowed to grow.
186192
/// For unrestricted growth, pass null. The default is 1 GB. To avoid writing partial events, the last event within the limit
187193
/// will be written in full even if it exceeds the limit.</param>
194+
/// <param name="trimFileSizeBytes">The size a log file will trimmed to after it reaches fileSizeLimitBytes</param>
188195
/// <param name="buffered">Indicates if flushing to the output file can be buffered or not. The default
189196
/// is false.</param>
190197
/// <param name="shared">Allow the log file to be shared by multiple processes. The default is false.</param>
@@ -203,6 +210,7 @@ public static LoggerConfiguration File(
203210
string path,
204211
LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum,
205212
long? fileSizeLimitBytes = DefaultFileSizeLimitBytes,
213+
int? trimFileSizeBytes = null,
206214
LoggingLevelSwitch levelSwitch = null,
207215
bool buffered = false,
208216
bool shared = false,
@@ -212,7 +220,7 @@ public static LoggerConfiguration File(
212220
int? retainedFileCountLimit = DefaultRetainedFileCountLimit,
213221
Encoding encoding = null)
214222
{
215-
return ConfigureFile(sinkConfiguration.Sink, formatter, path, restrictedToMinimumLevel, fileSizeLimitBytes, levelSwitch,
223+
return ConfigureFile(sinkConfiguration.Sink, formatter, path, restrictedToMinimumLevel, fileSizeLimitBytes, trimFileSizeBytes, levelSwitch,
216224
buffered, false, shared, flushToDiskInterval, encoding, rollingInterval, rollOnFileSizeLimit, retainedFileCountLimit);
217225
}
218226

@@ -269,7 +277,7 @@ public static LoggerConfiguration File(
269277
LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum,
270278
LoggingLevelSwitch levelSwitch = null)
271279
{
272-
return ConfigureFile(sinkConfiguration.Sink, formatter, path, restrictedToMinimumLevel, null, levelSwitch, false, true,
280+
return ConfigureFile(sinkConfiguration.Sink, formatter, path, restrictedToMinimumLevel, null, null, levelSwitch, false, true,
273281
false, null, null, RollingInterval.Infinite, false, null);
274282
}
275283

@@ -279,6 +287,7 @@ static LoggerConfiguration ConfigureFile(
279287
string path,
280288
LogEventLevel restrictedToMinimumLevel,
281289
long? fileSizeLimitBytes,
290+
int? trimFileSizeBytes,
282291
LoggingLevelSwitch levelSwitch,
283292
bool buffered,
284293
bool propagateExceptions,
@@ -293,6 +302,7 @@ static LoggerConfiguration ConfigureFile(
293302
if (formatter == null) throw new ArgumentNullException(nameof(formatter));
294303
if (path == null) throw new ArgumentNullException(nameof(path));
295304
if (fileSizeLimitBytes.HasValue && fileSizeLimitBytes < 0) throw new ArgumentException("Negative value provided; file size limit must be non-negative.", nameof(fileSizeLimitBytes));
305+
if (trimFileSizeBytes.HasValue && trimFileSizeBytes < 0) throw new ArgumentException("Negative value provided; trim file size must be non-negative.", nameof(trimFileSizeBytes));
296306
if (retainedFileCountLimit.HasValue && retainedFileCountLimit < 1) throw new ArgumentException("At least one file must be retained.", nameof(retainedFileCountLimit));
297307
if (shared && buffered) throw new ArgumentException("Buffered writes are not available when file sharing is enabled.", nameof(buffered));
298308

@@ -313,7 +323,7 @@ static LoggerConfiguration ConfigureFile(
313323
}
314324
else
315325
{
316-
sink = new FileSink(path, formatter, fileSizeLimitBytes, buffered: buffered);
326+
sink = new FileSink(path, formatter, fileSizeLimitBytes, trimFileSizeBytes, buffered: buffered);
317327
}
318328
#pragma warning restore 618
319329
}

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

+55-7
Original file line numberDiff line numberDiff line change
@@ -26,34 +26,41 @@ namespace Serilog.Sinks.File
2626
[Obsolete("This type will be removed from the public API in a future version; use `WriteTo.File()` instead.")]
2727
public sealed class FileSink : IFileSink, IDisposable
2828
{
29-
readonly TextWriter _output;
30-
readonly FileStream _underlyingStream;
29+
TextWriter _output;
30+
FileStream _underlyingStream;
3131
readonly ITextFormatter _textFormatter;
3232
readonly long? _fileSizeLimitBytes;
33+
readonly int? _trimFileSizeBytes;
3334
readonly bool _buffered;
3435
readonly object _syncRoot = new object();
35-
readonly WriteCountingStream _countingStreamWrapper;
36+
WriteCountingStream _countingStreamWrapper;
37+
38+
readonly string _path;
39+
readonly Encoding _encoding;
3640

3741
/// <summary>Construct a <see cref="FileSink"/>.</summary>
3842
/// <param name="path">Path to the file.</param>
3943
/// <param name="textFormatter">Formatter used to convert log events to text.</param>
4044
/// <param name="fileSizeLimitBytes">The approximate maximum size, in bytes, to which a log file will be allowed to grow.
4145
/// For unrestricted growth, pass null. The default is 1 GB. To avoid writing partial events, the last event within the limit
4246
/// will be written in full even if it exceeds the limit.</param>
47+
/// <param name="trimFileSizeBytes">The size a log file will trimmed to after it reaches fileSizeLimitBytes</param>
4348
/// <param name="encoding">Character encoding used to write the text file. The default is UTF-8 without BOM.</param>
4449
/// <param name="buffered">Indicates if flushing to the output file can be buffered or not. The default
4550
/// is false.</param>
4651
/// <returns>Configuration object allowing method chaining.</returns>
4752
/// <remarks>The file will be written using the UTF-8 character set.</remarks>
4853
/// <exception cref="IOException"></exception>
49-
public FileSink(string path, ITextFormatter textFormatter, long? fileSizeLimitBytes, Encoding encoding = null, bool buffered = false)
54+
public FileSink(string path, ITextFormatter textFormatter, long? fileSizeLimitBytes, int? trimFileSizeBytes = null, Encoding encoding = null, bool buffered = false)
5055
{
5156
if (path == null) throw new ArgumentNullException(nameof(path));
5257
if (textFormatter == null) throw new ArgumentNullException(nameof(textFormatter));
5358
if (fileSizeLimitBytes.HasValue && fileSizeLimitBytes < 0) throw new ArgumentException("Negative value provided; file size limit must be non-negative.");
59+
if (trimFileSizeBytes.HasValue && trimFileSizeBytes < 0) throw new ArgumentException("Negative value provided; trim file size must be non-negative.");
5460

5561
_textFormatter = textFormatter;
5662
_fileSizeLimitBytes = fileSizeLimitBytes;
63+
_trimFileSizeBytes = trimFileSizeBytes;
5764
_buffered = buffered;
5865

5966
var directory = Path.GetDirectoryName(path);
@@ -62,13 +69,45 @@ public FileSink(string path, ITextFormatter textFormatter, long? fileSizeLimitBy
6269
Directory.CreateDirectory(directory);
6370
}
6471

65-
Stream outputStream = _underlyingStream = System.IO.File.Open(path, FileMode.Append, FileAccess.Write, FileShare.Read);
72+
_path = path;
73+
_encoding = encoding;
74+
CreateOutput();
75+
}
76+
77+
long LogFileSize()
78+
{
79+
return new FileInfo(_path).Length;
80+
}
81+
82+
private void CreateOutput()
83+
{
84+
Stream outputStream = _underlyingStream = System.IO.File.Open(_path, FileMode.Append, FileAccess.Write, FileShare.Read);
6685
if (_fileSizeLimitBytes != null)
6786
{
6887
outputStream = _countingStreamWrapper = new WriteCountingStream(_underlyingStream);
6988
}
7089

71-
_output = new StreamWriter(outputStream, encoding ?? new UTF8Encoding(encoderShouldEmitUTF8Identifier: false));
90+
_output = new StreamWriter(outputStream, _encoding ?? new UTF8Encoding(encoderShouldEmitUTF8Identifier: false));
91+
}
92+
93+
private void TrimLog()
94+
{
95+
_output.Dispose();
96+
97+
byte[] appendBuffer;
98+
using (var appendFileStream = new FileStream(_path, FileMode.Open))
99+
{
100+
appendFileStream.Seek(-_trimFileSizeBytes.Value, SeekOrigin.End);
101+
appendBuffer = new byte[_trimFileSizeBytes.Value];
102+
appendFileStream.Read(appendBuffer, 0, _trimFileSizeBytes.Value);
103+
}
104+
105+
using (var truncateFileStream = new FileStream(_path, FileMode.Truncate))
106+
{
107+
truncateFileStream.Write(appendBuffer, 0, _trimFileSizeBytes.Value);
108+
}
109+
110+
CreateOutput();
72111
}
73112

74113
bool IFileSink.EmitOrOverflow(LogEvent logEvent)
@@ -78,8 +117,17 @@ bool IFileSink.EmitOrOverflow(LogEvent logEvent)
78117
{
79118
if (_fileSizeLimitBytes != null)
80119
{
81-
if (_countingStreamWrapper.CountedLength >= _fileSizeLimitBytes.Value)
120+
if (_trimFileSizeBytes != null)
121+
{
122+
if (_countingStreamWrapper.CountedLength >= _fileSizeLimitBytes)
123+
{
124+
TrimLog();
125+
}
126+
}
127+
else if (_countingStreamWrapper.CountedLength >= _fileSizeLimitBytes.Value)
128+
{
82129
return false;
130+
}
83131
}
84132

85133
_textFormatter.Format(logEvent, _output);

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ void OpenFile(DateTime now, int? minSequence = null)
144144
{
145145
_currentFile = _shared ?
146146
(IFileSink)new SharedFileSink(path, _textFormatter, _fileSizeLimitBytes, _encoding) :
147-
new FileSink(path, _textFormatter, _fileSizeLimitBytes, _encoding, _buffered);
147+
new FileSink(path, _textFormatter, _fileSizeLimitBytes, null, _encoding, _buffered);
148148
_currentFileSequence = sequence;
149149
}
150150
catch (IOException ex)

0 commit comments

Comments
 (0)