diff --git a/README.md b/README.md index b8ab721..5aa395f 100644 --- a/README.md +++ b/README.md @@ -36,4 +36,10 @@ To emit JSON, rather than plain text, a formatter can be specified: To configure an alternative formatter in XML ``, specify the formatter's assembly-qualified type name as the setting `value`. +### Performance + +By default, the file sink will flush each event written through it to disk. To improve write performance, specifying `buffered: true` will permit the underlying stream to buffer writes. + +The [Serilog.Sinks.Async](https://github.com/serilog/serilog-sinks-async) package can be used to wrap the file sink and perform all disk accss on a background worker thread. + _Copyright © 2016 Serilog Contributors - Provided under the [Apache License, Version 2.0](http://apache.org/licenses/LICENSE-2.0.html)._ diff --git a/example/Sample/Program.cs b/example/Sample/Program.cs new file mode 100644 index 0000000..99a158b --- /dev/null +++ b/example/Sample/Program.cs @@ -0,0 +1,38 @@ +using System; +using System.IO; +using Serilog; +using Serilog.Debugging; + +namespace Sample +{ + public class Program + { + public static void Main(string[] args) + { + SelfLog.Enable(Console.Out); + + Log.Logger = new LoggerConfiguration() + .WriteTo.File("log.txt") + .CreateLogger(); + + var sw = System.Diagnostics.Stopwatch.StartNew(); + + for (var i = 0; i < 1000000; ++i) + { + Log.Information("Hello, file logger!"); + } + + Log.CloseAndFlush(); + + sw.Stop(); + + Console.WriteLine($"Elapsed: {sw.ElapsedMilliseconds} ms"); + Console.WriteLine($"Size: {new FileInfo("log.txt").Length}"); + + Console.WriteLine("Press any key to delete the temporary log file..."); + Console.ReadKey(true); + + File.Delete("log.txt"); + } + } +} diff --git a/example/Sample/Sample.xproj b/example/Sample/Sample.xproj new file mode 100644 index 0000000..000aa06 --- /dev/null +++ b/example/Sample/Sample.xproj @@ -0,0 +1,21 @@ + + + + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + + a34235a2-a717-4a1c-bf5c-f4a9e06e1260 + Sample + .\obj + .\bin\ + v4.5.2 + + + + 2.0 + + + diff --git a/example/Sample/project.json b/example/Sample/project.json new file mode 100644 index 0000000..525d510 --- /dev/null +++ b/example/Sample/project.json @@ -0,0 +1,23 @@ +{ + "buildOptions": { + "emitEntryPoint": true + }, + + "dependencies": { + "Serilog.Sinks.File": { "target": "project" } + }, + + "frameworks": { + "netcoreapp1.0": { + "imports": "dnxcore50", + "dependencies": { + "Microsoft.NETCore.App": { + "type": "platform", + "version": "1.0.0" + } + } + }, + "net4.5": {} + }, + "runtimes": { "win10-x64": {} } +} diff --git a/serilog-sinks-file.sln b/serilog-sinks-file.sln index 8ab618f..bf1cf9a 100644 --- a/serilog-sinks-file.sln +++ b/serilog-sinks-file.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 14 -VisualStudioVersion = 14.0.25123.0 +VisualStudioVersion = 14.0.25420.1 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{037440DE-440B-4129-9F7A-09B42D00397E}" EndProject @@ -21,6 +21,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{7B927378-9 EndProject Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Serilog.Sinks.File.Tests", "test\Serilog.Sinks.File.Tests\Serilog.Sinks.File.Tests.xproj", "{3C2D8E01-5580-426A-BDD9-EC59CD98E618}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "example", "example", "{196B1544-C617-4D7C-96D1-628713BDD52A}" +EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Sample", "example\Sample\Sample.xproj", "{A34235A2-A717-4A1C-BF5C-F4A9E06E1260}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -35,6 +39,10 @@ Global {3C2D8E01-5580-426A-BDD9-EC59CD98E618}.Debug|Any CPU.Build.0 = Debug|Any CPU {3C2D8E01-5580-426A-BDD9-EC59CD98E618}.Release|Any CPU.ActiveCfg = Release|Any CPU {3C2D8E01-5580-426A-BDD9-EC59CD98E618}.Release|Any CPU.Build.0 = Release|Any CPU + {A34235A2-A717-4A1C-BF5C-F4A9E06E1260}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A34235A2-A717-4A1C-BF5C-F4A9E06E1260}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A34235A2-A717-4A1C-BF5C-F4A9E06E1260}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A34235A2-A717-4A1C-BF5C-F4A9E06E1260}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -42,5 +50,6 @@ Global GlobalSection(NestedProjects) = preSolution {57E0ED0E-0F45-48AB-A73D-6A92B7C32095} = {037440DE-440B-4129-9F7A-09B42D00397E} {3C2D8E01-5580-426A-BDD9-EC59CD98E618} = {7B927378-9F16-4F6F-B3F6-156395136646} + {A34235A2-A717-4A1C-BF5C-F4A9E06E1260} = {196B1544-C617-4D7C-96D1-628713BDD52A} EndGlobalSection EndGlobal diff --git a/src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs b/src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs index d31acb4..0942cb3 100644 --- a/src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs +++ b/src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs @@ -42,10 +42,12 @@ public static class FileLoggerConfigurationExtensions /// Supplies culture-specific formatting information, or null. /// A message template describing the format used to write to the sink. /// the default is "{Timestamp} [{Level}] {Message}{NewLine}{Exception}". - /// The maximum size, in bytes, to which a log file will be allowed to grow. - /// For unrestricted growth, pass null. The default is 1 GB. + /// The approximate maximum size, in bytes, to which a log file will be allowed to grow. + /// For unrestricted growth, pass null. The default is 1 GB. To avoid writing partial events, the last event within the limit + /// will be written in full even if it exceeds the limit. /// 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. /// Configuration object allowing method chaining. /// The file will be written using the UTF-8 character set. public static LoggerConfiguration File( @@ -56,14 +58,15 @@ public static LoggerConfiguration File( IFormatProvider formatProvider = null, long? fileSizeLimitBytes = DefaultFileSizeLimitBytes, LoggingLevelSwitch levelSwitch = null, - bool buffered = false) + bool buffered = false, + bool shared = false) { 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); + return File(sinkConfiguration, formatter, path, restrictedToMinimumLevel, fileSizeLimitBytes, levelSwitch, buffered: buffered, shared: shared); } /// @@ -72,7 +75,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. @@ -80,10 +83,12 @@ public static LoggerConfiguration File( /// events passed through the sink. Ignored when is specified. /// A switch allowing the pass-through minimum level /// to be changed at runtime. - /// The maximum size, in bytes, to which a log file will be allowed to grow. - /// For unrestricted growth, pass null. The default is 1 GB. + /// The approximate maximum size, in bytes, to which a log file will be allowed to grow. + /// For unrestricted growth, pass null. The default is 1 GB. To avoid writing partial events, the last event within the limit + /// will be written in full even if it exceeds the limit. /// 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. /// Configuration object allowing method chaining. /// The file will be written using the UTF-8 character set. public static LoggerConfiguration File( @@ -93,28 +98,121 @@ public static LoggerConfiguration File( LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum, long? fileSizeLimitBytes = DefaultFileSizeLimitBytes, LoggingLevelSwitch levelSwitch = null, - bool buffered = false) + bool buffered = false, + bool shared = false) + { + return ConfigureFile(sinkConfiguration.Sink, formatter, path, restrictedToMinimumLevel, fileSizeLimitBytes, levelSwitch, buffered: buffered, shared: shared); + } + + /// + /// Write log events to the specified file. + /// + /// Logger sink configuration. + /// Path to the file. + /// The minimum level for + /// events passed through the sink. Ignored when is specified. + /// A switch allowing the pass-through minimum level + /// to be changed at runtime. + /// Supplies culture-specific formatting information, or null. + /// A message template describing the format used to write to the sink. + /// the default is "{Timestamp} [{Level}] {Message}{NewLine}{Exception}". + /// Configuration object allowing method chaining. + /// The file will be written using the UTF-8 character set. + public static LoggerConfiguration File( + this LoggerAuditSinkConfiguration sinkConfiguration, + string path, + LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum, + string outputTemplate = DefaultOutputTemplate, + IFormatProvider formatProvider = null, + LoggingLevelSwitch levelSwitch = 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, levelSwitch); + } + + /// + /// Write log events to the specified 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 + /// and specify the outputTemplate parameter instead. + /// + /// Path to the file. + /// The minimum level for + /// events passed through the sink. Ignored when is specified. + /// A switch allowing the pass-through minimum level + /// to be changed at runtime. + /// Configuration object allowing method chaining. + /// The file will be written using the UTF-8 character set. + public static LoggerConfiguration File( + this LoggerAuditSinkConfiguration sinkConfiguration, + ITextFormatter formatter, + string path, + LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum, + LoggingLevelSwitch levelSwitch = null) + { + return ConfigureFile(sinkConfiguration.Sink, formatter, path, restrictedToMinimumLevel, null, levelSwitch, false, true); + } + + static LoggerConfiguration ConfigureFile( + this Func addSink, + ITextFormatter formatter, + string path, + LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum, + long? fileSizeLimitBytes = DefaultFileSizeLimitBytes, + LoggingLevelSwitch levelSwitch = null, + bool buffered = false, + bool propagateExceptions = false, + bool shared = false) + { + if (addSink == null) throw new ArgumentNullException(nameof(addSink)); if (formatter == null) throw new ArgumentNullException(nameof(formatter)); 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"); - FileSink sink; - try + if (shared) { - sink = new FileSink(path, formatter, fileSizeLimitBytes, buffered: buffered); +#if !ATOMIC_APPEND + throw new NotSupportedException("File sharing is not supported on this platform."); +#endif + + if (buffered) + throw new ArgumentException("Buffered writes are not available when file sharing is enabled.", nameof(buffered)); } - catch (ArgumentException) + + ILogEventSink sink; + try { - throw; +#if ATOMIC_APPEND + if (shared) + { + sink = new SharedFileSink(path, formatter, fileSizeLimitBytes); + } + else + { +#endif + sink = new FileSink(path, formatter, fileSizeLimitBytes, buffered: buffered); +#if ATOMIC_APPEND + } +#endif } catch (Exception ex) { SelfLog.WriteLine("Unable to open file sink for {0}: {1}", path, ex); - return sinkConfiguration.Sink(new NullSink()); + + if (propagateExceptions) + throw; + + return addSink(new NullSink(), LevelAlias.Maximum, null); } - return sinkConfiguration.Sink(sink, restrictedToMinimumLevel, levelSwitch); + return addSink(sink, restrictedToMinimumLevel, levelSwitch); } } } \ No newline at end of file diff --git a/src/Serilog.Sinks.File/Sinks/File/CharacterCountLimitedTextWriter.cs b/src/Serilog.Sinks.File/Sinks/File/CharacterCountLimitedTextWriter.cs deleted file mode 100644 index f444316..0000000 --- a/src/Serilog.Sinks.File/Sinks/File/CharacterCountLimitedTextWriter.cs +++ /dev/null @@ -1,74 +0,0 @@ -// Copyright 2013-2016 Serilog Contributors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -using System; -using System.IO; -using System.Text; -using System.Threading; - -namespace Serilog.Sinks.File -{ - sealed class CharacterCountLimitedTextWriter : TextWriter - { - readonly TextWriter _outputWriter; - long _remainingCharacters; - - public CharacterCountLimitedTextWriter(TextWriter outputWriter, long remainingCharacters) - { - if (outputWriter == null) throw new ArgumentNullException(nameof(outputWriter)); - _outputWriter = outputWriter; - _remainingCharacters = remainingCharacters; - } - - public override Encoding Encoding => _outputWriter.Encoding; - - protected override void Dispose(bool disposing) - { - if (disposing) - _outputWriter.Dispose(); - - base.Dispose(disposing); - } - - public override void Write(char value) - { - var remaining = Interlocked.Decrement(ref _remainingCharacters); - if (remaining >= 0) - { - _outputWriter.Write(value); - } - else - { - // Prevent underflow (interlocking prevents torn reads) - Interlocked.Exchange(ref _remainingCharacters, 0L); - } - } - - public override void Write(char[] buffer, int index, int count) - { - var remaining = Interlocked.Add(ref _remainingCharacters, -count); - if (remaining >= 0) - { - _outputWriter.Write(buffer, index, count); - } - else - { - // Prevent underflow (interlocking prevents torn reads) - Interlocked.Exchange(ref _remainingCharacters, 0L); - } - } - - public override void Flush() => _outputWriter.Flush(); - } -} \ No newline at end of file diff --git a/src/Serilog.Sinks.File/Sinks/File/FileSink.cs b/src/Serilog.Sinks.File/Sinks/File/FileSink.cs index 8268654..fe81144 100644 --- a/src/Serilog.Sinks.File/Sinks/File/FileSink.cs +++ b/src/Serilog.Sinks.File/Sinks/File/FileSink.cs @@ -14,9 +14,11 @@ using System; using System.IO; +#if ATOMIC_APPEND +using System.Security.AccessControl; +#endif using System.Text; using Serilog.Core; -using Serilog.Debugging; using Serilog.Events; using Serilog.Formatting; @@ -27,18 +29,20 @@ namespace Serilog.Sinks.File /// public sealed class FileSink : ILogEventSink, IDisposable { - const int BytesPerCharacterApproximate = 1; readonly TextWriter _output; readonly ITextFormatter _textFormatter; + readonly long? _fileSizeLimitBytes; readonly bool _buffered; readonly object _syncRoot = new object(); + readonly WriteCountingStream _countingStreamWrapper; /// Construct a . /// Path to the file. /// Formatter used to convert log events to text. - /// The maximum size, in bytes, to which a log file will be allowed to grow. - /// For unrestricted growth, pass null. The default is 1 GB. - /// Character encoding used to write the text file. The default is UTF-8. + /// The approximate maximum size, in bytes, to which a log file will be allowed to grow. + /// For unrestricted growth, pass null. The default is 1 GB. To avoid writing partial events, the last event within the limit + /// will be written in full even if it exceeds the limit. + /// Character encoding used to write the text file. The default is UTF-8 without BOM. /// Indicates if flushing to the output file can be buffered or not. The default /// is false. /// Configuration object allowing method chaining. @@ -51,38 +55,28 @@ public FileSink(string path, ITextFormatter textFormatter, long? fileSizeLimitBy if (fileSizeLimitBytes.HasValue && fileSizeLimitBytes < 0) throw new ArgumentException("Negative value provided; file size limit must be non-negative"); _textFormatter = textFormatter; + _fileSizeLimitBytes = fileSizeLimitBytes; _buffered = buffered; - TryCreateDirectory(path); - - var file = System.IO.File.Open(path, FileMode.Append, FileAccess.Write, FileShare.Read); - var outputWriter = new StreamWriter(file, encoding ?? new UTF8Encoding(encoderShouldEmitUTF8Identifier: false)); - if (fileSizeLimitBytes != null) - { - var initialBytes = file.Length; - var remainingCharacters = Math.Max(fileSizeLimitBytes.Value - initialBytes, 0L) / BytesPerCharacterApproximate; - _output = new CharacterCountLimitedTextWriter(outputWriter, remainingCharacters); - } - else + var directory = Path.GetDirectoryName(path); + if (!string.IsNullOrWhiteSpace(directory) && !Directory.Exists(directory)) { - _output = outputWriter; + Directory.CreateDirectory(directory); } - } - static void TryCreateDirectory(string path) - { - try - { - var directory = Path.GetDirectoryName(path); - if (!string.IsNullOrWhiteSpace(directory) && !Directory.Exists(directory)) - { - Directory.CreateDirectory(directory); - } - } - catch (Exception ex) +#if ATOMIC_APPEND + // FileSystemRights.AppendData improves performance substantially (~30%) when available. + Stream file = new FileStream(path, FileMode.Append, FileSystemRights.AppendData, FileShare.Read, 4096, FileOptions.None); +#else + Stream file = System.IO.File.Open(path, FileMode.Append, FileAccess.Write, FileShare.Read); +#endif + + if (_fileSizeLimitBytes != null) { - SelfLog.WriteLine("Failed to create directory {0}: {1}", path, ex); + file = _countingStreamWrapper = new WriteCountingStream(file); } + + _output = new StreamWriter(file, encoding ?? new UTF8Encoding(encoderShouldEmitUTF8Identifier: false)); } /// @@ -94,6 +88,12 @@ public void Emit(LogEvent logEvent) if (logEvent == null) throw new ArgumentNullException(nameof(logEvent)); lock (_syncRoot) { + if (_fileSizeLimitBytes != null) + { + if (_countingStreamWrapper.CountedLength >= _fileSizeLimitBytes.Value) + return; + } + _textFormatter.Format(logEvent, _output); if (!_buffered) _output.Flush(); diff --git a/src/Serilog.Sinks.File/Sinks/File/SharedFileSink.cs b/src/Serilog.Sinks.File/Sinks/File/SharedFileSink.cs new file mode 100644 index 0000000..02ecad3 --- /dev/null +++ b/src/Serilog.Sinks.File/Sinks/File/SharedFileSink.cs @@ -0,0 +1,154 @@ +// Copyright 2013-2016 Serilog Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#if ATOMIC_APPEND + +using System; +using System.IO; +using System.Security.AccessControl; +using System.Text; +using Serilog.Core; +using Serilog.Events; +using Serilog.Formatting; + +namespace Serilog.Sinks.File +{ + /// + /// Write log events to a disk file. + /// + public sealed class SharedFileSink : ILogEventSink, 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; + int _fileStreamBufferLength = DefaultFileStreamBufferLength; + + const int DefaultFileStreamBufferLength = 4096; + + /// Construct a . + /// Path to the file. + /// Formatter used to convert log events to text. + /// The approximate maximum size, in bytes, to which a log file will be allowed to grow. + /// For unrestricted growth, pass null. The default is 1 GB. To avoid writing partial events, the last event within the limit + /// will be written in full even if it exceeds the limit. + /// Character encoding used to write the text file. The default is UTF-8 without BOM. + /// 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) + { + 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"); + + _path = path; + _textFormatter = textFormatter; + _fileSizeLimitBytes = fileSizeLimitBytes; + + var directory = Path.GetDirectoryName(path); + if (!string.IsNullOrWhiteSpace(directory) && !Directory.Exists(directory)) + { + Directory.CreateDirectory(directory); + } + + // 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, + FileMode.Append, + FileSystemRights.AppendData, + FileShare.Write, + _fileStreamBufferLength, + FileOptions.None); + + if (_fileSizeLimitBytes != null) + { + _fileInfo = new FileInfo(path); + } + + _writeBuffer = new MemoryStream(); + _output = new StreamWriter(_writeBuffer, encoding ?? new UTF8Encoding(encoderShouldEmitUTF8Identifier: false)); + } + + /// + /// Emit the provided log event to the sink. + /// + /// The log event to write. + 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; + if (length > _fileStreamBufferLength) + { + var oldOutput = _fileOutput; + + _fileOutput = new FileStream( + _path, + FileMode.Append, + FileSystemRights.AppendData, + FileShare.Write, + length, + FileOptions.None); + _fileStreamBufferLength = length; + + oldOutput.Dispose(); + } + + _fileOutput.Write(bytes, 0, length); + _fileOutput.Flush(); + } + catch + { + // Make sure there's no leftover cruft in there. + _output.Flush(); + throw; + } + finally + { + _writeBuffer.Position = 0; + _writeBuffer.SetLength(0); + } + } + } + + /// + /// Performs application-defined tasks associated with freeing, releasing, or + /// resetting unmanaged resources. + /// + public void Dispose() => _fileOutput.Dispose(); + } +} + +#endif diff --git a/src/Serilog.Sinks.File/Sinks/File/WriteCountingStream.cs b/src/Serilog.Sinks.File/Sinks/File/WriteCountingStream.cs new file mode 100644 index 0000000..ae44fa4 --- /dev/null +++ b/src/Serilog.Sinks.File/Sinks/File/WriteCountingStream.cs @@ -0,0 +1,75 @@ +// Copyright 2013-2016 Serilog Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System; +using System.IO; + +namespace Serilog.Sinks.File +{ + sealed class WriteCountingStream : Stream + { + readonly Stream _stream; + long _countedLength; + + public WriteCountingStream(Stream stream) + { + if (stream == null) throw new ArgumentNullException(nameof(stream)); + _stream = stream; + _countedLength = stream.Length; + } + + public long CountedLength => _countedLength; + + protected override void Dispose(bool disposing) + { + if (disposing) + _stream.Dispose(); + + base.Dispose(disposing); + } + + public override void Write(byte[] buffer, int offset, int count) + { + _stream.Write(buffer, offset, count); + _countedLength += count; + } + + public override void Flush() => _stream.Flush(); + public override bool CanRead => false; + public override bool CanSeek => false; + public override bool CanWrite => true; + public override long Length => _stream.Length; + + public override long Position + { + get { return _stream.Position; } + set { throw new NotSupportedException(); } + } + + public override long Seek(long offset, SeekOrigin origin) + { + throw new NotSupportedException(); + } + + public override void SetLength(long value) + { + throw new NotSupportedException(); + } + + public override int Read(byte[] buffer, int offset, int count) + { + throw new NotSupportedException(); + } + } +} \ No newline at end of file diff --git a/src/Serilog.Sinks.File/project.json b/src/Serilog.Sinks.File/project.json index 3961ead..4c47d61 100644 --- a/src/Serilog.Sinks.File/project.json +++ b/src/Serilog.Sinks.File/project.json @@ -1,5 +1,5 @@ { - "version": "2.2.0-*", + "version": "3.0.0-*", "description": "Write Serilog events to a text file in plain or JSON format.", "authors": [ "Serilog Contributors" ], "packOptions": { @@ -9,14 +9,16 @@ "iconUrl": "http://serilog.net/images/serilog-sink-nuget.png" }, "dependencies": { - "Serilog": "2.0.0" + "Serilog": "2.2.0" }, "buildOptions": { "keyFile": "../../assets/Serilog.snk", "xmlDoc": true }, "frameworks": { - "net4.5": {}, + "net4.5": { + "buildOptions": { "define": [ "ATOMIC_APPEND" ] } + }, "netstandard1.3": { "dependencies": { "System.IO": "4.1.0", diff --git a/test/Serilog.Sinks.File.Tests/FileLoggerConfigurationExtensionsTests.cs b/test/Serilog.Sinks.File.Tests/FileLoggerConfigurationExtensionsTests.cs new file mode 100644 index 0000000..8dbbdbc --- /dev/null +++ b/test/Serilog.Sinks.File.Tests/FileLoggerConfigurationExtensionsTests.cs @@ -0,0 +1,55 @@ +using System; +using Serilog; +using Serilog.Sinks.File.Tests.Support; +using Serilog.Tests.Support; +using Xunit; + +namespace Serilog.Tests +{ + public class FileLoggerConfigurationExtensionsTests + { + const string InvalidPath = "/\\"; + + [Fact] + public void WhenWritingCreationExceptionsAreSuppressed() + { + new LoggerConfiguration() + .WriteTo.File(InvalidPath) + .CreateLogger(); + } + + [Fact] + public void WhenAuditingCreationExceptionsPropagate() + { + Assert.Throws(() => + new LoggerConfiguration() + .AuditTo.File(InvalidPath) + .CreateLogger()); + } + + [Fact] + public void WhenWritingLoggingExceptionsAreSuppressed() + { + using (var tmp = TempFolder.ForCaller()) + using (var log = new LoggerConfiguration() + .WriteTo.File(new ThrowingLogEventFormatter(), tmp.AllocateFilename()) + .CreateLogger()) + { + log.Information("Hello"); + } + } + + [Fact] + public void WhenAuditingLoggingExceptionsPropagate() + { + using (var tmp = TempFolder.ForCaller()) + using (var log = new LoggerConfiguration() + .AuditTo.File(new ThrowingLogEventFormatter(), tmp.AllocateFilename()) + .CreateLogger()) + { + var ex = Assert.Throws(() => log.Information("Hello")); + Assert.IsType(ex.GetBaseException()); + } + } + } +} diff --git a/test/Serilog.Sinks.File.Tests/Sinks/File/FileSinkTests.cs b/test/Serilog.Sinks.File.Tests/FileSinkTests.cs similarity index 63% rename from test/Serilog.Sinks.File.Tests/Sinks/File/FileSinkTests.cs rename to test/Serilog.Sinks.File.Tests/FileSinkTests.cs index e71cac3..b900f73 100644 --- a/test/Serilog.Sinks.File.Tests/Sinks/File/FileSinkTests.cs +++ b/test/Serilog.Sinks.File.Tests/FileSinkTests.cs @@ -1,4 +1,5 @@ -using System.IO; +using System; +using System.IO; using Xunit; using Serilog.Formatting.Json; using Serilog.Sinks.File.Tests.Support; @@ -53,21 +54,49 @@ public void FileIsAppendedToWhenAlreadyCreated() [Fact] public void WhenLimitIsSpecifiedFileSizeIsRestricted() { - const int maxBytes = 100; + const int maxBytes = 5000; + const int eventsToLimit = 10; using (var tmp = TempFolder.ForCaller()) { var path = tmp.AllocateFilename("txt"); - var evt = Some.LogEvent(new string('n', maxBytes + 1)); + var evt = Some.LogEvent(new string('n', maxBytes / eventsToLimit)); using (var sink = new FileSink(path, new JsonFormatter(), maxBytes)) { - sink.Emit(evt); + for (var i = 0; i < eventsToLimit * 2; i++) + { + sink.Emit(evt); + } + } + + var size = new FileInfo(path).Length; + Assert.True(size > maxBytes); + Assert.True(size < maxBytes * 2); + } + } + + [Fact] + public void WhenLimitIsNotSpecifiedFileSizeIsNotRestricted() + { + const int maxBytes = 5000; + const int eventsToLimit = 10; + + using (var tmp = TempFolder.ForCaller()) + { + var path = tmp.AllocateFilename("txt"); + var evt = Some.LogEvent(new string('n', maxBytes / eventsToLimit)); + + using (var sink = new FileSink(path, new JsonFormatter(), null)) + { + for (var i = 0; i < eventsToLimit * 2; i++) + { + sink.Emit(evt); + } } var size = new FileInfo(path).Length; - Assert.True(size > 0); - Assert.True(size < maxBytes); + Assert.True(size > maxBytes * 2); } } } diff --git a/test/Serilog.Sinks.File.Tests/SharedFileSinkTests.cs b/test/Serilog.Sinks.File.Tests/SharedFileSinkTests.cs new file mode 100644 index 0000000..ec325fc --- /dev/null +++ b/test/Serilog.Sinks.File.Tests/SharedFileSinkTests.cs @@ -0,0 +1,108 @@ +#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 +{ + public class SharedFileSinkTests + { + [Fact] + public void FileIsWrittenIfNonexistent() + { + using (var tmp = TempFolder.ForCaller()) + { + var nonexistent = tmp.AllocateFilename("txt"); + var evt = Some.LogEvent("Hello, world!"); + + using (var sink = new SharedFileSink(nonexistent, new JsonFormatter(), null)) + { + sink.Emit(evt); + } + + var lines = System.IO.File.ReadAllLines(nonexistent); + Assert.Contains("Hello, world!", lines[0]); + } + } + + [Fact] + public void FileIsAppendedToWhenAlreadyCreated() + { + using (var tmp = TempFolder.ForCaller()) + { + var path = tmp.AllocateFilename("txt"); + var evt = Some.LogEvent("Hello, world!"); + + using (var sink = new SharedFileSink(path, new JsonFormatter(), null)) + { + sink.Emit(evt); + } + + using (var sink = new SharedFileSink(path, new JsonFormatter(), null)) + { + sink.Emit(evt); + } + + var lines = System.IO.File.ReadAllLines(path); + Assert.Contains("Hello, world!", lines[0]); + Assert.Contains("Hello, world!", lines[1]); + } + } + + [Fact] + public void WhenLimitIsSpecifiedFileSizeIsRestricted() + { + const int maxBytes = 5000; + const int eventsToLimit = 10; + + using (var tmp = TempFolder.ForCaller()) + { + var path = tmp.AllocateFilename("txt"); + var evt = Some.LogEvent(new string('n', maxBytes / eventsToLimit)); + + using (var sink = new SharedFileSink(path, new JsonFormatter(), maxBytes)) + { + for (var i = 0; i < eventsToLimit * 2; i++) + { + sink.Emit(evt); + } + } + + var size = new FileInfo(path).Length; + Assert.True(size > maxBytes); + Assert.True(size < maxBytes * 2); + } + } + + [Fact] + public void WhenLimitIsNotSpecifiedFileSizeIsNotRestricted() + { + const int maxBytes = 5000; + const int eventsToLimit = 10; + + using (var tmp = TempFolder.ForCaller()) + { + var path = tmp.AllocateFilename("txt"); + var evt = Some.LogEvent(new string('n', maxBytes / eventsToLimit)); + + using (var sink = new SharedFileSink(path, new JsonFormatter(), null)) + { + for (var i = 0; i < eventsToLimit * 2; i++) + { + sink.Emit(evt); + } + } + + var size = new FileInfo(path).Length; + Assert.True(size > maxBytes * 2); + } + } + } +} + +#endif diff --git a/test/Serilog.Sinks.File.Tests/Support/ThrowingLogEventFormatter.cs b/test/Serilog.Sinks.File.Tests/Support/ThrowingLogEventFormatter.cs new file mode 100644 index 0000000..9170ef5 --- /dev/null +++ b/test/Serilog.Sinks.File.Tests/Support/ThrowingLogEventFormatter.cs @@ -0,0 +1,15 @@ +using System; +using System.IO; +using Serilog.Events; +using Serilog.Formatting; + +namespace Serilog.Tests.Support +{ + public class ThrowingLogEventFormatter : ITextFormatter + { + public void Format(LogEvent logEvent, TextWriter output) + { + throw new NotImplementedException(); + } + } +}