Skip to content

Commit f7bfe4e

Browse files
committed
Enabled FileSink and RollingFileSink's output stream in another stream, such as a GZipStream
1 parent ab9fc9f commit f7bfe4e

File tree

8 files changed

+195
-18
lines changed

8 files changed

+195
-18
lines changed

src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -135,11 +135,12 @@ public static LoggerConfiguration File(
135135
/// <param name="shared">Allow the log file to be shared by multiple processes. The default is false.</param>
136136
/// <param name="flushToDiskInterval">If provided, a full disk flush will be performed periodically at the specified interval.</param>
137137
/// <param name="rollingInterval">The interval at which logging will roll over to a new file.</param>
138-
/// <param name="rollOnFileSizeLimit">If <code>true</code>, a new file will be created when the file size limit is reached. Filenames
138+
/// <param name="rollOnFileSizeLimit">If <code>true</code>, a new file will be created when the file size limit is reached. Filenames
139139
/// will have a number appended in the format <code>_NNN</code>, with the first filename given no number.</param>
140140
/// <param name="retainedFileCountLimit">The maximum number of log files that will be retained,
141141
/// including the current log file. For unlimited retention, pass null. The default is 31.</param>
142142
/// <param name="encoding">Character encoding used to write the text file. The default is UTF-8 without BOM.</param>
143+
/// <param name="wrapper">Optionally enables wrapping the output stream in another stream, such as a GZipStream.</param>
143144
/// <returns>Configuration object allowing method chaining.</returns>
144145
/// <remarks>The file will be written using the UTF-8 character set.</remarks>
145146
public static LoggerConfiguration File(
@@ -156,7 +157,8 @@ public static LoggerConfiguration File(
156157
RollingInterval rollingInterval = RollingInterval.Infinite,
157158
bool rollOnFileSizeLimit = false,
158159
int? retainedFileCountLimit = DefaultRetainedFileCountLimit,
159-
Encoding encoding = null)
160+
Encoding encoding = null,
161+
StreamWrapper wrapper = null)
160162
{
161163
if (sinkConfiguration == null) throw new ArgumentNullException(nameof(sinkConfiguration));
162164
if (path == null) throw new ArgumentNullException(nameof(path));
@@ -165,7 +167,7 @@ public static LoggerConfiguration File(
165167
var formatter = new MessageTemplateTextFormatter(outputTemplate, formatProvider);
166168
return File(sinkConfiguration, formatter, path, restrictedToMinimumLevel, fileSizeLimitBytes,
167169
levelSwitch, buffered, shared, flushToDiskInterval,
168-
rollingInterval, rollOnFileSizeLimit, retainedFileCountLimit, encoding);
170+
rollingInterval, rollOnFileSizeLimit, retainedFileCountLimit, encoding, wrapper);
169171
}
170172

171173
/// <summary>
@@ -174,7 +176,7 @@ public static LoggerConfiguration File(
174176
/// <param name="sinkConfiguration">Logger sink configuration.</param>
175177
/// <param name="formatter">A formatter, such as <see cref="JsonFormatter"/>, to convert the log events into
176178
/// 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)"/>
179+
/// overload of <see cref="File(LoggerSinkConfiguration, string, LogEventLevel, string, IFormatProvider, long?, LoggingLevelSwitch, bool, bool, TimeSpan?, RollingInterval, bool, int?, Encoding, StreamWrapper)"/>
178180
/// and specify the outputTemplate parameter instead.
179181
/// </param>
180182
/// <param name="path">Path to the file.</param>
@@ -190,11 +192,12 @@ public static LoggerConfiguration File(
190192
/// <param name="shared">Allow the log file to be shared by multiple processes. The default is false.</param>
191193
/// <param name="flushToDiskInterval">If provided, a full disk flush will be performed periodically at the specified interval.</param>
192194
/// <param name="rollingInterval">The interval at which logging will roll over to a new file.</param>
193-
/// <param name="rollOnFileSizeLimit">If <code>true</code>, a new file will be created when the file size limit is reached. Filenames
195+
/// <param name="rollOnFileSizeLimit">If <code>true</code>, a new file will be created when the file size limit is reached. Filenames
194196
/// will have a number appended in the format <code>_NNN</code>, with the first filename given no number.</param>
195197
/// <param name="retainedFileCountLimit">The maximum number of log files that will be retained,
196198
/// including the current log file. For unlimited retention, pass null. The default is 31.</param>
197199
/// <param name="encoding">Character encoding used to write the text file. The default is UTF-8 without BOM.</param>
200+
/// <param name="wrapper">Optionally enables wrapping the output stream in another stream, such as a GZipStream.</param>
198201
/// <returns>Configuration object allowing method chaining.</returns>
199202
/// <remarks>The file will be written using the UTF-8 character set.</remarks>
200203
public static LoggerConfiguration File(
@@ -210,10 +213,12 @@ public static LoggerConfiguration File(
210213
RollingInterval rollingInterval = RollingInterval.Infinite,
211214
bool rollOnFileSizeLimit = false,
212215
int? retainedFileCountLimit = DefaultRetainedFileCountLimit,
213-
Encoding encoding = null)
216+
Encoding encoding = null,
217+
StreamWrapper wrapper = null)
214218
{
215219
return ConfigureFile(sinkConfiguration.Sink, formatter, path, restrictedToMinimumLevel, fileSizeLimitBytes, levelSwitch,
216-
buffered, false, shared, flushToDiskInterval, encoding, rollingInterval, rollOnFileSizeLimit, retainedFileCountLimit);
220+
buffered, false, shared, flushToDiskInterval, encoding, rollingInterval, rollOnFileSizeLimit,
221+
retainedFileCountLimit, wrapper);
217222
}
218223

219224
/// <summary>
@@ -270,7 +275,7 @@ public static LoggerConfiguration File(
270275
LoggingLevelSwitch levelSwitch = null)
271276
{
272277
return ConfigureFile(sinkConfiguration.Sink, formatter, path, restrictedToMinimumLevel, null, levelSwitch, false, true,
273-
false, null, null, RollingInterval.Infinite, false, null);
278+
false, null, null, RollingInterval.Infinite, false, null, null);
274279
}
275280

276281
static LoggerConfiguration ConfigureFile(
@@ -287,7 +292,8 @@ static LoggerConfiguration ConfigureFile(
287292
Encoding encoding,
288293
RollingInterval rollingInterval,
289294
bool rollOnFileSizeLimit,
290-
int? retainedFileCountLimit)
295+
int? retainedFileCountLimit,
296+
StreamWrapper wrapper)
291297
{
292298
if (addSink == null) throw new ArgumentNullException(nameof(addSink));
293299
if (formatter == null) throw new ArgumentNullException(nameof(formatter));
@@ -300,7 +306,7 @@ static LoggerConfiguration ConfigureFile(
300306

301307
if (rollOnFileSizeLimit || rollingInterval != RollingInterval.Infinite)
302308
{
303-
sink = new RollingFileSink(path, formatter, fileSizeLimitBytes, retainedFileCountLimit, encoding, buffered, shared, rollingInterval, rollOnFileSizeLimit);
309+
sink = new RollingFileSink(path, formatter, fileSizeLimitBytes, retainedFileCountLimit, encoding, buffered, shared, rollingInterval, rollOnFileSizeLimit, wrapper);
304310
}
305311
else
306312
{
@@ -309,11 +315,16 @@ static LoggerConfiguration ConfigureFile(
309315
#pragma warning disable 618
310316
if (shared)
311317
{
318+
if (wrapper != null)
319+
{
320+
SelfLog.WriteLine("Unable to use output stream wrapper - these are not supported for shared log files");
321+
}
322+
312323
sink = new SharedFileSink(path, formatter, fileSizeLimitBytes);
313324
}
314325
else
315326
{
316-
sink = new FileSink(path, formatter, fileSizeLimitBytes, buffered: buffered);
327+
sink = new FileSink(path, formatter, fileSizeLimitBytes, buffered: buffered, wrapper: wrapper);
317328
}
318329
#pragma warning restore 618
319330
}

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

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,12 @@ public sealed class FileSink : IFileSink, IDisposable
4343
/// <param name="encoding">Character encoding used to write the text file. The default is UTF-8 without BOM.</param>
4444
/// <param name="buffered">Indicates if flushing to the output file can be buffered or not. The default
4545
/// is false.</param>
46+
/// <param name="wrapper">Optionally enables wrapping the output stream in another stream, such as a GZipStream.</param>
4647
/// <returns>Configuration object allowing method chaining.</returns>
4748
/// <remarks>The file will be written using the UTF-8 character set.</remarks>
4849
/// <exception cref="IOException"></exception>
49-
public FileSink(string path, ITextFormatter textFormatter, long? fileSizeLimitBytes, Encoding encoding = null, bool buffered = false)
50+
public FileSink(string path, ITextFormatter textFormatter, long? fileSizeLimitBytes, Encoding encoding = null, bool buffered = false,
51+
StreamWrapper wrapper = null)
5052
{
5153
if (path == null) throw new ArgumentNullException(nameof(path));
5254
if (textFormatter == null) throw new ArgumentNullException(nameof(textFormatter));
@@ -68,6 +70,11 @@ public FileSink(string path, ITextFormatter textFormatter, long? fileSizeLimitBy
6870
outputStream = _countingStreamWrapper = new WriteCountingStream(_underlyingStream);
6971
}
7072

73+
if (wrapper != null)
74+
{
75+
outputStream = wrapper.Wrap(outputStream);
76+
}
77+
7178
_output = new StreamWriter(outputStream, encoding ?? new UTF8Encoding(encoderShouldEmitUTF8Identifier: false));
7279
}
7380

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ sealed class RollingFileSink : ILogEventSink, IFlushableFileSink, IDisposable
3535
readonly bool _buffered;
3636
readonly bool _shared;
3737
readonly bool _rollOnFileSizeLimit;
38+
readonly StreamWrapper _wrapper;
3839

3940
readonly object _syncRoot = new object();
4041
bool _isDisposed;
@@ -50,7 +51,8 @@ public RollingFileSink(string path,
5051
bool buffered,
5152
bool shared,
5253
RollingInterval rollingInterval,
53-
bool rollOnFileSizeLimit)
54+
bool rollOnFileSizeLimit,
55+
StreamWrapper wrapper = null)
5456
{
5557
if (path == null) throw new ArgumentNullException(nameof(path));
5658
if (fileSizeLimitBytes.HasValue && fileSizeLimitBytes < 0) throw new ArgumentException("Negative value provided; file size limit must be non-negative");
@@ -64,6 +66,7 @@ public RollingFileSink(string path,
6466
_buffered = buffered;
6567
_shared = shared;
6668
_rollOnFileSizeLimit = rollOnFileSizeLimit;
69+
_wrapper = wrapper;
6770
}
6871

6972
public void Emit(LogEvent logEvent)
@@ -144,7 +147,7 @@ void OpenFile(DateTime now, int? minSequence = null)
144147
{
145148
_currentFile = _shared ?
146149
(IFileSink)new SharedFileSink(path, _textFormatter, _fileSizeLimitBytes, _encoding) :
147-
new FileSink(path, _textFormatter, _fileSizeLimitBytes, _encoding, _buffered);
150+
new FileSink(path, _textFormatter, _fileSizeLimitBytes, _encoding, _buffered, _wrapper);
148151
_currentFileSequence = sequence;
149152
}
150153
catch (IOException ex)
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
using System.IO;
2+
3+
namespace Serilog
4+
{
5+
/// <summary>
6+
/// Wraps the log file's output stream in another stream, such as a GZipStream
7+
/// </summary>
8+
public abstract class StreamWrapper
9+
{
10+
/// <summary>
11+
/// Wraps <paramref name="sourceStream"/> in another stream, such as a GZipStream, then returns the wrapped stream
12+
/// </summary>
13+
/// <param name="sourceStream">The source log file stream</param>
14+
/// <returns>The wrapped stream</returns>
15+
public abstract Stream Wrap(Stream sourceStream);
16+
}
17+
}

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

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1-
using System.IO;
1+
using System.Collections.Generic;
2+
using System.IO;
3+
using System.IO.Compression;
4+
using System.Text;
25
using Xunit;
36
using Serilog.Formatting.Json;
47
using Serilog.Sinks.File.Tests.Support;
58
using Serilog.Tests.Support;
6-
using System.Text;
79

810
#pragma warning disable 618
911

@@ -141,6 +143,42 @@ public void WhenLimitIsNotSpecifiedAndEncodingHasNoPreambleDataIsCorrectlyAppend
141143
WriteTwoEventsAndCheckOutputFileLength(null, encoding);
142144
}
143145

146+
[Fact]
147+
public void WhenStreamWrapperIsSpecifiedOutputStreamIsWrapped()
148+
{
149+
var gzipWrapper = new GZipStreamWrapper();
150+
151+
using (var tmp = TempFolder.ForCaller())
152+
{
153+
var nonexistent = tmp.AllocateFilename("txt");
154+
var evt = Some.LogEvent("Hello, world!");
155+
156+
using (var sink = new FileSink(nonexistent, new JsonFormatter(), null, wrapper: gzipWrapper))
157+
{
158+
sink.Emit(evt);
159+
sink.Emit(evt);
160+
}
161+
162+
// Ensure the data was written through the wrapping GZipStream, by decompressing and comparing against
163+
// what we wrote
164+
var lines = new List<string>();
165+
using (var textStream = new MemoryStream())
166+
{
167+
using (var fs = System.IO.File.OpenRead(nonexistent))
168+
using (var decompressStream = new GZipStream(fs, CompressionMode.Decompress))
169+
{
170+
decompressStream.CopyTo(textStream);
171+
}
172+
173+
textStream.Position = 0;
174+
lines = textStream.ReadAllLines();
175+
}
176+
177+
Assert.Equal(2, lines.Count);
178+
Assert.Contains("Hello, world!", lines[0]);
179+
}
180+
}
181+
144182
static void WriteTwoEventsAndCheckOutputFileLength(long? maxBytes, Encoding encoding)
145183
{
146184
using (var tmp = TempFolder.ForCaller())
@@ -170,4 +208,3 @@ static void WriteTwoEventsAndCheckOutputFileLength(long? maxBytes, Encoding enco
170208
}
171209
}
172210
}
173-

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

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System;
22
using System.Collections.Generic;
33
using System.IO;
4+
using System.IO.Compression;
45
using System.Linq;
56
using System.Reflection;
67
using Xunit;
@@ -96,6 +97,64 @@ public void WhenSizeLimitIsBreachedNewFilesCreated()
9697
}
9798
}
9899

