Skip to content

Flush to disk option #20

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 3 commits into from
Oct 6, 2016
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
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
38 changes: 30 additions & 8 deletions src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ public static class FileLoggerConfigurationExtensions
/// <param name="buffered">Indicates if flushing to the output file can be buffered or not. The default
/// is false.</param>
/// <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>
/// <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 @@ -59,14 +60,15 @@ public static LoggerConfiguration File(
long? fileSizeLimitBytes = DefaultFileSizeLimitBytes,
LoggingLevelSwitch levelSwitch = null,
bool buffered = false,
bool shared = false)
bool shared = false,
TimeSpan? flushToDiskInterval = null)
{
if (sinkConfiguration == null) throw new ArgumentNullException(nameof(sinkConfiguration));
if (path == null) throw new ArgumentNullException(nameof(path));
if (outputTemplate == null) throw new ArgumentNullException(nameof(outputTemplate));

var formatter = new MessageTemplateTextFormatter(outputTemplate, formatProvider);
return File(sinkConfiguration, formatter, path, restrictedToMinimumLevel, fileSizeLimitBytes, levelSwitch, buffered: buffered, shared: shared);
return File(sinkConfiguration, formatter, path, restrictedToMinimumLevel, fileSizeLimitBytes, levelSwitch, buffered: buffered, shared: shared, flushToDiskInterval: flushToDiskInterval);
}

/// <summary>
Expand All @@ -75,7 +77,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)"/>
/// overload of <see cref="File(LoggerSinkConfiguration, string, LogEventLevel, string, IFormatProvider, long?, LoggingLevelSwitch, bool, bool, TimeSpan?)"/>
/// and specify the outputTemplate parameter instead.
/// </param>
/// <param name="path">Path to the file.</param>
Expand All @@ -89,6 +91,7 @@ public static LoggerConfiguration File(
/// <param name="buffered">Indicates if flushing to the output file can be buffered or not. The default
/// is false.</param>
/// <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>
/// <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 @@ -99,9 +102,10 @@ public static LoggerConfiguration File(
long? fileSizeLimitBytes = DefaultFileSizeLimitBytes,
LoggingLevelSwitch levelSwitch = null,
bool buffered = false,
bool shared = false)
bool shared = false,
TimeSpan? flushToDiskInterval = null)
{
return ConfigureFile(sinkConfiguration.Sink, formatter, path, restrictedToMinimumLevel, fileSizeLimitBytes, levelSwitch, buffered: buffered, shared: shared);
return ConfigureFile(sinkConfiguration.Sink, formatter, path, restrictedToMinimumLevel, fileSizeLimitBytes, levelSwitch, buffered: buffered, shared: shared, flushToDiskInterval: flushToDiskInterval);
}

/// <summary>
Expand Down Expand Up @@ -169,7 +173,8 @@ static LoggerConfiguration ConfigureFile(
LoggingLevelSwitch levelSwitch = null,
bool buffered = false,
bool propagateExceptions = false,
bool shared = false)
bool shared = false,
TimeSpan? flushToDiskInterval = null)
{
if (addSink == null) throw new ArgumentNullException(nameof(addSink));
if (formatter == null) throw new ArgumentNullException(nameof(formatter));
Expand All @@ -192,12 +197,28 @@ static LoggerConfiguration ConfigureFile(
#if ATOMIC_APPEND
if (shared)
{
sink = new SharedFileSink(path, formatter, fileSizeLimitBytes);
var sfs = new SharedFileSink(path, formatter, fileSizeLimitBytes);
if (flushToDiskInterval.HasValue)
{
sink = new PeriodicFlushToDiskSink<SharedFileSink>(sfs, flushToDiskInterval.Value);
}
else
{
sink = sfs;
}
}
else
{
#endif
sink = new FileSink(path, formatter, fileSizeLimitBytes, buffered: buffered);
var fs = new FileSink(path, formatter, fileSizeLimitBytes, buffered: buffered);
if (flushToDiskInterval.HasValue)
{
sink = new PeriodicFlushToDiskSink<FileSink>(fs, flushToDiskInterval.Value);
}
else
{
sink = fs;
}
#if ATOMIC_APPEND
}
#endif
Expand All @@ -212,6 +233,7 @@ static LoggerConfiguration ConfigureFile(
return addSink(new NullSink(), LevelAlias.Maximum, null);
}


return addSink(sink, restrictedToMinimumLevel, levelSwitch);
}
}
Expand Down
24 changes: 16 additions & 8 deletions src/Serilog.Sinks.File/Sinks/File/FileSink.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,10 @@ namespace Serilog.Sinks.File
/// <summary>
/// Write log events to a disk file.
/// </summary>
public sealed class FileSink : ILogEventSink, IDisposable
public sealed class FileSink : ILogEventSink, IFlushableFileSink, IDisposable
{
readonly TextWriter _output;
readonly FileStream _underlyingStream;
readonly ITextFormatter _textFormatter;
readonly long? _fileSizeLimitBytes;
readonly bool _buffered;
Expand Down Expand Up @@ -61,13 +62,13 @@ public FileSink(string path, ITextFormatter textFormatter, long? fileSizeLimitBy
Directory.CreateDirectory(directory);
}

Stream file = System.IO.File.Open(path, FileMode.Append, FileAccess.Write, FileShare.Read);
Stream outputStream = _underlyingStream = System.IO.File.Open(path, FileMode.Append, FileAccess.Write, FileShare.Read);
if (_fileSizeLimitBytes != null)
{
file = _countingStreamWrapper = new WriteCountingStream(file);
outputStream = _countingStreamWrapper = new WriteCountingStream(_underlyingStream);
}

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

/// <summary>
Expand All @@ -91,10 +92,17 @@ public void Emit(LogEvent logEvent)
}
}

