Skip to content

FileLifecycleHooks #80

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 20 commits into from
Apr 22, 2019
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
f7bfe4e
Enabled FileSink and RollingFileSink's output stream in another strea…
cocowalla Feb 1, 2019
9eee731
Rename StreamWrapper to FileLifecycleHooks
cocowalla Feb 14, 2019
83d817f
Ignore Rider cache/options files
cocowalla Feb 14, 2019
63c3601
Add docs re wrapped stream ownership
cocowalla Feb 14, 2019
fca6eb9
Check for directory existence before attempting access.
billrob Mar 12, 2019
0c65484
Merge pull request #88 from billrob/dev
nblumhardt Mar 13, 2019
e604418
Improve log message when FileLifecycleHooks provided for a shared log…
cocowalla Apr 20, 2019
873f6d4
Throw clear exception when wrapping the output stream returns null
cocowalla Apr 20, 2019
e310ab9
Throw when using hooks with a shared file
cocowalla Apr 20, 2019
1864db1
Merge branch 'feature/stream-wrapper' of https://github.com/cocowalla…
nblumhardt Apr 22, 2019
b660b51
Make FileSink constructor changes non-breaking; fix a bug whereby a s…
nblumhardt Apr 22, 2019
b904968
Reenable an old test that was lost
nblumhardt Apr 22, 2019
b6090cc
A test for the encoding fix
nblumhardt Apr 22, 2019
34344ad
Move FileLifecycleHooks down under Serilog.Sinks.File - avoids namesp…
nblumhardt Apr 22, 2019
0ae234e
Enable hooks and encoding for auditing methods
nblumhardt Apr 22, 2019
55fcb2b
Add backwards-compatible configuration overloads
nblumhardt Apr 22, 2019
d4fa80a
Expose encoding to OnOpened() hook; switch to AppVeyor Linux builds
nblumhardt Apr 22, 2019
9f7352d
Fix Linux build script targets
nblumhardt Apr 22, 2019
ca0ac8c
Test for header writing
nblumhardt Apr 22, 2019
5b3d64a
OnOpened() -> OnFileOpened()
nblumhardt Apr 22, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 22 additions & 11 deletions src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -135,11 +135,12 @@ public static LoggerConfiguration File(
/// <param name="shared">Allow the log file to be shared by multiple processes. The default is false.</param>
/// <param name="flushToDiskInterval">If provided, a full disk flush will be performed periodically at the specified interval.</param>
/// <param name="rollingInterval">The interval at which logging will roll over to a new file.</param>
/// <param name="rollOnFileSizeLimit">If <code>true</code>, a new file will be created when the file size limit is reached. Filenames
/// <param name="rollOnFileSizeLimit">If <code>true</code>, a new file will be created when the file size limit is reached. Filenames
/// will have a number appended in the format <code>_NNN</code>, with the first filename given no number.</param>
/// <param name="retainedFileCountLimit">The maximum number of log files that will be retained,
/// including the current log file. For unlimited retention, pass null. The default is 31.</param>
/// <param name="encoding">Character encoding used to write the text file. The default is UTF-8 without BOM.</param>
/// <param name="wrapper">Optionally enables wrapping the output stream in another stream, such as a GZipStream.</param>
/// <returns>Configuration object allowing method chaining.</returns>
/// <remarks>The file will be written using the UTF-8 character set.</remarks>
public static LoggerConfiguration File(
Expand All @@ -156,7 +157,8 @@ public static LoggerConfiguration File(
RollingInterval rollingInterval = RollingInterval.Infinite,
bool rollOnFileSizeLimit = false,
int? retainedFileCountLimit = DefaultRetainedFileCountLimit,
Encoding encoding = null)
Encoding encoding = null,
StreamWrapper wrapper = null)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The added parameter here will be a binary breaking change, even though calling code will recompile successfully.

There are a couple of options - for FileSink(), we should probably add a constructor overload to accept the new parameter, and have the old calls redirect to it.

At the level of these extension methods, it's possible to add an overload also, but the old methods have to be carefully un-defaulted and [Obsolete]d so that recompilation targets the new method (thereby giving us some future room to remove the old code). There are some examples of this in this file - see the first two methods in this class. Probably the best option for now, though in some future 5.0 we might start cleaning this up.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Regarding FileSink, I just wanted to be clear that this is what you mean:

// Original ctor redirects to new one
public FileSink(string path, ITextFormatter textFormatter, long? fileSizeLimitBytes, Encoding encoding = null, bool buffered = false)
	: this(path, textFormatter, fileSizeLimitBytes, encoding, buffered, null)
{
  // Intentionally emtpy
}

// New ctor
public FileSink(string path, ITextFormatter textFormatter, long? fileSizeLimitBytes, Encoding encoding = null, bool buffered = false,
	FileLifecycleHooks hooks = null)
{
  // Code here
}

After doing that, existing calls such as new FileSink(path, new JsonFormatter(), null) result in a compiler error due to The call is ambiguous. I can of course change all the calls in the library so they use the new ctor, but I guess this ctor ambiguity will break things for users?

The extension methods are rather fiddly to get right :nerd: but I'll see what I can do!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Usually in the past we've resolved the ambiguity by removing the = x default parameter values from the older version of the constructor. Alternatively, the new parameter can be positioned before the first defaulted one and not given a default value.

Copy link
Contributor Author

@cocowalla cocowalla Feb 14, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If adding a new defaulted parameter breaks binary compatibility, I would have though that removing defaults would have too?

2nd option would defo fix it though

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From the loader's perspective, only the number/kind/order of parameters is important, it doesn't see the default values at all. So adding a parameter, even if it has a default, changes the signature. Adding/removing/modifying default values doesn't break binary compatibility because they're not considered part of the signature. Fun and games :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, OK, interesting 😄

So let's say that a user calls this today:

new FileSink(path, textFormatter, 12345);

And then we remove the defaults from the remaining encoding and buffered parameters, and do this:

// Original ctor - defaults removed, and now redirects to new ctor
public FileSink(string path, ITextFormatter textFormatter, long? fileSizeLimitBytes, Encoding encoding, bool buffered)
	: this(path, textFormatter, fileSizeLimitBytes, encoding, buffered, null)
{
  // Intentionally emtpy
}

// New ctor
public FileSink(string path, ITextFormatter textFormatter, long? fileSizeLimitBytes, Encoding encoding = null, bool buffered = false,
	FileLifecycleHooks hooks = null)
{
  // Code here
}

In this scenario, will the old code continue to work?

I feel there must be some tooling that can check if binary compatibility is maintained - do you use anything like that?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, the old code will be binary compatible here, because the compiler inserts the default values directly into the call site.

So far I haven't found/used any recent tooling for this, but it could be out there. Mostly just painstaking work :-)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://www.fuget.org/ can be used to compare the API changes between 2 published versions of the same Nuget package. Quite helpful before remove the "pre-release" suffix :)

{
if (sinkConfiguration == null) throw new ArgumentNullException(nameof(sinkConfiguration));
if (path == null) throw new ArgumentNullException(nameof(path));
Expand All @@ -165,7 +167,7 @@ public static LoggerConfiguration File(
var formatter = new MessageTemplateTextFormatter(outputTemplate, formatProvider);
return File(sinkConfiguration, formatter, path, restrictedToMinimumLevel, fileSizeLimitBytes,
levelSwitch, buffered, shared, flushToDiskInterval,
rollingInterval, rollOnFileSizeLimit, retainedFileCountLimit, encoding);
rollingInterval, rollOnFileSizeLimit, retainedFileCountLimit, encoding, wrapper);
}

/// <summary>
Expand All @@ -174,7 +176,7 @@ public static LoggerConfiguration File(
/// <param name="sinkConfiguration">Logger sink configuration.</param>
/// <param name="formatter">A formatter, such as <see cref="JsonFormatter"/>, to convert the log events into
/// text for the file. If control of regular text formatting is required, use the other
/// overload of <see cref="File(LoggerSinkConfiguration, string, LogEventLevel, string, IFormatProvider, long?, LoggingLevelSwitch, bool, bool, TimeSpan?, RollingInterval, bool, int?, Encoding)"/>
/// overload of <see cref="File(LoggerSinkConfiguration, string, LogEventLevel, string, IFormatProvider, long?, LoggingLevelSwitch, bool, bool, TimeSpan?, RollingInterval, bool, int?, Encoding, StreamWrapper)"/>
/// and specify the outputTemplate parameter instead.
/// </param>
/// <param name="path">Path to the file.</param>
Expand All @@ -190,11 +192,12 @@ public static LoggerConfiguration File(
/// <param name="shared">Allow the log file to be shared by multiple processes. The default is false.</param>
/// <param name="flushToDiskInterval">If provided, a full disk flush will be performed periodically at the specified interval.</param>
/// <param name="rollingInterval">The interval at which logging will roll over to a new file.</param>
/// <param name="rollOnFileSizeLimit">If <code>true</code>, a new file will be created when the file size limit is reached. Filenames
/// <param name="rollOnFileSizeLimit">If <code>true</code>, a new file will be created when the file size limit is reached. Filenames
/// will have a number appended in the format <code>_NNN</code>, with the first filename given no number.</param>
/// <param name="retainedFileCountLimit">The maximum number of log files that will be retained,
/// including the current log file. For unlimited retention, pass null. The default is 31.</param>
/// <param name="encoding">Character encoding used to write the text file. The default is UTF-8 without BOM.</param>
/// <param name="wrapper">Optionally enables wrapping the output stream in another stream, such as a GZipStream.</param>
/// <returns>Configuration object allowing method chaining.</returns>
/// <remarks>The file will be written using the UTF-8 character set.</remarks>
public static LoggerConfiguration File(
Expand All @@ -210,10 +213,12 @@ public static LoggerConfiguration File(
RollingInterval rollingInterval = RollingInterval.Infinite,
bool rollOnFileSizeLimit = false,
int? retainedFileCountLimit = DefaultRetainedFileCountLimit,
Encoding encoding = null)
Encoding encoding = null,
StreamWrapper wrapper = null)
{
return ConfigureFile(sinkConfiguration.Sink, formatter, path, restrictedToMinimumLevel, fileSizeLimitBytes, levelSwitch,
buffered, false, shared, flushToDiskInterval, encoding, rollingInterval, rollOnFileSizeLimit, retainedFileCountLimit);
buffered, false, shared, flushToDiskInterval, encoding, rollingInterval, rollOnFileSizeLimit,
retainedFileCountLimit, wrapper);
}

/// <summary>
Expand Down Expand Up @@ -270,7 +275,7 @@ public static LoggerConfiguration File(
LoggingLevelSwitch levelSwitch = null)
{
return ConfigureFile(sinkConfiguration.Sink, formatter, path, restrictedToMinimumLevel, null, levelSwitch, false, true,
false, null, null, RollingInterval.Infinite, false, null);
false, null, null, RollingInterval.Infinite, false, null, null);
}

static LoggerConfiguration ConfigureFile(
Expand All @@ -287,7 +292,8 @@ static LoggerConfiguration ConfigureFile(
Encoding encoding,
RollingInterval rollingInterval,
bool rollOnFileSizeLimit,
int? retainedFileCountLimit)
int? retainedFileCountLimit,
StreamWrapper wrapper)
{
if (addSink == null) throw new ArgumentNullException(nameof(addSink));
if (formatter == null) throw new ArgumentNullException(nameof(formatter));
Expand All @@ -300,7 +306,7 @@ static LoggerConfiguration ConfigureFile(

if (rollOnFileSizeLimit || rollingInterval != RollingInterval.Infinite)
{
sink = new RollingFileSink(path, formatter, fileSizeLimitBytes, retainedFileCountLimit, encoding, buffered, shared, rollingInterval, rollOnFileSizeLimit);
sink = new RollingFileSink(path, formatter, fileSizeLimitBytes, retainedFileCountLimit, encoding, buffered, shared, rollingInterval, rollOnFileSizeLimit, wrapper);
}
else
{
Expand All @@ -309,11 +315,16 @@ static LoggerConfiguration ConfigureFile(
#pragma warning disable 618
if (shared)
{
if (wrapper != null)
{
SelfLog.WriteLine("Unable to use output stream wrapper - these are not supported for shared log files");
}

sink = new SharedFileSink(path, formatter, fileSizeLimitBytes);
}
else
{
sink = new FileSink(path, formatter, fileSizeLimitBytes, buffered: buffered);
sink = new FileSink(path, formatter, fileSizeLimitBytes, buffered: buffered, wrapper: wrapper);
}
#pragma warning restore 618
}
Expand Down
9 changes: 8 additions & 1 deletion src/Serilog.Sinks.File/Sinks/File/FileSink.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,12 @@ public sealed class FileSink : IFileSink, IDisposable
/// <param name="encoding">Character encoding used to write the text file. The default is UTF-8 without BOM.</param>
/// <param name="buffered">Indicates if flushing to the output file can be buffered or not. The default
/// is false.</param>
/// <param name="wrapper">Optionally enables wrapping the output stream in another stream, such as a GZipStream.</param>
/// <returns>Configuration object allowing method chaining.</returns>
/// <remarks>The file will be written using the UTF-8 character set.</remarks>
/// <exception cref="IOException"></exception>
public FileSink(string path, ITextFormatter textFormatter, long? fileSizeLimitBytes, Encoding encoding = null, bool buffered = false)
public FileSink(string path, ITextFormatter textFormatter, long? fileSizeLimitBytes, Encoding encoding = null, bool buffered = false,
StreamWrapper wrapper = null)
{
if (path == null) throw new ArgumentNullException(nameof(path));
if (textFormatter == null) throw new ArgumentNullException(nameof(textFormatter));
Expand All @@ -68,6 +70,11 @@ public FileSink(string path, ITextFormatter textFormatter, long? fileSizeLimitBy
outputStream = _countingStreamWrapper = new WriteCountingStream(_underlyingStream);
}

if (wrapper != null)
{
outputStream = wrapper.Wrap(outputStream);
}

_output = new StreamWriter(outputStream, encoding ?? new UTF8Encoding(encoderShouldEmitUTF8Identifier: false));
}

Expand Down
7 changes: 5 additions & 2 deletions src/Serilog.Sinks.File/Sinks/File/RollingFileSink.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ sealed class RollingFileSink : ILogEventSink, IFlushableFileSink, IDisposable
readonly bool _buffered;
readonly bool _shared;
readonly bool _rollOnFileSizeLimit;
readonly StreamWrapper _wrapper;

readonly object _syncRoot = new object();
bool _isDisposed;
Expand All @@ -50,7 +51,8 @@ public RollingFileSink(string path,
bool buffered,
bool shared,
RollingInterval rollingInterval,
bool rollOnFileSizeLimit)
bool rollOnFileSizeLimit,
StreamWrapper wrapper = null)
{
if (path == null) throw new ArgumentNullException(nameof(path));
if (fileSizeLimitBytes.HasValue && fileSizeLimitBytes < 0) throw new ArgumentException("Negative value provided; file size limit must be non-negative");
Expand All @@ -64,6 +66,7 @@ public RollingFileSink(string path,
_buffered = buffered;
_shared = shared;
_rollOnFileSizeLimit = rollOnFileSizeLimit;
_wrapper = wrapper;
}

public void Emit(LogEvent logEvent)
Expand Down Expand Up @@ -144,7 +147,7 @@ void OpenFile(DateTime now, int? minSequence = null)
{
_currentFile = _shared ?
(IFileSink)new SharedFileSink(path, _textFormatter, _fileSizeLimitBytes, _encoding) :
new FileSink(path, _textFormatter, _fileSizeLimitBytes, _encoding, _buffered);
new FileSink(path, _textFormatter, _fileSizeLimitBytes, _encoding, _buffered, _wrapper);
_currentFileSequence = sequence;
}
catch (IOException ex)
Expand Down
17 changes: 17 additions & 0 deletions src/Serilog.Sinks.File/StreamWrapper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using System.IO;

namespace Serilog
{
/// <summary>
/// Wraps the log file's output stream in another stream, such as a GZipStream
/// </summary>
public abstract class StreamWrapper
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the parameter lists to methods of are hot real-estate :-) we might avoid some future churn by naming this FileLifecycleHooks or something along those lines? It would need some thought put into the naming of Wrap(), since the convention would ideally be consistent with future methods we might add, like OnFileOpened(), OnFileClosed(). (Maybe we could pre-empt some of that? Wrap() could almost be the OnFileOpened() method, give or take a parameter or two?)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm with you on renaming StreamWrapper to avoid ctor hell as more hooks are added.

At first I wasn't too sure about OnFileOpened, since as-is the Wrap method is quite different to a 'standard' event handler. But after thinking a bit, you might have a point. Perhaps, as I guess you are alluding to, we could add a string path parameter, which would also have the bonus effect of making it more "general purpose". It would become:

public abstract Stream OnFileOpened(Stream underlyingStream, string path);

The idea would be that if you're not interested in changing the stream, you just return underlyingStream (this would be in the xml docs)

{
/// <summary>
/// Wraps <paramref name="sourceStream"/> in another stream, such as a GZipStream, then returns the wrapped stream
/// </summary>
/// <param name="sourceStream">The source log file stream</param>
/// <returns>The wrapped stream</returns>
public abstract Stream Wrap(Stream sourceStream);
}
}
43 changes: 40 additions & 3 deletions test/Serilog.Sinks.File.Tests/FileSinkTests.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
using System.IO;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Text;
using Xunit;
using Serilog.Formatting.Json;
using Serilog.Sinks.File.Tests.Support;
using Serilog.Tests.Support;
using System.Text;

#pragma warning disable 618

Expand Down Expand Up @@ -141,6 +143,42 @@ public void WhenLimitIsNotSpecifiedAndEncodingHasNoPreambleDataIsCorrectlyAppend
WriteTwoEventsAndCheckOutputFileLength(null, encoding);
}

[Fact]
public void WhenStreamWrapperIsSpecifiedOutputStreamIsWrapped()
{
var gzipWrapper = new GZipStreamWrapper();

using (var tmp = TempFolder.ForCaller())
{
var nonexistent = tmp.AllocateFilename("txt");
var evt = Some.LogEvent("Hello, world!");

using (var sink = new FileSink(nonexistent, new JsonFormatter(), null, wrapper: gzipWrapper))
{
sink.Emit(evt);
sink.Emit(evt);
}

// Ensure the data was written through the wrapping GZipStream, by decompressing and comparing against
// what we wrote
var lines = new List<string>();
using (var textStream = new MemoryStream())
{
using (var fs = System.IO.File.OpenRead(nonexistent))
using (var decompressStream = new GZipStream(fs, CompressionMode.Decompress))
{
decompressStream.CopyTo(textStream);
}

textStream.Position = 0;
lines = textStream.ReadAllLines();
}

Assert.Equal(2, lines.Count);
Assert.Contains("Hello, world!", lines[0]);
}
}

static void WriteTwoEventsAndCheckOutputFileLength(long? maxBytes, Encoding encoding)
{
using (var tmp = TempFolder.ForCaller())
Expand Down Expand Up @@ -170,4 +208,3 @@ static void WriteTwoEventsAndCheckOutputFileLength(long? maxBytes, Encoding enco
}
}
}

59 changes: 59 additions & 0 deletions test/Serilog.Sinks.File.Tests/RollingFileSinkTests.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Reflection;
using Xunit;
Expand Down Expand Up @@ -96,6 +97,64 @@ public void WhenSizeLimitIsBreachedNewFilesCreated()
}
}

