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);
}
}
}