diff --git a/src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs b/src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs
index 8cffba0..571f414 100644
--- a/src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs
+++ b/src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs
@@ -48,6 +48,7 @@ public static class FileLoggerConfigurationExtensions
/// Indicates if flushing to the output file can be buffered or not. The default
/// is false.
/// Allow the log file to be shared by multiple processes. The default is false.
+ /// If provided, a full disk flush will be performed periodically at the specified interval.
/// Configuration object allowing method chaining.
/// The file will be written using the UTF-8 character set.
public static LoggerConfiguration File(
@@ -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);
}
///
@@ -75,7 +77,7 @@ public static LoggerConfiguration File(
/// Logger sink configuration.
/// A formatter, such as , to convert the log events into
/// text for the file. If control of regular text formatting is required, use the other
- /// overload of
+ /// overload of
/// and specify the outputTemplate parameter instead.
///
/// Path to the file.
@@ -89,6 +91,7 @@ public static LoggerConfiguration File(
/// Indicates if flushing to the output file can be buffered or not. The default
/// is false.
/// Allow the log file to be shared by multiple processes. The default is false.
+ /// If provided, a full disk flush will be performed periodically at the specified interval.
/// Configuration object allowing method chaining.
/// The file will be written using the UTF-8 character set.
public static LoggerConfiguration File(
@@ -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);
}
///
@@ -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));
@@ -212,6 +217,11 @@ static LoggerConfiguration ConfigureFile(
return addSink(new NullSink(), LevelAlias.Maximum, null);
}
+ if (flushToDiskInterval.HasValue)
+ {
+ sink = new PeriodicFlushToDiskSink(sink, flushToDiskInterval.Value);
+ }
+
return addSink(sink, restrictedToMinimumLevel, levelSwitch);
}
}
diff --git a/src/Serilog.Sinks.File/Sinks/File/FileSink.cs b/src/Serilog.Sinks.File/Sinks/File/FileSink.cs
index 3af8386..443519a 100644
--- a/src/Serilog.Sinks.File/Sinks/File/FileSink.cs
+++ b/src/Serilog.Sinks.File/Sinks/File/FileSink.cs
@@ -24,9 +24,10 @@ namespace Serilog.Sinks.File
///
/// Write log events to a disk file.
///
- 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;
@@ -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));
}
///
@@ -91,10 +92,23 @@ public void Emit(LogEvent logEvent)
}
}
- ///
- /// Performs application-defined tasks associated with freeing, releasing, or
- /// resetting unmanaged resources.
- ///
- public void Dispose() => _output.Dispose();
+ ///
+ public void Dispose()
+ {
+ lock (_syncRoot)
+ {
+ _output.Dispose();
+ }
+ }
+
+ ///
+ public void FlushToDisk()
+ {
+ lock (_syncRoot)
+ {
+ _output.Flush();
+ _underlyingStream.Flush(true);
+ }
+ }
}
}
diff --git a/src/Serilog.Sinks.File/Sinks/File/IFlushableFileSink.cs b/src/Serilog.Sinks.File/Sinks/File/IFlushableFileSink.cs
new file mode 100644
index 0000000..c74727e
--- /dev/null
+++ b/src/Serilog.Sinks.File/Sinks/File/IFlushableFileSink.cs
@@ -0,0 +1,13 @@
+namespace Serilog.Sinks.File
+{
+ ///
+ /// Supported by (file-based) sinks that can be explicitly flushed.
+ ///
+ public interface IFlushableFileSink
+ {
+ ///
+ /// Flush buffered contents to disk.
+ ///
+ void FlushToDisk();
+ }
+}
diff --git a/src/Serilog.Sinks.File/Sinks/File/PeriodicFlushToDiskSink.cs b/src/Serilog.Sinks.File/Sinks/File/PeriodicFlushToDiskSink.cs
new file mode 100644
index 0000000..cafb72e
--- /dev/null
+++ b/src/Serilog.Sinks.File/Sinks/File/PeriodicFlushToDiskSink.cs
@@ -0,0 +1,74 @@
+using System;
+using System.Threading;
+using Serilog.Core;
+using Serilog.Debugging;
+using Serilog.Events;
+
+namespace Serilog.Sinks.File
+{
+ ///
+ /// A sink wrapper that periodically flushes the wrapped sink to disk.
+ ///
+ public class PeriodicFlushToDiskSink : ILogEventSink, IDisposable
+ {
+ readonly ILogEventSink _sink;
+ readonly Timer _timer;
+ int _flushRequired;
+
+ ///
+ /// Construct a that wraps
+ /// and flushes it at the specified .
+ ///
+ /// The sink to wrap.
+ /// The interval at which to flush the underlying sink.
+ ///
+ public PeriodicFlushToDiskSink(ILogEventSink sink, TimeSpan flushInterval)
+ {
+ if (sink == null) throw new ArgumentNullException(nameof(sink));
+
+ _sink = sink;
+
+ var flushable = sink as IFlushableFileSink;
+ if (flushable != null)
+ {
+ _timer = new Timer(_ => FlushToDisk(flushable), null, flushInterval, flushInterval);
+ }
+ else
+ {
+ _timer = new Timer(_ => { }, null, Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan);
+ SelfLog.WriteLine("{0} configured to flush {1}, but {2} not implemented", typeof(PeriodicFlushToDiskSink), sink, nameof(IFlushableFileSink));
+ }
+ }
+
+ ///
+ public void Emit(LogEvent logEvent)
+ {
+ _sink.Emit(logEvent);
+ Interlocked.Exchange(ref _flushRequired, 1);
+ }
+
+ ///
+ public void Dispose()
+ {
+ _timer.Dispose();
+ (_sink as IDisposable)?.Dispose();
+ }
+
+ void FlushToDisk(IFlushableFileSink flushable)
+ {
+ try
+ {
+ if (Interlocked.CompareExchange(ref _flushRequired, 0, 1) == 1)
+ {
+ // May throw ObjectDisposedException, since we're not trying to synchronize
+ // anything here in the wrapper.
+ flushable.FlushToDisk();
+ }
+ }
+ catch (Exception ex)
+ {
+ SelfLog.WriteLine("{0} could not flush the underlying sink to disk: {1}", typeof(PeriodicFlushToDiskSink), ex);
+ }
+ }
+ }
+}
diff --git a/src/Serilog.Sinks.File/Sinks/File/SharedFileSink.cs b/src/Serilog.Sinks.File/Sinks/File/SharedFileSink.cs
index a8af612..bbb5142 100644
--- a/src/Serilog.Sinks.File/Sinks/File/SharedFileSink.cs
+++ b/src/Serilog.Sinks.File/Sinks/File/SharedFileSink.cs
@@ -27,7 +27,7 @@ namespace Serilog.Sinks.File
///
/// Write log events to a disk file.
///
- public sealed class SharedFileSink : ILogEventSink, IDisposable
+ public sealed class SharedFileSink : ILogEventSink, IFlushableFileSink, IDisposable
{
readonly MemoryStream _writeBuffer;
readonly string _path;
@@ -35,7 +35,6 @@ public sealed class SharedFileSink : ILogEventSink, IDisposable
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;
@@ -53,11 +52,13 @@ public sealed class SharedFileSink : ILogEventSink, IDisposable
/// Configuration object allowing method chaining.
/// The file will be written using the UTF-8 character set.
///
- 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;
@@ -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));
}
///
@@ -96,12 +93,6 @@ 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
@@ -109,7 +100,7 @@ public void Emit(LogEvent logEvent)
_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;
@@ -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();
}
@@ -143,11 +144,25 @@ public void Emit(LogEvent logEvent)
}
}
- ///
- /// Performs application-defined tasks associated with freeing, releasing, or
- /// resetting unmanaged resources.
- ///
- public void Dispose() => _fileOutput.Dispose();
+
+ ///
+ public void Dispose()
+ {
+ lock (_syncRoot)
+ {
+ _fileOutput.Dispose();
+ }
+ }
+
+ ///
+ public void FlushToDisk()
+ {
+ lock (_syncRoot)
+ {
+ _output.Flush();
+ _fileOutput.Flush(true);
+ }
+ }
}
}
diff --git a/src/Serilog.Sinks.File/Sinks/File/WriteCountingStream.cs b/src/Serilog.Sinks.File/Sinks/File/WriteCountingStream.cs
index ae44fa4..f0ec6a9 100644
--- a/src/Serilog.Sinks.File/Sinks/File/WriteCountingStream.cs
+++ b/src/Serilog.Sinks.File/Sinks/File/WriteCountingStream.cs
@@ -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; }
diff --git a/src/Serilog.Sinks.File/project.json b/src/Serilog.Sinks.File/project.json
index 95f9137..82265a8 100644
--- a/src/Serilog.Sinks.File/project.json
+++ b/src/Serilog.Sinks.File/project.json
@@ -1,5 +1,5 @@
{
- "version": "3.0.1-*",
+ "version": "3.1.0-*",
"description": "Write Serilog events to a text file in plain or JSON format.",
"authors": [ "Serilog Contributors" ],
"packOptions": {
@@ -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",
@@ -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"
}
}
}
diff --git a/test/Serilog.Sinks.File.Tests/FileLoggerConfigurationExtensionsTests.cs b/test/Serilog.Sinks.File.Tests/FileLoggerConfigurationExtensionsTests.cs
index 8dbbdbc..78c35b3 100644
--- a/test/Serilog.Sinks.File.Tests/FileLoggerConfigurationExtensionsTests.cs
+++ b/test/Serilog.Sinks.File.Tests/FileLoggerConfigurationExtensionsTests.cs
@@ -1,4 +1,5 @@
using System;
+using System.Threading;
using Serilog;
using Serilog.Sinks.File.Tests.Support;
using Serilog.Tests.Support;
@@ -51,5 +52,33 @@ public void WhenAuditingLoggingExceptionsPropagate()
Assert.IsType(ex.GetBaseException());
}
}
+
+ [Fact]
+ public void WhenFlushingToDiskReportedFileSinkCanBeCreatedAndDisposed()
+ {
+ using (var tmp = TempFolder.ForCaller())
+ using (var log = new LoggerConfiguration()
+ .WriteTo.File(tmp.AllocateFilename(), flushToDiskInterval: TimeSpan.FromMilliseconds(500))
+ .CreateLogger())
+ {
+ log.Information("Hello");
+ Thread.Sleep(TimeSpan.FromSeconds(1));
+ }
+ }
+
+#if ATOMIC_APPEND
+ [Fact]
+ public void WhenFlushingToDiskReportedSharedFileSinkCanBeCreatedAndDisposed()
+ {
+ using (var tmp = TempFolder.ForCaller())
+ using (var log = new LoggerConfiguration()
+ .WriteTo.File(tmp.AllocateFilename(), shared: true, flushToDiskInterval: TimeSpan.FromMilliseconds(500))
+ .CreateLogger())
+ {
+ log.Information("Hello");
+ Thread.Sleep(TimeSpan.FromSeconds(1));
+ }
+ }
+#endif
}
}
diff --git a/test/Serilog.Sinks.File.Tests/Serilog.Sinks.File.Tests.xproj b/test/Serilog.Sinks.File.Tests/Serilog.Sinks.File.Tests.xproj
index 9e7949e..3234f8a 100644
--- a/test/Serilog.Sinks.File.Tests/Serilog.Sinks.File.Tests.xproj
+++ b/test/Serilog.Sinks.File.Tests/Serilog.Sinks.File.Tests.xproj
@@ -14,5 +14,8 @@
2.0
+
+
+
\ No newline at end of file
diff --git a/test/Serilog.Sinks.File.Tests/SharedFileSinkTests.cs b/test/Serilog.Sinks.File.Tests/SharedFileSinkTests.cs
index ec325fc..d0bd751 100644
--- a/test/Serilog.Sinks.File.Tests/SharedFileSinkTests.cs
+++ b/test/Serilog.Sinks.File.Tests/SharedFileSinkTests.cs
@@ -1,11 +1,9 @@
#if ATOMIC_APPEND
-using System;
using System.IO;
using Xunit;
using Serilog.Formatting.Json;
using Serilog.Sinks.File.Tests.Support;
-using Serilog.Sinks.File;
using Serilog.Tests.Support;
namespace Serilog.Sinks.File.Tests
diff --git a/test/Serilog.Sinks.File.Tests/project.json b/test/Serilog.Sinks.File.Tests/project.json
index 8b7feb8..1fa084d 100644
--- a/test/Serilog.Sinks.File.Tests/project.json
+++ b/test/Serilog.Sinks.File.Tests/project.json
@@ -6,7 +6,6 @@
"dotnet-test-xunit": "1.0.0-rc2-build10025"
},
"frameworks": {
- "net4.5.2": {},
"netcoreapp1.0": {
"dependencies": {
"Microsoft.NETCore.App": {
@@ -18,6 +17,11 @@
"dnxcore50",
"portable-net45+win8"
]
+ },
+ "net4.5.2": {
+ "buildOptions": {
+ "define": ["ATOMIC_APPEND"]
+ }
}
}
}