[Fact]
public void WhenStreamWrapperSpecifiedIsUsedForRolledFiles()
{
var gzipWrapper = new GZipStreamWrapper();
var fileName = Some.String() + ".txt";

using (var temp = new TempFolder())
{
string[] files;
var logEvents = new[]
{
Some.InformationEvent(),
Some.InformationEvent(),
Some.InformationEvent()
};

using (var log = new LoggerConfiguration()
.WriteTo.File(Path.Combine(temp.Path, fileName), rollOnFileSizeLimit: true, fileSizeLimitBytes: 1, wrapper: gzipWrapper)
.CreateLogger())
{

foreach (var logEvent in logEvents)
{
log.Write(logEvent);
}

files = Directory.GetFiles(temp.Path)
.OrderBy(p => p, StringComparer.OrdinalIgnoreCase)
.ToArray();

Assert.Equal(3, files.Length);
Assert.True(files[0].EndsWith(fileName), files[0]);
Assert.True(files[1].EndsWith("_001.txt"), files[1]);
Assert.True(files[2].EndsWith("_002.txt"), files[2]);
}

// Ensure the data was written through the wrapping GZipStream, by decompressing and comparing against
// what we wrote
for (var i = 0; i < files.Length; i++)
{
using (var textStream = new MemoryStream())
{
using (var fs = System.IO.File.OpenRead(files[i]))
using (var decompressStream = new GZipStream(fs, CompressionMode.Decompress))
{
decompressStream.CopyTo(textStream);
}

textStream.Position = 0;
var lines = textStream.ReadAllLines();

Assert.Equal(1, lines.Count);
Assert.True(lines[0].EndsWith(logEvents[i].MessageTemplate.Text));
}
}
}
}

