Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ bld/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/

# Rider cache/options directory
.idea

# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
Expand Down
21 changes: 21 additions & 0 deletions src/Serilog.Sinks.File/FileLifecycleHooks.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@

namespace Serilog
{
using System.IO;

/// <summary>
/// Enables hooking into log file lifecycle events
/// </summary>
public abstract class FileLifecycleHooks
{
/// <summary>
/// Wraps <paramref name="underlyingStream"/> in another stream, such as a GZipStream, then returns the wrapped stream
/// </summary>
/// <remarks>
/// Serilog is responsible for disposing of the wrapped stream
/// </remarks>
/// <param name="underlyingStream">The underlying log file stream</param>
/// <returns>The wrapped stream</returns>
public abstract Stream Wrap(Stream underlyingStream);
}
}
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="hooks">Optionally enables hooking into log file lifecycle events.</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,
FileLifecycleHooks hooks = null)
{
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, hooks);
}

/// <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, FileLifecycleHooks)"/>
/// 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="hooks">Optionally enables hooking into log file lifecycle events.</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,
FileLifecycleHooks hooks = 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, hooks);
}

/// <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,
FileLifecycleHooks hooks)
{
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, hooks);
}
else
{
Expand All @@ -309,11 +315,16 @@ static LoggerConfiguration ConfigureFile(
#pragma warning disable 618
if (shared)
{
if (hooks != 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, hooks: hooks);
}
#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="hooks">Optionally enables hooking into log file lifecycle events.</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,
FileLifecycleHooks hooks = 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 (hooks != null)
{
outputStream = hooks.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 FileLifecycleHooks _hooks;

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,
FileLifecycleHooks hooks = 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;
_hooks = hooks;
}

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, _hooks);
_currentFileSequence = sequence;
}
catch (IOException ex)
Expand Down
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 GZipHooks();

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, hooks: 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 GZipHooks();
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, hooks: 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;
}
}
}
Loading