diff --git a/example/Sample/Program.cs b/example/Sample/Program.cs new file mode 100644 index 0000000..ef4dd55 --- /dev/null +++ b/example/Sample/Program.cs @@ -0,0 +1,31 @@ +using System; +using System.IO; +using Serilog; + +namespace Sample +{ + public class Program + { + public static void Main(string[] args) + { + 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"); + + File.Delete("log.txt"); + } + } +} diff --git a/example/Sample/Properties/AssemblyInfo.cs b/example/Sample/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..0a6ff03 --- /dev/null +++ b/example/Sample/Properties/AssemblyInfo.cs @@ -0,0 +1,19 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Sample")] +[assembly: AssemblyTrademark("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("a34235a2-a717-4a1c-bf5c-f4a9e06e1260")] 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..a9b2c70 --- /dev/null +++ b/example/Sample/project.json @@ -0,0 +1,20 @@ +{ + "version": "1.0.0-*", + "buildOptions": { + "emitEntryPoint": true + }, + + "dependencies": { + "Serilog.Sinks.File": { "target": "project" }, + "Microsoft.NETCore.App": { + "type": "platform", + "version": "1.0.0" + } + }, + + "frameworks": { + "netcoreapp1.0": { + "imports": "dnxcore50" + } + } +} 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 9e77cbb..28ac33a 100644 --- a/src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs +++ b/src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs @@ -42,8 +42,9 @@ 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. /// Configuration object allowing method chaining. @@ -80,8 +81,9 @@ 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. /// Configuration object allowing method chaining. 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 088b5bd..3af8386 100644 --- a/src/Serilog.Sinks.File/Sinks/File/FileSink.cs +++ b/src/Serilog.Sinks.File/Sinks/File/FileSink.cs @@ -26,18 +26,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. @@ -50,6 +52,7 @@ 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; var directory = Path.GetDirectoryName(path); @@ -58,18 +61,13 @@ public FileSink(string path, ITextFormatter textFormatter, long? fileSizeLimitBy Directory.CreateDirectory(directory); } - 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) + Stream file = System.IO.File.Open(path, FileMode.Append, FileAccess.Write, FileShare.Read); + if (_fileSizeLimitBytes != null) { - var initialBytes = file.Length; - var remainingCharacters = Math.Max(fileSizeLimitBytes.Value - initialBytes, 0L) / BytesPerCharacterApproximate; - _output = new CharacterCountLimitedTextWriter(outputWriter, remainingCharacters); - } - else - { - _output = outputWriter; + file = _countingStreamWrapper = new WriteCountingStream(file); } + + _output = new StreamWriter(file, encoding ?? new UTF8Encoding(encoderShouldEmitUTF8Identifier: false)); } /// @@ -81,6 +79,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/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/test/Serilog.Sinks.File.Tests/FileSinkTests.cs b/test/Serilog.Sinks.File.Tests/FileSinkTests.cs index f553494..b900f73 100644 --- a/test/Serilog.Sinks.File.Tests/FileSinkTests.cs +++ b/test/Serilog.Sinks.File.Tests/FileSinkTests.cs @@ -54,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); } } }