[Fact]
public void IfTheLogFolderDoesNotExistItWillBeCreated()
{
Expand Down
20 changes: 19 additions & 1 deletion test/Serilog.Sinks.File.Tests/Support/Extensions.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using Serilog.Events;
using System.Collections.Generic;
using System.IO;
using Serilog.Events;

namespace Serilog.Sinks.File.Tests.Support
{
Expand All @@ -8,5 +10,21 @@ public static object LiteralValue(this LogEventPropertyValue @this)
{
return ((ScalarValue)@this).Value;
}

public static List<string> ReadAllLines(this Stream @this)
{
var lines = new List<string>();

using (var reader = new StreamReader(@this))
{
string line;
while ((line = reader.ReadLine()) != null)
{
lines.Add(line);
}
}

return lines;
}
}
}
25 changes: 25 additions & 0 deletions test/Serilog.Sinks.File.Tests/Support/GZipStreamWrapper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using System.IO;
using System.IO.Compression;

namespace Serilog.Sinks.File.Tests.Support
{
/// <inheritdoc />
/// <summary>
/// Demonstrates the use of <seealso cref="T:Serilog.StreamWrapper" />, by compressing log output using GZip
/// </summary>
public class GZipStreamWrapper : StreamWrapper
{
readonly int _bufferSize;

public GZipStreamWrapper(int bufferSize = 1024 * 32)
{
_bufferSize = bufferSize;
}

public override Stream Wrap(Stream sourceStream)
{
var compressStream = new GZipStream(sourceStream, CompressionMode.Compress);
return new BufferedStream(compressStream, _bufferSize);
}
}
}