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"] + } } } }