100+
[Fact]
101+
public void WhenStreamWrapperSpecifiedIsUsedForRolledFiles()
102+
{
103+
var gzipWrapper = new GZipStreamWrapper();
104+
var fileName = Some.String() + ".txt";
105+
106+
using (var temp = new TempFolder())
107+
{
108+
string[] files;
109+
var logEvents = new[]
110+
{
111+
Some.InformationEvent(),
112+
Some.InformationEvent(),
113+
Some.InformationEvent()
114+
};
115+
116+
using (var log = new LoggerConfiguration()
117+
.WriteTo.File(Path.Combine(temp.Path, fileName), rollOnFileSizeLimit: true, fileSizeLimitBytes: 1, wrapper: gzipWrapper)
118+
.CreateLogger())
119+
{
120+
121+
foreach (var logEvent in logEvents)
122+
{
123+
log.Write(logEvent);
124+
}
125+
126+
files = Directory.GetFiles(temp.Path)
127+
.OrderBy(p => p, StringComparer.OrdinalIgnoreCase)
128+
.ToArray();
129+
130+
Assert.Equal(3, files.Length);
131+
Assert.True(files[0].EndsWith(fileName), files[0]);
132+
Assert.True(files[1].EndsWith("_001.txt"), files[1]);
133+
Assert.True(files[2].EndsWith("_002.txt"), files[2]);
134+
}
135+
136+
// Ensure the data was written through the wrapping GZipStream, by decompressing and comparing against
137+
// what we wrote
138+
for (var i = 0; i < files.Length; i++)
139+
{
140+
using (var textStream = new MemoryStream())
141+
{
142+
using (var fs = System.IO.File.OpenRead(files[i]))
143+
using (var decompressStream = new GZipStream(fs, CompressionMode.Decompress))
144+
{
145+
decompressStream.CopyTo(textStream);
146+
}
147+
148+
textStream.Position = 0;
149+
var lines = textStream.ReadAllLines();
150+
151+
Assert.Equal(1, lines.Count);
152+
Assert.True(lines[0].EndsWith(logEvents[i].MessageTemplate.Text));
153+
}
154+
}
155+
}
156+
}
157+
99158
[Fact]
100159
public void IfTheLogFolderDoesNotExistItWillBeCreated()
101160
{

test/Serilog.Sinks.File.Tests/Support/Extensions.cs

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
using Serilog.Events;
1+
using System.Collections.Generic;
2+
using System.IO;
3+
using Serilog.Events;
24

35
namespace Serilog.Sinks.File.Tests.Support
46
{
@@ -8,5 +10,21 @@ public static object LiteralValue(this LogEventPropertyValue @this)
810
{
911
return ((ScalarValue)@this).Value;
1012
}
13+
14+
public static List<string> ReadAllLines(this Stream @this)
15+
{
16+
var lines = new List<string>();
17+
18+
using (var reader = new StreamReader(@this))
19+
{
20+
string line;
21+
while ((line = reader.ReadLine()) != null)
22+
{
23+
lines.Add(line);
24+
}
25+
}
26+
27+
return lines;
28+
}
1129
}
1230
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
using System.IO;
2+
using System.IO.Compression;
3+
4+
namespace Serilog.Sinks.File.Tests.Support
5+
{
6+
/// <inheritdoc />
7+
/// <summary>
8+
/// Demonstrates the use of <seealso cref="T:Serilog.StreamWrapper" />, by compressing log output using GZip
9+
/// </summary>
10+
public class GZipStreamWrapper : StreamWrapper
11+
{
12+
readonly int _bufferSize;
13+
14+
public GZipStreamWrapper(int bufferSize = 1024 * 32)
15+
{
16+
_bufferSize = bufferSize;
17+
}
18+
19+
public override Stream Wrap(Stream sourceStream)
20+
{
21+
var compressStream = new GZipStream(sourceStream, CompressionMode.Compress);
22+
return new BufferedStream(compressStream, _bufferSize);
23+
}
24+
}
25+
}

0 commit comments

Comments
 (0)