/// <summary>
/// Performs application-defined tasks associated with freeing, releasing, or
/// resetting unmanaged resources.
/// </summary>
/// <inheritdoc />
public void Dispose() => _output.Dispose();

/// <inheritdoc />
public void FlushToDisk()
{
lock (_syncRoot)
{
_output.Flush();
_underlyingStream.Flush(true);
}
}
}
}
13 changes: 13 additions & 0 deletions src/Serilog.Sinks.File/Sinks/File/IFlushableFileSink.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
namespace Serilog.Sinks.File
{
/// <summary>
/// Supported by (file-based) sinks that can be explicitly flushed.
/// </summary>
public interface IFlushableFileSink
{
/// <summary>
/// Flush buffered contents to disk.
/// </summary>
void FlushToDisk();
}
}
66 changes: 66 additions & 0 deletions src/Serilog.Sinks.File/Sinks/File/PeriodicFlushToDiskSink`1.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
using System;
using System.Threading;
using Serilog.Core;
using Serilog.Debugging;
using Serilog.Events;

namespace Serilog.Sinks.File
{
/// <summary>
/// A sink wrapper that periodically flushes the wrapped sink to disk.
/// </summary>
/// <typeparam name="TSink">The type of the wrapped sink.</typeparam>
public class PeriodicFlushToDiskSink<TSink> : ILogEventSink, IDisposable
Copy link

@pakrym pakrym Oct 5, 2016

Choose a reason for hiding this comment

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

Why generic? To avoid interface method invocation?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, I'm not so keen on this upon second thoughts. It is to make clear that the passed-in ILogEventSink should also be IFlushableFileSink (no union types in C#) but the code would be a bit nicer to read if non-generic and tested at runtime. Going to change this.

where TSink : ILogEventSink, IFlushableFileSink
{
readonly TSink _sink;
readonly Timer _timer;
int _flushRequired;

/// <summary>
/// Construct a <see cref="PeriodicFlushToDiskSink{TSink}"/> that wraps
/// <paramref name="sink"/> and flushes it at the specified <paramref name="flushInterval"/>.
/// </summary>
/// <param name="sink">The sink to wrap.</param>
/// <param name="flushInterval">The interval at which to flush the underlying sink.</param>
/// <exception cref="ArgumentNullException"></exception>
public PeriodicFlushToDiskSink(TSink sink, TimeSpan flushInterval)
{
if (sink == null) throw new ArgumentNullException(nameof(sink));

_sink = sink;
_timer = new Timer(_ => FlushToDisk(), null, flushInterval, flushInterval);
}

/// <inheritdoc />
public void Emit(LogEvent logEvent)
{
_sink.Emit(logEvent);
Interlocked.Exchange(ref _flushRequired, 1);
}

/// <inheritdoc />
public void Dispose()
{
_timer.Dispose();
(_sink as IDisposable)?.Dispose();
}

void FlushToDisk()
{
try
{
if (Interlocked.CompareExchange(ref _flushRequired, 0, 1) == 1)
{
// May throw ObjectDisposedException, since we're not trying to synchronize
// anything here in the wrapper.
_sink.FlushToDisk();
}
}
catch (Exception ex)
{
SelfLog.WriteLine("Could not flush the underlying sink to disk: {0}", ex);
}
}
}
}
53 changes: 31 additions & 22 deletions src/Serilog.Sinks.File/Sinks/File/SharedFileSink.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,14 @@ namespace Serilog.Sinks.File
/// <summary>
/// Write log events to a disk file.
/// </summary>
public sealed class SharedFileSink : ILogEventSink, IDisposable
public sealed class SharedFileSink : ILogEventSink, IFlushableFileSink, IDisposable
{
readonly MemoryStream _writeBuffer;
readonly string _path;
readonly TextWriter _output;
readonly ITextFormatter _textFormatter;
readonly long? _fileSizeLimitBytes;
readonly object _syncRoot = new object();
readonly FileInfo _fileInfo;

// The stream is reopened with a larger buffer if atomic writes beyond the current buffer size are needed.
FileStream _fileOutput;
Expand All @@ -53,11 +52,13 @@ public sealed class SharedFileSink : ILogEventSink, IDisposable
/// <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 SharedFileSink(string path, ITextFormatter textFormatter, long? fileSizeLimitBytes, Encoding encoding = null)
public SharedFileSink(string path, ITextFormatter textFormatter, long? fileSizeLimitBytes,
Encoding encoding = null)
{
if (path == null) throw new ArgumentNullException(nameof(path));
if (textFormatter == null) throw new ArgumentNullException(nameof(textFormatter));
if (fileSizeLimitBytes.HasValue && fileSizeLimitBytes < 0) throw new ArgumentException("Negative value provided; file size limit must be non-negative");
if (fileSizeLimitBytes.HasValue && fileSizeLimitBytes < 0)
throw new ArgumentException("Negative value provided; file size limit must be non-negative");

_path = path;
_textFormatter = textFormatter;
Expand All @@ -72,20 +73,16 @@ public SharedFileSink(string path, ITextFormatter textFormatter, long? fileSizeL
// FileSystemRights.AppendData sets the Win32 FILE_APPEND_DATA flag. On Linux this is O_APPEND, but that API is not yet
// exposed by .NET Core.
_fileOutput = new FileStream(
path,
path,
FileMode.Append,
FileSystemRights.AppendData,
FileShare.ReadWrite,
_fileStreamBufferLength,
FileOptions.None);

if (_fileSizeLimitBytes != null)
{
_fileInfo = new FileInfo(path);
}

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

/// <summary>
Expand All @@ -96,20 +93,14 @@ public void Emit(LogEvent logEvent)
{
if (logEvent == null) throw new ArgumentNullException(nameof(logEvent));

if (_fileSizeLimitBytes != null)
{
if (_fileInfo.Length >= _fileSizeLimitBytes.Value)
return;
}

lock (_syncRoot)
{
try
{
_textFormatter.Format(logEvent, _output);
_output.Flush();
var bytes = _writeBuffer.GetBuffer();
var length = (int)_writeBuffer.Length;
var length = (int) _writeBuffer.Length;
if (length > _fileStreamBufferLength)
{
var oldOutput = _fileOutput;
Expand All @@ -126,6 +117,16 @@ public void Emit(LogEvent logEvent)
oldOutput.Dispose();
}

if (_fileSizeLimitBytes != null)
{
try
{
if (_fileOutput.Length >= _fileSizeLimitBytes.Value)
return;
}
catch (FileNotFoundException) { } // Cheaper and more reliable than checking existence
}

_fileOutput.Write(bytes, 0, length);
_fileOutput.Flush();
}
Expand All @@ -143,11 +144,19 @@ public void Emit(LogEvent logEvent)
}
}

/// <summary>
/// Performs application-defined tasks associated with freeing, releasing, or
/// resetting unmanaged resources.
/// </summary>

/// <inheritdoc />
public void Dispose() => _fileOutput.Dispose();

/// <inheritdoc />
public void FlushToDisk()
{
lock (_syncRoot)
{
_output.Flush();
_fileOutput.Flush(true);
}
}
}
}

Expand Down
1 change: 1 addition & 0 deletions src/Serilog.Sinks.File/Sinks/File/WriteCountingStream.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ public override void Write(byte[] buffer, int offset, int count)
public override bool CanWrite => true;
public override long Length => _stream.Length;


public override long Position
{
get { return _stream.Position; }
Expand Down
7 changes: 4 additions & 3 deletions src/Serilog.Sinks.File/project.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"version": "3.0.2-*",
"version": "3.1.0-*",
"description": "Write Serilog events to a text file in plain or JSON format.",
"authors": [ "Serilog Contributors" ],
"packOptions": {
Expand All @@ -9,7 +9,7 @@
"iconUrl": "http://serilog.net/images/serilog-sink-nuget.png"
},
"dependencies": {
"Serilog": "2.2.0"
"Serilog": "2.3.0"
},
"buildOptions": {
"keyFile": "../../assets/Serilog.snk",
Expand All @@ -24,7 +24,8 @@
"System.IO": "4.1.0",
"System.IO.FileSystem": "4.0.1",
"System.IO.FileSystem.Primitives": "4.0.1",
"System.Text.Encoding.Extensions": "4.0.11"
"System.Text.Encoding.Extensions": "4.0.11",
"System.Threading.Timer": "4.0.1"
}
}
}
Expand Down
Loading