From 1b029c8552327f386be30e736e392691be304a98 Mon Sep 17 00:00:00 2001 From: Nicholas Blumhardt Date: Tue, 15 Nov 2016 08:00:25 +1000 Subject: [PATCH 01/69] PR code review items from #23 --- src/Serilog.Sinks.File/Sinks/File/WriteCountingStream.cs | 2 +- test/Serilog.Sinks.File.Tests/FileSinkTests.cs | 4 ++-- .../{ => Support}/FixedOutputFormatter.cs | 6 +----- 3 files changed, 4 insertions(+), 8 deletions(-) rename test/Serilog.Sinks.File.Tests/{ => Support}/FixedOutputFormatter.cs (78%) diff --git a/src/Serilog.Sinks.File/Sinks/File/WriteCountingStream.cs b/src/Serilog.Sinks.File/Sinks/File/WriteCountingStream.cs index 2693fc4..da8f0dd 100644 --- a/src/Serilog.Sinks.File/Sinks/File/WriteCountingStream.cs +++ b/src/Serilog.Sinks.File/Sinks/File/WriteCountingStream.cs @@ -60,7 +60,7 @@ public override long Position public override long Seek(long offset, SeekOrigin origin) { - return _stream.Seek(offset, origin); + throw new InvalidOperationException($"Seek operations are not available through `{nameof(WriteCountingStream)}`."); } public override void SetLength(long value) diff --git a/test/Serilog.Sinks.File.Tests/FileSinkTests.cs b/test/Serilog.Sinks.File.Tests/FileSinkTests.cs index 747b40f..0c0a13d 100644 --- a/test/Serilog.Sinks.File.Tests/FileSinkTests.cs +++ b/test/Serilog.Sinks.File.Tests/FileSinkTests.cs @@ -143,12 +143,12 @@ public void WhenLimitIsNotSpecifiedAndEncodingHasNoPreambleDataIsCorrectlyAppend WriteTwoEventsAndCheckOutputFileLength(maxBytes, encoding); } - private static void WriteTwoEventsAndCheckOutputFileLength(long? maxBytes, Encoding encoding) + static void WriteTwoEventsAndCheckOutputFileLength(long? maxBytes, Encoding encoding) { using (var tmp = TempFolder.ForCaller()) { var path = tmp.AllocateFilename("txt"); - var evt = Some.LogEvent("Irelevant as it will be replaced by the formatter "); + var evt = Some.LogEvent("Irrelevant as it will be replaced by the formatter"); var actualEventOutput = "x"; var formatter = new FixedOutputFormatter(actualEventOutput); var eventOuputLength = encoding.GetByteCount(actualEventOutput); diff --git a/test/Serilog.Sinks.File.Tests/FixedOutputFormatter.cs b/test/Serilog.Sinks.File.Tests/Support/FixedOutputFormatter.cs similarity index 78% rename from test/Serilog.Sinks.File.Tests/FixedOutputFormatter.cs rename to test/Serilog.Sinks.File.Tests/Support/FixedOutputFormatter.cs index 0e14e13..24b70da 100644 --- a/test/Serilog.Sinks.File.Tests/FixedOutputFormatter.cs +++ b/test/Serilog.Sinks.File.Tests/Support/FixedOutputFormatter.cs @@ -1,12 +1,8 @@ using Serilog.Formatting; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; using Serilog.Events; using System.IO; -namespace Serilog.Tests +namespace Serilog.Tests.Support { public class FixedOutputFormatter : ITextFormatter { From 77eed1b5772be8ad549cdbbcc2d8dcfa2ef92a54 Mon Sep 17 00:00:00 2001 From: Nicholas Blumhardt Date: Tue, 15 Nov 2016 13:18:27 +1000 Subject: [PATCH 02/69] Dev version bump [Skip CI] --- src/Serilog.Sinks.File/project.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Serilog.Sinks.File/project.json b/src/Serilog.Sinks.File/project.json index 435dae0..8adf2e2 100644 --- a/src/Serilog.Sinks.File/project.json +++ b/src/Serilog.Sinks.File/project.json @@ -1,5 +1,5 @@ { - "version": "3.1.1-*", + "version": "3.1.2-*", "description": "Write Serilog events to a text file in plain or JSON format.", "authors": [ "Serilog Contributors" ], "packOptions": { From bce2b627555f06fbd8b4dd8403ef690e490baac2 Mon Sep 17 00:00:00 2001 From: Nicholas Blumhardt Date: Thu, 24 Nov 2016 14:50:32 +1000 Subject: [PATCH 03/69] Fixes https://github.com/serilog/serilog-sinks-rollingfile/issues/37 - multiprocess sharing on .NET Core --- .gitignore | 1 + example/Sample/Program.cs | 6 +- .../FileLoggerConfigurationExtensions.cs | 6 +- ...Sink.cs => SharedFileSink.AtomicAppend.cs} | 3 +- .../Sinks/File/SharedFileSink.OSMutex.cs | 132 ++++++++++++++++++ src/Serilog.Sinks.File/project.json | 15 +- .../SharedFileSinkTests.cs | 6 +- test/Serilog.Sinks.File.Tests/project.json | 3 - 8 files changed, 154 insertions(+), 18 deletions(-) rename src/Serilog.Sinks.File/Sinks/File/{SharedFileSink.cs => SharedFileSink.AtomicAppend.cs} (98%) create mode 100644 src/Serilog.Sinks.File/Sinks/File/SharedFileSink.OSMutex.cs diff --git a/.gitignore b/.gitignore index 94420dc..483adfa 100644 --- a/.gitignore +++ b/.gitignore @@ -234,3 +234,4 @@ _Pvt_Extensions # FAKE - F# Make .fake/ +example/Sample/log.txt diff --git a/example/Sample/Program.cs b/example/Sample/Program.cs index 99a158b..9651b1a 100644 --- a/example/Sample/Program.cs +++ b/example/Sample/Program.cs @@ -11,12 +11,12 @@ public static void Main(string[] args) { SelfLog.Enable(Console.Out); + var sw = System.Diagnostics.Stopwatch.StartNew(); + Log.Logger = new LoggerConfiguration() - .WriteTo.File("log.txt") + .WriteTo.File("log.txt", shared: true) .CreateLogger(); - var sw = System.Diagnostics.Stopwatch.StartNew(); - for (var i = 0; i < 1000000; ++i) { Log.Information("Hello, file logger!"); diff --git a/src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs b/src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs index 571f414..3a54388 100644 --- a/src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs +++ b/src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs @@ -183,7 +183,7 @@ static LoggerConfiguration ConfigureFile( if (shared) { -#if ATOMIC_APPEND +#if SHARING if (buffered) throw new ArgumentException("Buffered writes are not available when file sharing is enabled.", nameof(buffered)); #else @@ -194,7 +194,7 @@ static LoggerConfiguration ConfigureFile( ILogEventSink sink; try { -#if ATOMIC_APPEND +#if SHARING if (shared) { sink = new SharedFileSink(path, formatter, fileSizeLimitBytes); @@ -203,7 +203,7 @@ static LoggerConfiguration ConfigureFile( { #endif sink = new FileSink(path, formatter, fileSizeLimitBytes, buffered: buffered); -#if ATOMIC_APPEND +#if SHARING } #endif } diff --git a/src/Serilog.Sinks.File/Sinks/File/SharedFileSink.cs b/src/Serilog.Sinks.File/Sinks/File/SharedFileSink.AtomicAppend.cs similarity index 98% rename from src/Serilog.Sinks.File/Sinks/File/SharedFileSink.cs rename to src/Serilog.Sinks.File/Sinks/File/SharedFileSink.AtomicAppend.cs index bbb5142..4ea5022 100644 --- a/src/Serilog.Sinks.File/Sinks/File/SharedFileSink.cs +++ b/src/Serilog.Sinks.File/Sinks/File/SharedFileSink.AtomicAppend.cs @@ -52,8 +52,7 @@ public sealed class SharedFileSink : ILogEventSink, IFlushableFileSink, IDisposa /// 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)); diff --git a/src/Serilog.Sinks.File/Sinks/File/SharedFileSink.OSMutex.cs b/src/Serilog.Sinks.File/Sinks/File/SharedFileSink.OSMutex.cs new file mode 100644 index 0000000..8def830 --- /dev/null +++ b/src/Serilog.Sinks.File/Sinks/File/SharedFileSink.OSMutex.cs @@ -0,0 +1,132 @@ +// 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 OS_MUTEX + +using System; +using System.IO; +using System.Text; +using Serilog.Core; +using Serilog.Events; +using Serilog.Formatting; +using System.Threading; + +namespace Serilog.Sinks.File +{ + /// + /// Write log events to a disk file. + /// + public sealed class SharedFileSink : ILogEventSink, IFlushableFileSink, IDisposable + { + readonly TextWriter _output; + readonly FileStream _underlyingStream; + readonly ITextFormatter _textFormatter; + readonly long? _fileSizeLimitBytes; + readonly object _syncRoot = new object(); + + const string MutexNameSuffix = ".serilog"; + readonly Mutex _mutex; + + /// 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"); + + _textFormatter = textFormatter; + _fileSizeLimitBytes = fileSizeLimitBytes; + + var directory = Path.GetDirectoryName(path); + if (!string.IsNullOrWhiteSpace(directory) && !Directory.Exists(directory)) + { + Directory.CreateDirectory(directory); + } + + // Backslash is special on Windows + _mutex = new Mutex(false, path.Replace('\\', ':') + MutexNameSuffix); + _underlyingStream = System.IO.File.Open(path, FileMode.Append, FileAccess.Write, FileShare.ReadWrite); + _output = new StreamWriter(_underlyingStream, 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)); + + lock (_syncRoot) + { + _mutex.WaitOne(); + try + { + _underlyingStream.Seek(0, SeekOrigin.End); + if (_fileSizeLimitBytes != null) + { + if (_underlyingStream.Length >= _fileSizeLimitBytes.Value) + return; + } + + _textFormatter.Format(logEvent, _output); + _output.Flush(); + _underlyingStream.Flush(); + } + finally + { + _mutex.ReleaseMutex(); + } + } + } + + /// + public void Dispose() + { + lock (_syncRoot) + { + _output.Dispose(); + } + } + + /// + public void FlushToDisk() + { + lock (_syncRoot) + { + _mutex.WaitOne(); + try + { + _underlyingStream.Flush(true); + } + finally + { + _mutex.ReleaseMutex(); + } + } + } + } +} + +#endif diff --git a/src/Serilog.Sinks.File/project.json b/src/Serilog.Sinks.File/project.json index 8adf2e2..39905e1 100644 --- a/src/Serilog.Sinks.File/project.json +++ b/src/Serilog.Sinks.File/project.json @@ -17,7 +17,18 @@ }, "frameworks": { "net4.5": { - "buildOptions": { "define": [ "ATOMIC_APPEND" ] } + "buildOptions": { "define": [ "SHARING", "ATOMIC_APPEND" ] } + }, + "netcoreapp1.0": { + "buildOptions": { "define": [ "SHARING", "OS_MUTEX" ] }, + "dependencies": { + "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.Threading.Timer": "4.0.1", + "System.Threading": "4.0.11" + } }, "netstandard1.3": { "dependencies": { @@ -25,7 +36,7 @@ "System.IO.FileSystem": "4.0.1", "System.IO.FileSystem.Primitives": "4.0.1", "System.Text.Encoding.Extensions": "4.0.11", - "System.Threading.Timer": "4.0.1" + "System.Threading.Timer": "4.0.1" } } } diff --git a/test/Serilog.Sinks.File.Tests/SharedFileSinkTests.cs b/test/Serilog.Sinks.File.Tests/SharedFileSinkTests.cs index d0bd751..f63eac1 100644 --- a/test/Serilog.Sinks.File.Tests/SharedFileSinkTests.cs +++ b/test/Serilog.Sinks.File.Tests/SharedFileSinkTests.cs @@ -1,6 +1,4 @@ -#if ATOMIC_APPEND - -using System.IO; +using System.IO; using Xunit; using Serilog.Formatting.Json; using Serilog.Sinks.File.Tests.Support; @@ -102,5 +100,3 @@ public void WhenLimitIsNotSpecifiedFileSizeIsNotRestricted() } } } - -#endif diff --git a/test/Serilog.Sinks.File.Tests/project.json b/test/Serilog.Sinks.File.Tests/project.json index 1fa084d..3f14b0e 100644 --- a/test/Serilog.Sinks.File.Tests/project.json +++ b/test/Serilog.Sinks.File.Tests/project.json @@ -19,9 +19,6 @@ ] }, "net4.5.2": { - "buildOptions": { - "define": ["ATOMIC_APPEND"] - } } } } From 06e457a3d19b4a0cb00717a0bfd5c3be2d2016fe Mon Sep 17 00:00:00 2001 From: Nicholas Blumhardt Date: Thu, 24 Nov 2016 15:17:00 +1000 Subject: [PATCH 04/69] Sharing on netstandard1.3; dispose _mutex; ten second timeout waiting for mutex acquisition --- example/Sample/Program.cs | 2 +- .../FileLoggerConfigurationExtensions.cs | 16 ++-------------- .../Sinks/File/SharedFileSink.OSMutex.cs | 17 +++++++++++++++-- src/Serilog.Sinks.File/project.json | 15 +++------------ 4 files changed, 21 insertions(+), 29 deletions(-) diff --git a/example/Sample/Program.cs b/example/Sample/Program.cs index 9651b1a..89e6c95 100644 --- a/example/Sample/Program.cs +++ b/example/Sample/Program.cs @@ -14,7 +14,7 @@ public static void Main(string[] args) var sw = System.Diagnostics.Stopwatch.StartNew(); Log.Logger = new LoggerConfiguration() - .WriteTo.File("log.txt", shared: true) + .WriteTo.File("log.txt") .CreateLogger(); for (var i = 0; i < 1000000; ++i) diff --git a/src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs b/src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs index 3a54388..353e6c6 100644 --- a/src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs +++ b/src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs @@ -180,32 +180,20 @@ static LoggerConfiguration ConfigureFile( 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"); - - if (shared) - { -#if SHARING - if (buffered) - throw new ArgumentException("Buffered writes are not available when file sharing is enabled.", nameof(buffered)); -#else - throw new NotSupportedException("File sharing is not supported on this platform."); -#endif - } + if (shared && buffered) + throw new ArgumentException("Buffered writes are not available when file sharing is enabled.", nameof(buffered)); ILogEventSink sink; try { -#if SHARING if (shared) { sink = new SharedFileSink(path, formatter, fileSizeLimitBytes); } else { -#endif sink = new FileSink(path, formatter, fileSizeLimitBytes, buffered: buffered); -#if SHARING } -#endif } catch (Exception ex) { diff --git a/src/Serilog.Sinks.File/Sinks/File/SharedFileSink.OSMutex.cs b/src/Serilog.Sinks.File/Sinks/File/SharedFileSink.OSMutex.cs index 8def830..22e0263 100644 --- a/src/Serilog.Sinks.File/Sinks/File/SharedFileSink.OSMutex.cs +++ b/src/Serilog.Sinks.File/Sinks/File/SharedFileSink.OSMutex.cs @@ -21,6 +21,7 @@ using Serilog.Events; using Serilog.Formatting; using System.Threading; +using Serilog.Debugging; namespace Serilog.Sinks.File { @@ -36,6 +37,7 @@ public sealed class SharedFileSink : ILogEventSink, IFlushableFileSink, IDisposa readonly object _syncRoot = new object(); const string MutexNameSuffix = ".serilog"; + const int MutexWaitTimeout = 10000; readonly Mutex _mutex; /// Construct a . @@ -80,7 +82,12 @@ public void Emit(LogEvent logEvent) lock (_syncRoot) { - _mutex.WaitOne(); + if (!_mutex.WaitOne(MutexWaitTimeout)) + { + SelfLog.WriteLine("Shared file mutex could not be acquired in {0} ms for event emitting", MutexWaitTimeout); + return; + } + try { _underlyingStream.Seek(0, SeekOrigin.End); @@ -107,6 +114,7 @@ public void Dispose() lock (_syncRoot) { _output.Dispose(); + _mutex.Dispose(); } } @@ -115,7 +123,12 @@ public void FlushToDisk() { lock (_syncRoot) { - _mutex.WaitOne(); + if (!_mutex.WaitOne(MutexWaitTimeout)) + { + SelfLog.WriteLine("Shared file mutex could not be acquired in {0} ms for disk flush operation", MutexWaitTimeout); + return; + } + try { _underlyingStream.Flush(true); diff --git a/src/Serilog.Sinks.File/project.json b/src/Serilog.Sinks.File/project.json index 39905e1..73378e9 100644 --- a/src/Serilog.Sinks.File/project.json +++ b/src/Serilog.Sinks.File/project.json @@ -17,10 +17,10 @@ }, "frameworks": { "net4.5": { - "buildOptions": { "define": [ "SHARING", "ATOMIC_APPEND" ] } + "buildOptions": { "define": [ "ATOMIC_APPEND" ] } }, - "netcoreapp1.0": { - "buildOptions": { "define": [ "SHARING", "OS_MUTEX" ] }, + "netstandard1.3": { + "buildOptions": { "define": [ "OS_MUTEX" ] }, "dependencies": { "System.IO": "4.1.0", "System.IO.FileSystem": "4.0.1", @@ -29,15 +29,6 @@ "System.Threading.Timer": "4.0.1", "System.Threading": "4.0.11" } - }, - "netstandard1.3": { - "dependencies": { - "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.Threading.Timer": "4.0.1" - } } } } From b0234273629a80985cdb5c22bf15542df5be45bb Mon Sep 17 00:00:00 2001 From: Nicholas Blumhardt Date: Fri, 25 Nov 2016 13:21:01 +1000 Subject: [PATCH 05/69] Handle AbandonedMutexException(); exclude path separator char for Linux --- .../Sinks/File/SharedFileSink.OSMutex.cs | 41 +++++++++++++------ 1 file changed, 29 insertions(+), 12 deletions(-) diff --git a/src/Serilog.Sinks.File/Sinks/File/SharedFileSink.OSMutex.cs b/src/Serilog.Sinks.File/Sinks/File/SharedFileSink.OSMutex.cs index 22e0263..d3cf809 100644 --- a/src/Serilog.Sinks.File/Sinks/File/SharedFileSink.OSMutex.cs +++ b/src/Serilog.Sinks.File/Sinks/File/SharedFileSink.OSMutex.cs @@ -66,8 +66,8 @@ public SharedFileSink(string path, ITextFormatter textFormatter, long? fileSizeL Directory.CreateDirectory(directory); } - // Backslash is special on Windows - _mutex = new Mutex(false, path.Replace('\\', ':') + MutexNameSuffix); + var mutexName = Path.GetFullPath(path).Replace(Path.DirectorySeparatorChar, ':') + MutexNameSuffix; + _mutex = new Mutex(false, mutexName); _underlyingStream = System.IO.File.Open(path, FileMode.Append, FileAccess.Write, FileShare.ReadWrite); _output = new StreamWriter(_underlyingStream, encoding ?? new UTF8Encoding(encoderShouldEmitUTF8Identifier: false)); } @@ -82,11 +82,8 @@ public void Emit(LogEvent logEvent) lock (_syncRoot) { - if (!_mutex.WaitOne(MutexWaitTimeout)) - { - SelfLog.WriteLine("Shared file mutex could not be acquired in {0} ms for event emitting", MutexWaitTimeout); + if (!TryAcquireMutex()) return; - } try { @@ -103,7 +100,7 @@ public void Emit(LogEvent logEvent) } finally { - _mutex.ReleaseMutex(); + ReleaseMutex(); } } } @@ -123,11 +120,8 @@ public void FlushToDisk() { lock (_syncRoot) { - if (!_mutex.WaitOne(MutexWaitTimeout)) - { - SelfLog.WriteLine("Shared file mutex could not be acquired in {0} ms for disk flush operation", MutexWaitTimeout); + if (!TryAcquireMutex()) return; - } try { @@ -135,9 +129,32 @@ public void FlushToDisk() } finally { - _mutex.ReleaseMutex(); + ReleaseMutex(); + } + } + } + + bool TryAcquireMutex() + { + try + { + if (!_mutex.WaitOne(MutexWaitTimeout)) + { + SelfLog.WriteLine("Shared file mutex could not be acquired within {0} ms", MutexWaitTimeout); + return false; } } + catch (AbandonedMutexException) + { + SelfLog.WriteLine("Inherited shared file mutex after abandonment by another process"); + } + + return true; + } + + void ReleaseMutex() + { + _mutex.ReleaseMutex(); } } } From 4d94c35fab05d1991419711951742c3ef3ccd0c3 Mon Sep 17 00:00:00 2001 From: Nicholas Blumhardt Date: Mon, 28 Nov 2016 08:38:43 +1000 Subject: [PATCH 06/69] Dev version bump to 3.2 - new functionality --- src/Serilog.Sinks.File/project.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Serilog.Sinks.File/project.json b/src/Serilog.Sinks.File/project.json index 73378e9..24b7530 100644 --- a/src/Serilog.Sinks.File/project.json +++ b/src/Serilog.Sinks.File/project.json @@ -1,5 +1,5 @@ { - "version": "3.1.2-*", + "version": "3.2.0-*", "description": "Write Serilog events to a text file in plain or JSON format.", "authors": [ "Serilog Contributors" ], "packOptions": { From 256536b67be8d441db193c765a003218c19a5d96 Mon Sep 17 00:00:00 2001 From: Sergey Komisarchik Date: Sun, 27 Nov 2016 12:15:01 +0300 Subject: [PATCH 07/69] enable travis ci --- .editorconfig | 13 ++++- .travis.yml | 49 +++++++++++++++++++ build.sh | 12 +++++ serilog-sinks-file.sln | 3 ++ .../FileLoggerConfigurationExtensionsTests.cs | 6 +-- .../Support/TempFolder.cs | 8 ++- 6 files changed, 84 insertions(+), 7 deletions(-) create mode 100644 .travis.yml create mode 100644 build.sh diff --git a/.editorconfig b/.editorconfig index f76c08a..f16002a 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,5 +1,16 @@ -root=true +root = true [*] +trim_trailing_whitespace = true +insert_final_newline = true indent_style = space indent_size = 4 + +[*.{csproj,json,config,yml}] +indent_size = 2 + +[*.sh] +end_of_line = lf + +[*.{cmd, bat}] +end_of_line = crlf diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..e9daee9 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,49 @@ +language: csharp + +#dotnet cli require Ubuntu 14.04 +sudo: required +dist: trusty + +#dotnet cli require OSX 10.10 +osx_image: xcode7.1 + +addons: + apt: + packages: + - gettext + - libcurl4-openssl-dev + - libicu-dev + - libssl-dev + - libunwind8 + - zlib1g + +os: + - linux + +env: + global: + - DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true + - TMP: /tmp + + matrix: + - CLI_VERSION=1.0.0-preview2-003121 + - CLI_VERSION=Latest + +matrix: + allow_failures: + - env: CLI_VERSION=Latest + +before_install: + - if test "$TRAVIS_OS_NAME" == "osx"; then brew update; brew install openssl; brew link --force openssl; fi + # Download script to install dotnet cli + - if test "$CLI_OBTAIN_URL" == ""; then export CLI_OBTAIN_URL="https://raw.githubusercontent.com/dotnet/cli/rel/1.0.0-preview2/scripts/obtain/dotnet-install.sh"; fi + - curl -L --create-dirs $CLI_OBTAIN_URL -o ./scripts/obtain/install.sh + - find ./scripts -name "*.sh" -exec chmod +x {} \; + - export DOTNET_INSTALL_DIR="$PWD/.dotnetcli" + # use bash to workaround bug https://github.com/dotnet/cli/issues/1725 + - sudo bash ./scripts/obtain/install.sh --channel "preview" --version "$CLI_VERSION" --install-dir "$DOTNET_INSTALL_DIR" --no-path + # add dotnet to PATH + - export PATH="$DOTNET_INSTALL_DIR:$PATH" + +script: + - ./build.sh \ No newline at end of file diff --git a/build.sh b/build.sh new file mode 100644 index 0000000..39408c4 --- /dev/null +++ b/build.sh @@ -0,0 +1,12 @@ +#!/bin/bash +dotnet restore --no-cache +for path in src/*/project.json; do + dirname="$(dirname "${path}")" + dotnet build ${dirname} -f netstandard1.3 -c Release +done + +for path in test/*.Tests/project.json; do + dirname="$(dirname "${path}")" + dotnet build ${dirname} -f netcoreapp1.0 -c Release + dotnet test ${dirname} -f netcoreapp1.0 -c Release +done diff --git a/serilog-sinks-file.sln b/serilog-sinks-file.sln index bf1cf9a..536f0ab 100644 --- a/serilog-sinks-file.sln +++ b/serilog-sinks-file.sln @@ -7,8 +7,11 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{037440DE-440 EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "assets", "assets", "{E9D1B5E1-DEB9-4A04-8BAB-24EC7240ADAF}" ProjectSection(SolutionItems) = preProject + .editorconfig = .editorconfig + .travis.yml = .travis.yml appveyor.yml = appveyor.yml Build.ps1 = Build.ps1 + build.sh = build.sh global.json = global.json NuGet.Config = NuGet.Config README.md = README.md diff --git a/test/Serilog.Sinks.File.Tests/FileLoggerConfigurationExtensionsTests.cs b/test/Serilog.Sinks.File.Tests/FileLoggerConfigurationExtensionsTests.cs index 78c35b3..e39cccb 100644 --- a/test/Serilog.Sinks.File.Tests/FileLoggerConfigurationExtensionsTests.cs +++ b/test/Serilog.Sinks.File.Tests/FileLoggerConfigurationExtensionsTests.cs @@ -1,15 +1,15 @@ using System; using System.Threading; -using Serilog; using Serilog.Sinks.File.Tests.Support; using Serilog.Tests.Support; using Xunit; +using System.IO; namespace Serilog.Tests { public class FileLoggerConfigurationExtensionsTests { - const string InvalidPath = "/\\"; + static readonly string InvalidPath = new string(Path.GetInvalidPathChars()); [Fact] public void WhenWritingCreationExceptionsAreSuppressed() @@ -66,7 +66,6 @@ public void WhenFlushingToDiskReportedFileSinkCanBeCreatedAndDisposed() } } -#if ATOMIC_APPEND [Fact] public void WhenFlushingToDiskReportedSharedFileSinkCanBeCreatedAndDisposed() { @@ -79,6 +78,5 @@ public void WhenFlushingToDiskReportedSharedFileSinkCanBeCreatedAndDisposed() Thread.Sleep(TimeSpan.FromSeconds(1)); } } -#endif } } diff --git a/test/Serilog.Sinks.File.Tests/Support/TempFolder.cs b/test/Serilog.Sinks.File.Tests/Support/TempFolder.cs index a57d71f..f809c05 100644 --- a/test/Serilog.Sinks.File.Tests/Support/TempFolder.cs +++ b/test/Serilog.Sinks.File.Tests/Support/TempFolder.cs @@ -37,10 +37,14 @@ public void Dispose() } } - public static TempFolder ForCaller([CallerMemberName] string caller = null) + public static TempFolder ForCaller([CallerMemberName] string caller = null, [CallerFilePath] string sourceFileName = "") { if (caller == null) throw new ArgumentNullException(nameof(caller)); - return new TempFolder(caller); + if (sourceFileName == null) throw new ArgumentNullException(nameof(sourceFileName)); + + var folderName = System.IO.Path.GetFileNameWithoutExtension(sourceFileName) + "_" + caller; + + return new TempFolder(folderName); } public string AllocateFilename(string ext = null) From db2cbe1037641ab11722ab423de71049db364e30 Mon Sep 17 00:00:00 2001 From: Nicholas Blumhardt Date: Tue, 29 Nov 2016 14:47:18 +1000 Subject: [PATCH 08/69] Dropped in the Travis build badge [Skip CI] --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 267b97d..db97f6e 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Serilog.Sinks.File [![Build status](https://ci.appveyor.com/api/projects/status/hh9gymy0n6tne46j?svg=true)](https://ci.appveyor.com/project/serilog/serilog-sinks-file) [![NuGet Version](http://img.shields.io/nuget/v/Serilog.Sinks.File.svg?style=flat)](https://www.nuget.org/packages/Serilog.Sinks.File/) [![Documentation](https://img.shields.io/badge/docs-wiki-yellow.svg)](https://github.com/serilog/serilog/wiki) [![Join the chat at https://gitter.im/serilog/serilog](https://img.shields.io/gitter/room/serilog/serilog.svg)](https://gitter.im/serilog/serilog) +# Serilog.Sinks.File [![Build status](https://ci.appveyor.com/api/projects/status/hh9gymy0n6tne46j?svg=true)](https://ci.appveyor.com/project/serilog/serilog-sinks-file) [![Travis build](https://travis-ci.org/serilog/serilog-sinks-file.svg)](https://travis-ci.org/serilog/serilog-sinks-file) [![NuGet Version](http://img.shields.io/nuget/v/Serilog.Sinks.File.svg?style=flat)](https://www.nuget.org/packages/Serilog.Sinks.File/) [![Documentation](https://img.shields.io/badge/docs-wiki-yellow.svg)](https://github.com/serilog/serilog/wiki) [![Join the chat at https://gitter.im/serilog/serilog](https://img.shields.io/gitter/room/serilog/serilog.svg)](https://gitter.im/serilog/serilog) Writes [Serilog](https://serilog.net) events to a text file. From 5a1519ec8dbe12da0ece867e50d7cdae812a046b Mon Sep 17 00:00:00 2001 From: Matthew Erbs Date: Tue, 29 Nov 2016 15:05:46 +1000 Subject: [PATCH 09/69] Execute permissions on build.sh. --- build.sh | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 build.sh diff --git a/build.sh b/build.sh old mode 100644 new mode 100755 From ffe253f608e144508f07d3fd3b31c025b1baab84 Mon Sep 17 00:00:00 2001 From: Nicholas Blumhardt Date: Tue, 3 Jan 2017 12:29:13 +1000 Subject: [PATCH 10/69] Dev version bump [Skip CI] --- src/Serilog.Sinks.File/project.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Serilog.Sinks.File/project.json b/src/Serilog.Sinks.File/project.json index 24b7530..50c5c75 100644 --- a/src/Serilog.Sinks.File/project.json +++ b/src/Serilog.Sinks.File/project.json @@ -1,5 +1,5 @@ { - "version": "3.2.0-*", + "version": "3.2.1-*", "description": "Write Serilog events to a text file in plain or JSON format.", "authors": [ "Serilog Contributors" ], "packOptions": { From ade7acddcdc3a4fa6684092ab38f29d9222be3e2 Mon Sep 17 00:00:00 2001 From: Nikita Balabaev Date: Mon, 15 May 2017 10:02:31 +0700 Subject: [PATCH 11/69] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index db97f6e..7a6f5d4 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,6 @@ Multiple processes can concurrently write to the same log file if the `shared` p 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. +The [Serilog.Sinks.Async](https://github.com/serilog/serilog-sinks-async) package can be used to wrap the file sink and perform all disk access 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)._ From b8986cb75de0f14ce835ecaaa41c5c69bab18947 Mon Sep 17 00:00:00 2001 From: Nicholas Blumhardt Date: Wed, 11 Oct 2017 15:27:10 +1000 Subject: [PATCH 12/69] Rolling WIP --- example/Sample/Sample.csproj | 26 ++++ example/Sample/Sample.xproj | 21 ---- example/Sample/project.json | 23 ---- global.json | 6 - serilog-sinks-file.sln | 18 +-- .../FileLoggerConfigurationExtensions.cs | 115 ++++++++++++++++-- src/Serilog.Sinks.File/RollingInterval.cs | 52 ++++++++ .../Serilog.Sinks.File.csproj | 48 ++++++++ .../Serilog.Sinks.File.xproj | 18 --- src/Serilog.Sinks.File/project.json | 34 ------ .../Serilog.Sinks.File.Tests.csproj | 31 +++++ .../Serilog.Sinks.File.Tests.xproj | 21 ---- test/Serilog.Sinks.File.Tests/project.json | 24 ---- 13 files changed, 272 insertions(+), 165 deletions(-) create mode 100644 example/Sample/Sample.csproj delete mode 100644 example/Sample/Sample.xproj delete mode 100644 example/Sample/project.json delete mode 100644 global.json create mode 100644 src/Serilog.Sinks.File/RollingInterval.cs create mode 100644 src/Serilog.Sinks.File/Serilog.Sinks.File.csproj delete mode 100644 src/Serilog.Sinks.File/Serilog.Sinks.File.xproj delete mode 100644 src/Serilog.Sinks.File/project.json create mode 100644 test/Serilog.Sinks.File.Tests/Serilog.Sinks.File.Tests.csproj delete mode 100644 test/Serilog.Sinks.File.Tests/Serilog.Sinks.File.Tests.xproj delete mode 100644 test/Serilog.Sinks.File.Tests/project.json diff --git a/example/Sample/Sample.csproj b/example/Sample/Sample.csproj new file mode 100644 index 0000000..6691382 --- /dev/null +++ b/example/Sample/Sample.csproj @@ -0,0 +1,26 @@ + + + + netcoreapp1.0;net45 + Sample + Exe + Sample + win10-x64 + + + + + + + + + + + + + + + + + + diff --git a/example/Sample/Sample.xproj b/example/Sample/Sample.xproj deleted file mode 100644 index 000aa06..0000000 --- a/example/Sample/Sample.xproj +++ /dev/null @@ -1,21 +0,0 @@ - - - - 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 deleted file mode 100644 index 525d510..0000000 --- a/example/Sample/project.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "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/global.json b/global.json deleted file mode 100644 index a2b2a41..0000000 --- a/global.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "projects": [ "src", "test" ], - "sdk": { - "version": "1.0.0-preview2-003121" - } -} diff --git a/serilog-sinks-file.sln b/serilog-sinks-file.sln index 536f0ab..71527e4 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.25420.1 +# Visual Studio 15 +VisualStudioVersion = 15.0.26730.15 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{037440DE-440B-4129-9F7A-09B42D00397E}" EndProject @@ -12,21 +12,20 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "assets", "assets", "{E9D1B5 appveyor.yml = appveyor.yml Build.ps1 = Build.ps1 build.sh = build.sh - global.json = global.json NuGet.Config = NuGet.Config README.md = README.md assets\Serilog.snk = assets\Serilog.snk EndProjectSection EndProject -Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Serilog.Sinks.File", "src\Serilog.Sinks.File\Serilog.Sinks.File.xproj", "{57E0ED0E-0F45-48AB-A73D-6A92B7C32095}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{7B927378-9F16-4F6F-B3F6-156395136646}" 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}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Serilog.Sinks.File", "src\Serilog.Sinks.File\Serilog.Sinks.File.csproj", "{57E0ED0E-0F45-48AB-A73D-6A92B7C32095}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Serilog.Sinks.File.Tests", "test\Serilog.Sinks.File.Tests\Serilog.Sinks.File.Tests.csproj", "{3C2D8E01-5580-426A-BDD9-EC59CD98E618}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sample", "example\Sample\Sample.csproj", "{A34235A2-A717-4A1C-BF5C-F4A9E06E1260}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -55,4 +54,7 @@ Global {3C2D8E01-5580-426A-BDD9-EC59CD98E618} = {7B927378-9F16-4F6F-B3F6-156395136646} {A34235A2-A717-4A1C-BF5C-F4A9E06E1260} = {196B1544-C617-4D7C-96D1-628713BDD52A} EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {EA0197B4-FCA8-4DF2-BF34-274FA41333D1} + EndGlobalSection EndGlobal diff --git a/src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs b/src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs index 353e6c6..5b31436 100644 --- a/src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs +++ b/src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs @@ -13,6 +13,7 @@ // limitations under the License. using System; +using System.ComponentModel; using Serilog.Configuration; using Serilog.Core; using Serilog.Debugging; @@ -22,13 +23,15 @@ using Serilog.Formatting.Json; using Serilog.Sinks.File; +// ReSharper disable MethodOverloadWithOptionalParameter + namespace Serilog { /// Extends with methods to add file sinks. public static class FileLoggerConfigurationExtensions { const long DefaultFileSizeLimitBytes = 1L * 1024 * 1024 * 1024; - const string DefaultOutputTemplate = "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level}] {Message}{NewLine}{Exception}"; + const string DefaultOutputTemplate = "[{Timestamp:o} {Level:u3}] {Message:lj} {Properties}{NewLine}{Exception}"; /// /// Write log events to the specified file. @@ -41,7 +44,7 @@ public static class FileLoggerConfigurationExtensions /// 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}". + /// the default is "[{Timestamp:o} {Level:u3}] {Message:lj} {Properties}{NewLine}{Exception}". /// 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. @@ -51,6 +54,89 @@ public static class FileLoggerConfigurationExtensions /// 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. + [Obsolete("New code should not be compiled against this obsolete overload"), EditorBrowsable(EditorBrowsableState.Never)] + public static LoggerConfiguration File( + this LoggerSinkConfiguration sinkConfiguration, + string path, + LogEventLevel restrictedToMinimumLevel, + string outputTemplate, + IFormatProvider formatProvider, + long? fileSizeLimitBytes, + LoggingLevelSwitch levelSwitch, + bool buffered, + bool shared, + TimeSpan? flushToDiskInterval) + { + 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, flushToDiskInterval: flushToDiskInterval); + } + + /// + /// 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. + /// 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. + /// 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. + [Obsolete("New code should not be compiled against this obsolete overload"), EditorBrowsable(EditorBrowsableState.Never)] + public static LoggerConfiguration File( + this LoggerSinkConfiguration sinkConfiguration, + ITextFormatter formatter, + string path, + LogEventLevel restrictedToMinimumLevel, + long? fileSizeLimitBytes, + LoggingLevelSwitch levelSwitch, + bool buffered, + bool shared, + TimeSpan? flushToDiskInterval) + { + return ConfigureFile(sinkConfiguration.Sink, formatter, path, restrictedToMinimumLevel, fileSizeLimitBytes, levelSwitch, buffered: buffered, shared: shared, flushToDiskInterval: flushToDiskInterval); + } + + /// + /// 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:o} {Level:u3}] {Message:lj} {Properties}{NewLine}{Exception}". + /// 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. + /// If provided, a full disk flush will be performed periodically at the specified interval. + /// The interval at which logging will roll over to a new file. + /// If true, a new file will be created when the file size limit is reached. Filenames + /// will have a number appended in the format _NNNNN, with the first filename given no number. + /// Configuration object allowing method chaining. + /// The file will be written using the UTF-8 character set. public static LoggerConfiguration File( this LoggerSinkConfiguration sinkConfiguration, string path, @@ -61,14 +147,16 @@ public static LoggerConfiguration File( LoggingLevelSwitch levelSwitch = null, bool buffered = false, bool shared = false, - TimeSpan? flushToDiskInterval = null) + TimeSpan? flushToDiskInterval = null, + RollingInterval rollingInterval = RollingInterval.Infinite, + bool rollOnFileSizeLimit = 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: buffered, shared: shared, flushToDiskInterval: flushToDiskInterval); + return File(sinkConfiguration, formatter, path, restrictedToMinimumLevel, fileSizeLimitBytes, levelSwitch, buffered: buffered, shared: shared, flushToDiskInterval: flushToDiskInterval, rollingInterval: rollingInterval, rollOnFileSizeLimit: rollOnFileSizeLimit); } /// @@ -77,7 +165,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. @@ -92,6 +180,9 @@ public static LoggerConfiguration File( /// 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. + /// The interval at which logging will roll over to a new file. + /// If true, a new file will be created when the file size limit is reached. Filenames + /// will have a number appended in the format _NNNNN, with the first filename given no number. /// Configuration object allowing method chaining. /// The file will be written using the UTF-8 character set. public static LoggerConfiguration File( @@ -103,9 +194,11 @@ public static LoggerConfiguration File( LoggingLevelSwitch levelSwitch = null, bool buffered = false, bool shared = false, - TimeSpan? flushToDiskInterval = null) + TimeSpan? flushToDiskInterval = null, + RollingInterval rollingInterval = RollingInterval.Infinite, + bool rollOnFileSizeLimit = false) { - return ConfigureFile(sinkConfiguration.Sink, formatter, path, restrictedToMinimumLevel, fileSizeLimitBytes, levelSwitch, buffered: buffered, shared: shared, flushToDiskInterval: flushToDiskInterval); + return ConfigureFile(sinkConfiguration.Sink, formatter, path, restrictedToMinimumLevel, fileSizeLimitBytes, levelSwitch, buffered: buffered, shared: shared, flushToDiskInterval: flushToDiskInterval, rollingInterval: rollingInterval, rollOnFileSizeLimit: rollOnFileSizeLimit); } /// @@ -119,7 +212,7 @@ public static LoggerConfiguration File( /// 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}". + /// the default is "[{Timestamp:o} {Level:u3}] {Message:lj} {Properties}{NewLine}{Exception}". /// Configuration object allowing method chaining. /// The file will be written using the UTF-8 character set. public static LoggerConfiguration File( @@ -174,7 +267,9 @@ static LoggerConfiguration ConfigureFile( bool buffered = false, bool propagateExceptions = false, bool shared = false, - TimeSpan? flushToDiskInterval = null) + TimeSpan? flushToDiskInterval = null, + RollingInterval rollingInterval = RollingInterval.Infinite, + bool rollOnFileSizeLimit = false) { if (addSink == null) throw new ArgumentNullException(nameof(addSink)); if (formatter == null) throw new ArgumentNullException(nameof(formatter)); @@ -212,5 +307,5 @@ static LoggerConfiguration ConfigureFile( return addSink(sink, restrictedToMinimumLevel, levelSwitch); } - } + } } diff --git a/src/Serilog.Sinks.File/RollingInterval.cs b/src/Serilog.Sinks.File/RollingInterval.cs new file mode 100644 index 0000000..9fac848 --- /dev/null +++ b/src/Serilog.Sinks.File/RollingInterval.cs @@ -0,0 +1,52 @@ +// Copyright 2017 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. + +namespace Serilog +{ + /// + /// Specifies the frequency at which the log file should roll. + /// + public enum RollingInterval + { + /// + /// The log file will never roll; no time period information will be appended to the log file name. + /// + Infinite, + + /// + /// Roll every year. Filenames will have a four-digit year appended in the pattern yyyy. + /// + Year, + + /// + /// Roll every calendar month. Filenames will have yyyyMM appended. + /// + Month, + + /// + /// Roll every day. Filenames will have yyyyMMdd appended. + /// + Day, + + /// + /// Roll every hour. Filenames will have yyyyMMddHH appended. + /// + Hour, + + /// + /// Roll every minute. Filenames will have yyyyMMddHHmm appended. + /// + Minute + } +} diff --git a/src/Serilog.Sinks.File/Serilog.Sinks.File.csproj b/src/Serilog.Sinks.File/Serilog.Sinks.File.csproj new file mode 100644 index 0000000..096e70c --- /dev/null +++ b/src/Serilog.Sinks.File/Serilog.Sinks.File.csproj @@ -0,0 +1,48 @@ + + + + Write Serilog events to a text file in plain or JSON format. + 4.0.0 + Serilog Contributors + net45;netstandard1.3 + true + Serilog.Sinks.File + ../../assets/Serilog.snk + true + true + Serilog.Sinks.File + serilog;file;io + http://serilog.net/images/serilog-sink-nuget.png + http://serilog.net + http://www.apache.org/licenses/LICENSE-2.0 + false + Serilog + + + + + + + + + + + + + $(DefineConstants);ATOMIC_APPEND + + + + $(DefineConstants);OS_MUTEX + + + + + + + + + + + + diff --git a/src/Serilog.Sinks.File/Serilog.Sinks.File.xproj b/src/Serilog.Sinks.File/Serilog.Sinks.File.xproj deleted file mode 100644 index c8d4c28..0000000 --- a/src/Serilog.Sinks.File/Serilog.Sinks.File.xproj +++ /dev/null @@ -1,18 +0,0 @@ - - - - 14.0 - $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) - - - - 57e0ed0e-0f45-48ab-a73d-6a92b7c32095 - Serilog - .\obj - .\bin\ - - - 2.0 - - - \ No newline at end of file diff --git a/src/Serilog.Sinks.File/project.json b/src/Serilog.Sinks.File/project.json deleted file mode 100644 index 50c5c75..0000000 --- a/src/Serilog.Sinks.File/project.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "version": "3.2.1-*", - "description": "Write Serilog events to a text file in plain or JSON format.", - "authors": [ "Serilog Contributors" ], - "packOptions": { - "tags": [ "serilog", "file", "io" ], - "projectUrl": "http://serilog.net", - "licenseUrl": "http://www.apache.org/licenses/LICENSE-2.0", - "iconUrl": "http://serilog.net/images/serilog-sink-nuget.png" - }, - "dependencies": { - "Serilog": "2.3.0" - }, - "buildOptions": { - "keyFile": "../../assets/Serilog.snk", - "xmlDoc": true - }, - "frameworks": { - "net4.5": { - "buildOptions": { "define": [ "ATOMIC_APPEND" ] } - }, - "netstandard1.3": { - "buildOptions": { "define": [ "OS_MUTEX" ] }, - "dependencies": { - "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.Threading.Timer": "4.0.1", - "System.Threading": "4.0.11" - } - } - } -} diff --git a/test/Serilog.Sinks.File.Tests/Serilog.Sinks.File.Tests.csproj b/test/Serilog.Sinks.File.Tests/Serilog.Sinks.File.Tests.csproj new file mode 100644 index 0000000..e38eb92 --- /dev/null +++ b/test/Serilog.Sinks.File.Tests/Serilog.Sinks.File.Tests.csproj @@ -0,0 +1,31 @@ + + + + netcoreapp1.0;net452 + Serilog.Sinks.File.Tests + Serilog.Sinks.File.Tests + true + $(PackageTargetFallback);dnxcore50;portable-net45+win8 + 1.0.4 + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/Serilog.Sinks.File.Tests/Serilog.Sinks.File.Tests.xproj b/test/Serilog.Sinks.File.Tests/Serilog.Sinks.File.Tests.xproj deleted file mode 100644 index 3234f8a..0000000 --- a/test/Serilog.Sinks.File.Tests/Serilog.Sinks.File.Tests.xproj +++ /dev/null @@ -1,21 +0,0 @@ - - - - 14.0 - $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) - - - - 3c2d8e01-5580-426a-bdd9-ec59cd98e618 - Serilog.Tests - .\obj - .\bin\ - - - 2.0 - - - - - - \ No newline at end of file diff --git a/test/Serilog.Sinks.File.Tests/project.json b/test/Serilog.Sinks.File.Tests/project.json deleted file mode 100644 index 3f14b0e..0000000 --- a/test/Serilog.Sinks.File.Tests/project.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "testRunner": "xunit", - "dependencies": { - "Serilog.Sinks.File": { "target": "project" }, - "xunit": "2.1.0", - "dotnet-test-xunit": "1.0.0-rc2-build10025" - }, - "frameworks": { - "netcoreapp1.0": { - "dependencies": { - "Microsoft.NETCore.App": { - "type": "platform", - "version": "1.0.0" - } - }, - "imports": [ - "dnxcore50", - "portable-net45+win8" - ] - }, - "net4.5.2": { - } - } -} From 7614553eb0eabe45b5a80afc9dd3481221fcd219 Mon Sep 17 00:00:00 2001 From: Nicholas Blumhardt Date: Fri, 13 Oct 2017 14:39:26 +1000 Subject: [PATCH 13/69] WIP --- example/Sample/Program.cs | 2 +- example/Sample/Sample.csproj | 10 +- .../FileLoggerConfigurationExtensions.cs | 76 ++-- .../Serilog.Sinks.File.csproj | 14 +- src/Serilog.Sinks.File/Sinks/File/Clock.cs | 38 ++ src/Serilog.Sinks.File/Sinks/File/FileSink.cs | 25 +- .../Sinks/File/IFileSink.cs | 29 ++ .../Sinks/File/IFlushableFileSink.cs | 16 +- src/Serilog.Sinks.File/Sinks/File/IOErrors.cs | 31 ++ .../Sinks/File/PathRoller.cs | 116 +++++ .../Sinks/File/RollingFileSink.cs | 233 ++++++++++ .../Sinks/File/RollingIntervalExtensions.cs | 86 ++++ .../Sinks/File/RollingLogFile.cs | 34 ++ .../Sinks/File/SharedFileSink.AtomicAppend.cs | 20 +- .../Sinks/File/SharedFileSink.OSMutex.cs | 22 +- .../FileLoggerConfigurationExtensionsTests.cs | 8 + .../RollingFileSinkTests.cs | 150 +++++++ .../Serilog.Sinks.File.Tests.csproj | 23 +- .../Support/CollectingSink.cs | 21 + .../Support/DelegateDisposable.cs | 24 + .../Support/DelegatingEnricher.cs | 22 + .../Support/DelegatingSink.cs | 33 ++ .../Support/DisposableLogger.cs | 422 ++++++++++++++++++ .../Support/DisposeTrackingSink.cs | 20 + .../Support/Extensions.cs | 12 + test/Serilog.Sinks.File.Tests/Support/Some.cs | 93 +++- .../TemplatedPathRollerTests.cs | 126 ++++++ 27 files changed, 1621 insertions(+), 85 deletions(-) create mode 100644 src/Serilog.Sinks.File/Sinks/File/Clock.cs create mode 100644 src/Serilog.Sinks.File/Sinks/File/IFileSink.cs create mode 100644 src/Serilog.Sinks.File/Sinks/File/IOErrors.cs create mode 100644 src/Serilog.Sinks.File/Sinks/File/PathRoller.cs create mode 100644 src/Serilog.Sinks.File/Sinks/File/RollingFileSink.cs create mode 100644 src/Serilog.Sinks.File/Sinks/File/RollingIntervalExtensions.cs create mode 100644 src/Serilog.Sinks.File/Sinks/File/RollingLogFile.cs create mode 100644 test/Serilog.Sinks.File.Tests/RollingFileSinkTests.cs create mode 100644 test/Serilog.Sinks.File.Tests/Support/CollectingSink.cs create mode 100644 test/Serilog.Sinks.File.Tests/Support/DelegateDisposable.cs create mode 100644 test/Serilog.Sinks.File.Tests/Support/DelegatingEnricher.cs create mode 100644 test/Serilog.Sinks.File.Tests/Support/DelegatingSink.cs create mode 100644 test/Serilog.Sinks.File.Tests/Support/DisposableLogger.cs create mode 100644 test/Serilog.Sinks.File.Tests/Support/DisposeTrackingSink.cs create mode 100644 test/Serilog.Sinks.File.Tests/Support/Extensions.cs create mode 100644 test/Serilog.Sinks.File.Tests/TemplatedPathRollerTests.cs diff --git a/example/Sample/Program.cs b/example/Sample/Program.cs index 89e6c95..98bf4c5 100644 --- a/example/Sample/Program.cs +++ b/example/Sample/Program.cs @@ -14,7 +14,7 @@ public static void Main(string[] args) var sw = System.Diagnostics.Stopwatch.StartNew(); Log.Logger = new LoggerConfiguration() - .WriteTo.File("log.txt") + .WriteTo.File("log.txt", fileSizeLimitBytes: 1000000, rollingInterval: RollingInterval.Day, rollOnFileSizeLimit: true) .CreateLogger(); for (var i = 0; i < 1000000; ++i) diff --git a/example/Sample/Sample.csproj b/example/Sample/Sample.csproj index 6691382..ec04f95 100644 --- a/example/Sample/Sample.csproj +++ b/example/Sample/Sample.csproj @@ -1,25 +1,23 @@ - netcoreapp1.0;net45 + netcoreapp2.0;net47 Sample Exe Sample - win10-x64 + true - + - - - + diff --git a/src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs b/src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs index 5b31436..d6c645d 100644 --- a/src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs +++ b/src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs @@ -14,6 +14,7 @@ using System; using System.ComponentModel; +using System.Text; using Serilog.Configuration; using Serilog.Core; using Serilog.Debugging; @@ -30,8 +31,9 @@ namespace Serilog /// Extends with methods to add file sinks. public static class FileLoggerConfigurationExtensions { + const int DefaultRetainedFileCountLimit = 31; // A long month of logs const long DefaultFileSizeLimitBytes = 1L * 1024 * 1024 * 1024; - const string DefaultOutputTemplate = "[{Timestamp:o} {Level:u3}] {Message:lj} {Properties}{NewLine}{Exception}"; + const string DefaultOutputTemplate = "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception}"; /// /// Write log events to the specified file. @@ -44,7 +46,7 @@ public static class FileLoggerConfigurationExtensions /// 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:o} {Level:u3}] {Message:lj} {Properties}{NewLine}{Exception}". + /// the default is "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception}". /// 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. @@ -124,7 +126,7 @@ public static LoggerConfiguration File( /// 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:o} {Level:u3}] {Message:lj} {Properties}{NewLine}{Exception}". + /// the default is "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception}". /// 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. @@ -135,6 +137,9 @@ public static LoggerConfiguration File( /// The interval at which logging will roll over to a new file. /// If true, a new file will be created when the file size limit is reached. Filenames /// will have a number appended in the format _NNNNN, with the first filename given no number. + /// The maximum number of log files that will be retained, + /// including the current log file. For unlimited retention, pass null. The default is 31. + /// 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 static LoggerConfiguration File( @@ -149,7 +154,9 @@ public static LoggerConfiguration File( bool shared = false, TimeSpan? flushToDiskInterval = null, RollingInterval rollingInterval = RollingInterval.Infinite, - bool rollOnFileSizeLimit = false) + bool rollOnFileSizeLimit = false, + int? retainedFileCountLimit = DefaultRetainedFileCountLimit, + Encoding encoding = null) { if (sinkConfiguration == null) throw new ArgumentNullException(nameof(sinkConfiguration)); if (path == null) throw new ArgumentNullException(nameof(path)); @@ -165,7 +172,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. @@ -183,6 +190,9 @@ public static LoggerConfiguration File( /// The interval at which logging will roll over to a new file. /// If true, a new file will be created when the file size limit is reached. Filenames /// will have a number appended in the format _NNNNN, with the first filename given no number. + /// The maximum number of log files that will be retained, + /// including the current log file. For unlimited retention, pass null. The default is 31. + /// 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 static LoggerConfiguration File( @@ -196,9 +206,13 @@ public static LoggerConfiguration File( bool shared = false, TimeSpan? flushToDiskInterval = null, RollingInterval rollingInterval = RollingInterval.Infinite, - bool rollOnFileSizeLimit = false) + bool rollOnFileSizeLimit = false, + int? retainedFileCountLimit = DefaultRetainedFileCountLimit, + Encoding encoding = null) { - return ConfigureFile(sinkConfiguration.Sink, formatter, path, restrictedToMinimumLevel, fileSizeLimitBytes, levelSwitch, buffered: buffered, shared: shared, flushToDiskInterval: flushToDiskInterval, rollingInterval: rollingInterval, rollOnFileSizeLimit: rollOnFileSizeLimit); + return ConfigureFile(sinkConfiguration.Sink, formatter, path, restrictedToMinimumLevel, fileSizeLimitBytes, levelSwitch, + buffered: buffered, shared: shared, flushToDiskInterval: flushToDiskInterval, rollingInterval: rollingInterval, + rollOnFileSizeLimit: rollOnFileSizeLimit, retainedFileCountLimit: retainedFileCountLimit, encoding: encoding); } /// @@ -212,7 +226,7 @@ public static LoggerConfiguration File( /// 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:o} {Level:u3}] {Message:lj} {Properties}{NewLine}{Exception}". + /// the default is "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception}". /// Configuration object allowing method chaining. /// The file will be written using the UTF-8 character set. public static LoggerConfiguration File( @@ -268,36 +282,48 @@ static LoggerConfiguration ConfigureFile( bool propagateExceptions = false, bool shared = false, TimeSpan? flushToDiskInterval = null, + Encoding encoding = null, RollingInterval rollingInterval = RollingInterval.Infinite, - bool rollOnFileSizeLimit = false) + bool rollOnFileSizeLimit = false, + int? retainedFileCountLimit = DefaultRetainedFileCountLimit) { 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"); - if (shared && buffered) - throw new ArgumentException("Buffered writes are not available when file sharing is enabled.", nameof(buffered)); + if (fileSizeLimitBytes.HasValue && fileSizeLimitBytes < 0) throw new ArgumentException("Negative value provided; file size limit must be non-negative.", nameof(fileSizeLimitBytes)); + if (retainedFileCountLimit.HasValue && retainedFileCountLimit < 1) throw new ArgumentException("At least one file must be retained.", nameof(retainedFileCountLimit)); + if (shared && buffered) throw new ArgumentException("Buffered writes are not available when file sharing is enabled.", nameof(buffered)); ILogEventSink sink; - try + + if (rollOnFileSizeLimit || rollingInterval != RollingInterval.Infinite) + { + sink = new RollingFileSink(path, formatter, fileSizeLimitBytes, retainedFileCountLimit, encoding, buffered, shared, rollingInterval, rollOnFileSizeLimit); + } + else { - if (shared) + try { - sink = new SharedFileSink(path, formatter, fileSizeLimitBytes); +#pragma warning disable 618 + if (shared) + { + sink = new SharedFileSink(path, formatter, fileSizeLimitBytes); + } + else + { + sink = new FileSink(path, formatter, fileSizeLimitBytes, buffered: buffered); + } +#pragma warning restore 618 } - else + catch (Exception ex) { - sink = new FileSink(path, formatter, fileSizeLimitBytes, buffered: buffered); - } - } - catch (Exception ex) - { - SelfLog.WriteLine("Unable to open file sink for {0}: {1}", path, ex); + SelfLog.WriteLine("Unable to open file sink for {0}: {1}", path, ex); - if (propagateExceptions) - throw; + if (propagateExceptions) + throw; - return addSink(new NullSink(), LevelAlias.Maximum, null); + return addSink(new NullSink(), LevelAlias.Maximum, null); + } } if (flushToDiskInterval.HasValue) diff --git a/src/Serilog.Sinks.File/Serilog.Sinks.File.csproj b/src/Serilog.Sinks.File/Serilog.Sinks.File.csproj index 096e70c..569e5b0 100644 --- a/src/Serilog.Sinks.File/Serilog.Sinks.File.csproj +++ b/src/Serilog.Sinks.File/Serilog.Sinks.File.csproj @@ -1,7 +1,7 @@ - Write Serilog events to a text file in plain or JSON format. + Write Serilog events to text files in plain or JSON format. 4.0.0 Serilog Contributors net45;netstandard1.3 @@ -11,25 +11,30 @@ true true Serilog.Sinks.File - serilog;file;io + serilog;file http://serilog.net/images/serilog-sink-nuget.png http://serilog.net http://www.apache.org/licenses/LICENSE-2.0 false Serilog + true + Serilog.Sinks.File + + true - + + - $(DefineConstants);ATOMIC_APPEND + $(DefineConstants);ATOMIC_APPEND;HRESULTS @@ -43,6 +48,7 @@ + diff --git a/src/Serilog.Sinks.File/Sinks/File/Clock.cs b/src/Serilog.Sinks.File/Sinks/File/Clock.cs new file mode 100644 index 0000000..b7cf3cc --- /dev/null +++ b/src/Serilog.Sinks.File/Sinks/File/Clock.cs @@ -0,0 +1,38 @@ +// 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; + +namespace Serilog.Sinks.File +{ + static class Clock + { + static Func _dateTimeNow = () => DateTime.Now; + + [ThreadStatic] + static DateTime _testDateTimeNow; + + public static DateTime DateTimeNow => _dateTimeNow(); + + // Time is set per thread to support parallel + // If any thread uses the clock in test mode, all threads + // must use it in test mode; once set to test mode only + // terminating the application returns it to normal use. + public static void SetTestDateTimeNow(DateTime now) + { + _testDateTimeNow = now; + _dateTimeNow = () => _testDateTimeNow; + } + } +} diff --git a/src/Serilog.Sinks.File/Sinks/File/FileSink.cs b/src/Serilog.Sinks.File/Sinks/File/FileSink.cs index 443519a..bfd288f 100644 --- a/src/Serilog.Sinks.File/Sinks/File/FileSink.cs +++ b/src/Serilog.Sinks.File/Sinks/File/FileSink.cs @@ -15,7 +15,6 @@ using System; using System.IO; using System.Text; -using Serilog.Core; using Serilog.Events; using Serilog.Formatting; @@ -24,7 +23,8 @@ namespace Serilog.Sinks.File /// /// Write log events to a disk file. /// - public sealed class FileSink : ILogEventSink, IFlushableFileSink, IDisposable + [Obsolete("This type will be removed from the public API in a future version; use `WriteTo.File()` instead.")] + public sealed class FileSink : IFileSink, IDisposable { readonly TextWriter _output; readonly FileStream _underlyingStream; @@ -50,7 +50,7 @@ public FileSink(string path, ITextFormatter textFormatter, long? fileSizeLimitBy { 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."); _textFormatter = textFormatter; _fileSizeLimitBytes = fileSizeLimitBytes; @@ -71,11 +71,7 @@ public FileSink(string path, ITextFormatter textFormatter, long? fileSizeLimitBy _output = new StreamWriter(outputStream, encoding ?? new UTF8Encoding(encoderShouldEmitUTF8Identifier: false)); } - /// - /// Emit the provided log event to the sink. - /// - /// The log event to write. - public void Emit(LogEvent logEvent) + bool IFileSink.EmitOrOverflow(LogEvent logEvent) { if (logEvent == null) throw new ArgumentNullException(nameof(logEvent)); lock (_syncRoot) @@ -83,15 +79,26 @@ public void Emit(LogEvent logEvent) if (_fileSizeLimitBytes != null) { if (_countingStreamWrapper.CountedLength >= _fileSizeLimitBytes.Value) - return; + return false; } _textFormatter.Format(logEvent, _output); if (!_buffered) _output.Flush(); + + return true; } } + /// + /// Emit the provided log event to the sink. + /// + /// The log event to write. + public void Emit(LogEvent logEvent) + { + ((IFileSink) this).EmitOrOverflow(logEvent); + } + /// public void Dispose() { diff --git a/src/Serilog.Sinks.File/Sinks/File/IFileSink.cs b/src/Serilog.Sinks.File/Sinks/File/IFileSink.cs new file mode 100644 index 0000000..89268ab --- /dev/null +++ b/src/Serilog.Sinks.File/Sinks/File/IFileSink.cs @@ -0,0 +1,29 @@ +// Copyright 2017 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 Serilog.Core; +using Serilog.Events; + +namespace Serilog.Sinks.File +{ + /// + /// Exists only for the convenience of , which + /// switches implementations based on sharing. Would refactor, but preserving + /// backwards compatibility. + /// + interface IFileSink : ILogEventSink, IFlushableFileSink + { + bool EmitOrOverflow(LogEvent logEvent); + } +} diff --git a/src/Serilog.Sinks.File/Sinks/File/IFlushableFileSink.cs b/src/Serilog.Sinks.File/Sinks/File/IFlushableFileSink.cs index c74727e..75d6e52 100644 --- a/src/Serilog.Sinks.File/Sinks/File/IFlushableFileSink.cs +++ b/src/Serilog.Sinks.File/Sinks/File/IFlushableFileSink.cs @@ -1,4 +1,18 @@ -namespace Serilog.Sinks.File +// Copyright 2017 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. + +namespace Serilog.Sinks.File { /// /// Supported by (file-based) sinks that can be explicitly flushed. diff --git a/src/Serilog.Sinks.File/Sinks/File/IOErrors.cs b/src/Serilog.Sinks.File/Sinks/File/IOErrors.cs new file mode 100644 index 0000000..36fe8bc --- /dev/null +++ b/src/Serilog.Sinks.File/Sinks/File/IOErrors.cs @@ -0,0 +1,31 @@ +// 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.IO; + +namespace Serilog.Sinks.File +{ + static class IOErrors + { + public static bool IsLockedFile(IOException ex) + { +#if HRESULTS + var errorCode = System.Runtime.InteropServices.Marshal.GetHRForException(ex) & ((1 << 16) - 1); + return errorCode == 32 || errorCode == 33; +#else + return true; +#endif + } + } +} diff --git a/src/Serilog.Sinks.File/Sinks/File/PathRoller.cs b/src/Serilog.Sinks.File/Sinks/File/PathRoller.cs new file mode 100644 index 0000000..17c496b --- /dev/null +++ b/src/Serilog.Sinks.File/Sinks/File/PathRoller.cs @@ -0,0 +1,116 @@ +// 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.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Text.RegularExpressions; + +namespace Serilog.Sinks.File +{ + class PathRoller + { + const string PeriodMatchGroup = "period"; + const string SequenceNumberMatchGroup = "sequence"; + + readonly string _directory; + readonly string _filenamePrefix; + readonly string _filenameSuffix; + readonly Regex _filenameMatcher; + + readonly RollingInterval _interval; + readonly string _periodFormat; + + public PathRoller(string path, RollingInterval interval) + { + if (path == null) throw new ArgumentNullException(nameof(path)); + _interval = interval; + _periodFormat = interval.GetFormat(); + + var pathDirectory = Path.GetDirectoryName(path); + if (string.IsNullOrEmpty(pathDirectory)) + pathDirectory = Directory.GetCurrentDirectory(); + + _directory = Path.GetFullPath(pathDirectory); + _filenamePrefix = Path.GetFileNameWithoutExtension(path); + _filenameSuffix = Path.GetExtension(path); + _filenameMatcher = new Regex( + "^" + + Regex.Escape(_filenamePrefix) + + "(?<" + PeriodMatchGroup + ">\\d{" + _periodFormat.Length + "})" + + "(?<" + SequenceNumberMatchGroup + ">_[0-9]{3,}){0,1}" + + Regex.Escape(_filenameSuffix) + + "$"); + + DirectorySearchPattern = $"{_filenamePrefix}*{_filenameSuffix}"; + } + + public string LogFileDirectory => _directory; + + public string DirectorySearchPattern { get; } + + public void GetLogFilePath(DateTime date, int? sequenceNumber, out string path) + { + var currentCheckpoint = GetCurrentCheckpoint(date); + + var tok = currentCheckpoint?.ToString(_periodFormat, CultureInfo.InvariantCulture) ?? ""; + + if (sequenceNumber != null) + tok += "_" + sequenceNumber.Value.ToString("000", CultureInfo.InvariantCulture); + + path = Path.Combine(_directory, _filenamePrefix + tok + _filenameSuffix); + } + + public IEnumerable SelectMatches(IEnumerable filenames) + { + foreach (var filename in filenames) + { + var match = _filenameMatcher.Match(filename); + if (!match.Success) + continue; + + int? inc = null; + var incGroup = match.Groups[SequenceNumberMatchGroup]; + if (incGroup.Captures.Count != 0) + { + var incPart = incGroup.Captures[0].Value.Substring(1); + inc = int.Parse(incPart, CultureInfo.InvariantCulture); + } + + DateTime? period = null; + var periodGroup = match.Groups[PeriodMatchGroup]; + if (periodGroup.Captures.Count != 0) + { + var dateTimePart = periodGroup.Captures[0].Value; + if (DateTime.TryParseExact( + dateTimePart, + _periodFormat, + CultureInfo.InvariantCulture, + DateTimeStyles.None, + out var dateTime)) + { + period = dateTime; + } + } + + yield return new RollingLogFile(filename, period, inc); + } + } + + public DateTime? GetCurrentCheckpoint(DateTime instant) => _interval.GetCurrentCheckpoint(instant); + + public DateTime? GetNextCheckpoint(DateTime instant) => _interval.GetNextCheckpoint(instant); + } +} diff --git a/src/Serilog.Sinks.File/Sinks/File/RollingFileSink.cs b/src/Serilog.Sinks.File/Sinks/File/RollingFileSink.cs new file mode 100644 index 0000000..cd91bc1 --- /dev/null +++ b/src/Serilog.Sinks.File/Sinks/File/RollingFileSink.cs @@ -0,0 +1,233 @@ +// 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. + +#pragma warning disable 618 + +using System; +using System.IO; +using System.Linq; +using System.Text; +using Serilog.Core; +using Serilog.Debugging; +using Serilog.Events; +using Serilog.Formatting; + +namespace Serilog.Sinks.File +{ + sealed class RollingFileSink : ILogEventSink, IFlushableFileSink, IDisposable + { + readonly PathRoller _roller; + readonly ITextFormatter _textFormatter; + readonly long? _fileSizeLimitBytes; + readonly int? _retainedFileCountLimit; + readonly Encoding _encoding; + readonly bool _buffered; + readonly bool _shared; + readonly bool _rollOnFileSizeLimit; + + readonly object _syncRoot = new object(); + bool _isDisposed; + DateTime? _nextCheckpoint; + IFileSink _currentFile; + int? _currentFileSequence; + + public RollingFileSink(string path, + ITextFormatter textFormatter, + long? fileSizeLimitBytes, + int? retainedFileCountLimit, + Encoding encoding, + bool buffered, + bool shared, + RollingInterval rollingInterval, + bool rollOnFileSizeLimit) + { + 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"); + if (retainedFileCountLimit.HasValue && retainedFileCountLimit < 1) throw new ArgumentException("Zero or negative value provided; retained file count limit must be at least 1"); + + _roller = new PathRoller(path, rollingInterval); + _textFormatter = textFormatter; + _fileSizeLimitBytes = fileSizeLimitBytes; + _retainedFileCountLimit = retainedFileCountLimit; + _encoding = encoding; + _buffered = buffered; + _shared = shared; + _rollOnFileSizeLimit = rollOnFileSizeLimit; + } + + public void Emit(LogEvent logEvent) + { + if (logEvent == null) throw new ArgumentNullException(nameof(logEvent)); + + lock (_syncRoot) + { + if (_isDisposed) throw new ObjectDisposedException("The log file has been disposed."); + + var now = Clock.DateTimeNow; + AlignCurrentFileTo(now); + + while (_currentFile?.EmitOrOverflow(logEvent) == false && _rollOnFileSizeLimit) + { + AlignCurrentFileTo(now, nextSequence: true); + } + } + } + + void AlignCurrentFileTo(DateTime now, bool nextSequence = false) + { + if (!_nextCheckpoint.HasValue) + { + OpenFile(now); + } + else if (nextSequence || now >= _nextCheckpoint.Value) + { + int? minSequence = null; + if (nextSequence) + { + if (_currentFileSequence == null) + minSequence = 1; + else + minSequence = _currentFileSequence.Value + 1; + } + + CloseFile(); + OpenFile(now, minSequence); + } + } + + void OpenFile(DateTime now, int? minSequence = null) + { + var currentCheckpoint = _roller.GetCurrentCheckpoint(now); + + // We only try periodically because repeated failures + // to open log files REALLY slow an app down. + _nextCheckpoint = _roller.GetNextCheckpoint(now) ?? now.AddMinutes(30); + + var existingFiles = Enumerable.Empty(); + try + { + existingFiles = Directory.GetFiles(_roller.LogFileDirectory, _roller.DirectorySearchPattern) + .Select(Path.GetFileName); + } + catch (DirectoryNotFoundException) { } + + var latestForThisCheckpoint = _roller + .SelectMatches(existingFiles) + .Where(m => m.DateTime == currentCheckpoint) + .OrderByDescending(m => m.SequenceNumber) + .FirstOrDefault(); + + var sequence = latestForThisCheckpoint?.SequenceNumber; + if (minSequence != null) + { + if (sequence == null || sequence.Value < minSequence.Value) + sequence = minSequence; + } + + const int maxAttempts = 3; + for (var attempt = 0; attempt < maxAttempts; attempt++) + { + _roller.GetLogFilePath(now, sequence, out var path); + + try + { + _currentFile = _shared ? + (IFileSink)new SharedFileSink(path, _textFormatter, _fileSizeLimitBytes, _encoding) : + new FileSink(path, _textFormatter, _fileSizeLimitBytes, _encoding, _buffered); + _currentFileSequence = sequence; + } + catch (IOException ex) + { + if (IOErrors.IsLockedFile(ex)) + { + SelfLog.WriteLine("File target {0} was locked, attempting to open next in sequence (attempt {1})", path, attempt + 1); + sequence = (sequence ?? 0) + 1; + continue; + } + + throw; + } + + ApplyRetentionPolicy(path); + return; + } + } + + void ApplyRetentionPolicy(string currentFilePath) + { + if (_retainedFileCountLimit == null) return; + + var currentFileName = Path.GetFileName(currentFilePath); + + // We consider the current file to exist, even if nothing's been written yet, + // because files are only opened on response to an event being processed. + var potentialMatches = Directory.GetFiles(_roller.LogFileDirectory, _roller.DirectorySearchPattern) + .Select(Path.GetFileName) + .Union(new [] { currentFileName }); + + var newestFirst = _roller + .SelectMatches(potentialMatches) + .OrderByDescending(m => m.DateTime) + .ThenByDescending(m => m.SequenceNumber) + .Select(m => m.Filename); + + var toRemove = newestFirst + .Where(n => StringComparer.OrdinalIgnoreCase.Compare(currentFileName, n) != 0) + .Skip(_retainedFileCountLimit.Value - 1) + .ToList(); + + foreach (var obsolete in toRemove) + { + var fullPath = Path.Combine(_roller.LogFileDirectory, obsolete); + try + { + System.IO.File.Delete(fullPath); + } + catch (Exception ex) + { + SelfLog.WriteLine("Error {0} while removing obsolete log file {1}", ex, fullPath); + } + } + } + + public void Dispose() + { + lock (_syncRoot) + { + if (_currentFile == null) return; + CloseFile(); + _isDisposed = true; + } + } + + void CloseFile() + { + if (_currentFile != null) + { + (_currentFile as IDisposable)?.Dispose(); + _currentFile = null; + } + + _nextCheckpoint = null; + } + + public void FlushToDisk() + { + lock (_syncRoot) + { + _currentFile?.FlushToDisk(); + } + } + } +} diff --git a/src/Serilog.Sinks.File/Sinks/File/RollingIntervalExtensions.cs b/src/Serilog.Sinks.File/Sinks/File/RollingIntervalExtensions.cs new file mode 100644 index 0000000..364153a --- /dev/null +++ b/src/Serilog.Sinks.File/Sinks/File/RollingIntervalExtensions.cs @@ -0,0 +1,86 @@ +// Copyright 2017 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; + +namespace Serilog.Sinks.File +{ + static class RollingIntervalExtensions + { + public static string GetFormat(this RollingInterval interval) + { + switch (interval) + { + case RollingInterval.Infinite: + return ""; + case RollingInterval.Year: + return "yyyy"; + case RollingInterval.Month: + return "yyyyMM"; + case RollingInterval.Day: + return "yyyyMMdd"; + case RollingInterval.Hour: + return "yyyyMMddHH"; + case RollingInterval.Minute: + return "yyyyMMddHHmm"; + default: + throw new ArgumentException("Invalid rolling interval"); + } + } + + public static DateTime? GetCurrentCheckpoint(this RollingInterval interval, DateTime instant) + { + switch (interval) + { + case RollingInterval.Infinite: + return null; + case RollingInterval.Year: + return new DateTime(instant.Year, 0, 0, 0, 0, 0, instant.Kind); + case RollingInterval.Month: + return new DateTime(instant.Year, instant.Month, 0, 0, 0, 0, instant.Kind); + case RollingInterval.Day: + return new DateTime(instant.Year, instant.Month, instant.Day, 0, 0, 0, instant.Kind); + case RollingInterval.Hour: + return new DateTime(instant.Year, instant.Month, instant.Day, instant.Hour, 0, 0, instant.Kind); + case RollingInterval.Minute: + return new DateTime(instant.Year, instant.Month, instant.Day, instant.Hour, instant.Minute, 0, instant.Kind); + default: + throw new ArgumentException("Invalid rolling interval"); + } + } + + public static DateTime? GetNextCheckpoint(this RollingInterval interval, DateTime instant) + { + var current = GetCurrentCheckpoint(interval, instant); + if (current == null) + return null; + + switch (interval) + { + case RollingInterval.Year: + return current.Value.AddYears(1); + case RollingInterval.Month: + return current.Value.AddMonths(1); + case RollingInterval.Day: + return current.Value.AddDays(1); + case RollingInterval.Hour: + return current.Value.AddHours(1); + case RollingInterval.Minute: + return current.Value.AddMinutes(1); + default: + throw new ArgumentException("Invalid rolling interval"); + } + } + } +} diff --git a/src/Serilog.Sinks.File/Sinks/File/RollingLogFile.cs b/src/Serilog.Sinks.File/Sinks/File/RollingLogFile.cs new file mode 100644 index 0000000..3850e7d --- /dev/null +++ b/src/Serilog.Sinks.File/Sinks/File/RollingLogFile.cs @@ -0,0 +1,34 @@ +// 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; + +namespace Serilog.Sinks.File +{ + class RollingLogFile + { + public RollingLogFile(string filename, DateTime? dateTime, int? sequenceNumber) + { + Filename = filename; + DateTime = dateTime; + SequenceNumber = sequenceNumber; + } + + public string Filename { get; } + + public DateTime? DateTime { get; } + + public int? SequenceNumber { get; } + } +} diff --git a/src/Serilog.Sinks.File/Sinks/File/SharedFileSink.AtomicAppend.cs b/src/Serilog.Sinks.File/Sinks/File/SharedFileSink.AtomicAppend.cs index 4ea5022..805e786 100644 --- a/src/Serilog.Sinks.File/Sinks/File/SharedFileSink.AtomicAppend.cs +++ b/src/Serilog.Sinks.File/Sinks/File/SharedFileSink.AtomicAppend.cs @@ -27,7 +27,8 @@ namespace Serilog.Sinks.File /// /// Write log events to a disk file. /// - public sealed class SharedFileSink : ILogEventSink, IFlushableFileSink, IDisposable + [Obsolete("This type will be removed from the public API in a future version; use `WriteTo.File(shared: true)` instead.")] + public sealed class SharedFileSink : IFileSink, IDisposable { readonly MemoryStream _writeBuffer; readonly string _path; @@ -84,11 +85,7 @@ public SharedFileSink(string path, ITextFormatter textFormatter, long? fileSizeL encoding ?? new UTF8Encoding(encoderShouldEmitUTF8Identifier: false)); } - /// - /// Emit the provided log event to the sink. - /// - /// The log event to write. - public void Emit(LogEvent logEvent) + bool IFileSink.EmitOrOverflow(LogEvent logEvent) { if (logEvent == null) throw new ArgumentNullException(nameof(logEvent)); @@ -121,13 +118,14 @@ public void Emit(LogEvent logEvent) try { if (_fileOutput.Length >= _fileSizeLimitBytes.Value) - return; + return false; } catch (FileNotFoundException) { } // Cheaper and more reliable than checking existence } _fileOutput.Write(bytes, 0, length); _fileOutput.Flush(); + return true; } catch { @@ -143,6 +141,14 @@ public void Emit(LogEvent logEvent) } } + /// + /// Emit the provided log event to the sink. + /// + /// The log event to write. + public void Emit(LogEvent logEvent) + { + ((IFileSink)this).EmitOrOverflow(logEvent); + } /// public void Dispose() diff --git a/src/Serilog.Sinks.File/Sinks/File/SharedFileSink.OSMutex.cs b/src/Serilog.Sinks.File/Sinks/File/SharedFileSink.OSMutex.cs index d3cf809..a779bda 100644 --- a/src/Serilog.Sinks.File/Sinks/File/SharedFileSink.OSMutex.cs +++ b/src/Serilog.Sinks.File/Sinks/File/SharedFileSink.OSMutex.cs @@ -28,7 +28,7 @@ namespace Serilog.Sinks.File /// /// Write log events to a disk file. /// - public sealed class SharedFileSink : ILogEventSink, IFlushableFileSink, IDisposable + public sealed class SharedFileSink : IFileSink, IDisposable { readonly TextWriter _output; readonly FileStream _underlyingStream; @@ -72,18 +72,14 @@ public SharedFileSink(string path, ITextFormatter textFormatter, long? fileSizeL _output = new StreamWriter(_underlyingStream, encoding ?? new UTF8Encoding(encoderShouldEmitUTF8Identifier: false)); } - /// - /// Emit the provided log event to the sink. - /// - /// The log event to write. - public void Emit(LogEvent logEvent) + bool IFileSink.EmitOrOverflow(LogEvent logEvent) { if (logEvent == null) throw new ArgumentNullException(nameof(logEvent)); lock (_syncRoot) { if (!TryAcquireMutex()) - return; + return true; // We didn't overflow, but, roll-on-size should not be attempted try { @@ -91,12 +87,13 @@ public void Emit(LogEvent logEvent) if (_fileSizeLimitBytes != null) { if (_underlyingStream.Length >= _fileSizeLimitBytes.Value) - return; + return false; } _textFormatter.Format(logEvent, _output); _output.Flush(); _underlyingStream.Flush(); + return true; } finally { @@ -105,6 +102,15 @@ public void Emit(LogEvent logEvent) } } + /// + /// Emit the provided log event to the sink. + /// + /// The log event to write. + public void Emit(LogEvent logEvent) + { + ((IFileSink)this).EmitOrOverflow(logEvent); + } + /// public void Dispose() { diff --git a/test/Serilog.Sinks.File.Tests/FileLoggerConfigurationExtensionsTests.cs b/test/Serilog.Sinks.File.Tests/FileLoggerConfigurationExtensionsTests.cs index e39cccb..def8073 100644 --- a/test/Serilog.Sinks.File.Tests/FileLoggerConfigurationExtensionsTests.cs +++ b/test/Serilog.Sinks.File.Tests/FileLoggerConfigurationExtensionsTests.cs @@ -78,5 +78,13 @@ public void WhenFlushingToDiskReportedSharedFileSinkCanBeCreatedAndDisposed() Thread.Sleep(TimeSpan.FromSeconds(1)); } } + + [Fact] + public void BufferingIsNotAvailableWhenSharingEnabled() + { + Assert.Throws(() => + new LoggerConfiguration() + .WriteTo.File("logs", buffered: true, shared: true)); + } } } diff --git a/test/Serilog.Sinks.File.Tests/RollingFileSinkTests.cs b/test/Serilog.Sinks.File.Tests/RollingFileSinkTests.cs new file mode 100644 index 0000000..f9348bc --- /dev/null +++ b/test/Serilog.Sinks.File.Tests/RollingFileSinkTests.cs @@ -0,0 +1,150 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using Xunit; +using Serilog.Events; +using Serilog.Sinks.File.Tests.Support; +using Serilog.Configuration; + +namespace Serilog.Sinks.File.Tests +{ + public class RollingFileSinkTests + { + [Fact] + public void LogEventsAreEmittedToTheFileNamedAccordingToTheEventTimestamp() + { + TestRollingEventSequence(Some.InformationEvent()); + } + + [Fact] + public void EventsAreWrittenWhenSharingIsEnabled() + { + TestRollingEventSequence( + (pf, wt) => wt.File(pf, shared: true, rollingInterval: RollingInterval.Day), + new[] { Some.InformationEvent() }); + } + + [Fact] + public void EventsAreWrittenWhenBufferingIsEnabled() + { + TestRollingEventSequence( + (pf, wt) => wt.File(pf, buffered: true, rollingInterval: RollingInterval.Day), + new[] { Some.InformationEvent() }); + } + + [Fact] + public void EventsAreWrittenWhenDiskFlushingIsEnabled() + { + // Doesn't test flushing, but ensures we haven't broken basic logging + TestRollingEventSequence( + (pf, wt) => wt.File(pf, flushToDiskInterval: TimeSpan.FromMilliseconds(50), rollingInterval: RollingInterval.Day), + new[] { Some.InformationEvent() }); + } + + [Fact] + public void WhenTheDateChangesTheCorrectFileIsWritten() + { + var e1 = Some.InformationEvent(); + var e2 = Some.InformationEvent(e1.Timestamp.AddDays(1)); + TestRollingEventSequence(e1, e2); + } + + [Fact] + public void WhenRetentionCountIsSetOldFilesAreDeleted() + { + LogEvent e1 = Some.InformationEvent(), + e2 = Some.InformationEvent(e1.Timestamp.AddDays(1)), + e3 = Some.InformationEvent(e2.Timestamp.AddDays(5)); + + TestRollingEventSequence( + (pf, wt) => wt.File(pf, retainedFileCountLimit: 2, rollingInterval: RollingInterval.Day), + new[] { e1, e2, e3 }, + files => + { + Assert.Equal(3, files.Count); + Assert.True(!System.IO.File.Exists(files[0])); + Assert.True(System.IO.File.Exists(files[1])); + Assert.True(System.IO.File.Exists(files[2])); + }); + } + + [Fact] + public void IfTheLogFolderDoesNotExistItWillBeCreated() + { + var fileName = Some.String() + "-{Date}.txt"; + var temp = Some.TempFolderPath(); + var folder = Path.Combine(temp, Guid.NewGuid().ToString()); + var pathFormat = Path.Combine(folder, fileName); + + ILogger log = null; + + try + { + log = new LoggerConfiguration() + .WriteTo.File(pathFormat, retainedFileCountLimit: 3, rollingInterval: RollingInterval.Day) + .CreateLogger(); + + log.Write(Some.InformationEvent()); + + Assert.True(Directory.Exists(folder)); + } + finally + { + var disposable = (IDisposable)log; + if (disposable != null) disposable.Dispose(); + Directory.Delete(temp, true); + } + } + + [Fact] + public void AssemblyVersionIsFixedAt200() + { + var assembly = typeof(FileLoggerConfigurationExtensions).GetTypeInfo().Assembly; + Assert.Equal("2.0.0.0", assembly.GetName().Version.ToString(4)); + } + + static void TestRollingEventSequence(params LogEvent[] events) + { + TestRollingEventSequence( + (pf, wt) => wt.File(pf, retainedFileCountLimit: null, rollingInterval: RollingInterval.Day), + events); + } + + static void TestRollingEventSequence( + Action configureFile, + IEnumerable events, + Action> verifyWritten = null) + { + var fileName = Some.String() + "-{Date}.txt"; + var folder = Some.TempFolderPath(); + var pathFormat = Path.Combine(folder, fileName); + + var config = new LoggerConfiguration(); + configureFile(pathFormat, config.WriteTo); + var log = config.CreateLogger(); + + var verified = new List(); + + try + { + foreach (var @event in events) + { + Clock.SetTestDateTimeNow(@event.Timestamp.DateTime); + log.Write(@event); + + var expected = pathFormat.Replace("{Date}", @event.Timestamp.ToString("yyyyMMdd")); + Assert.True(System.IO.File.Exists(expected)); + + verified.Add(expected); + } + } + finally + { + log.Dispose(); + verifyWritten?.Invoke(verified); + Directory.Delete(folder, true); + } + } + } +} diff --git a/test/Serilog.Sinks.File.Tests/Serilog.Sinks.File.Tests.csproj b/test/Serilog.Sinks.File.Tests/Serilog.Sinks.File.Tests.csproj index e38eb92..ca40b9b 100644 --- a/test/Serilog.Sinks.File.Tests/Serilog.Sinks.File.Tests.csproj +++ b/test/Serilog.Sinks.File.Tests/Serilog.Sinks.File.Tests.csproj @@ -1,26 +1,26 @@ - netcoreapp1.0;net452 + net452;netcoreapp1.0 + true Serilog.Sinks.File.Tests - Serilog.Sinks.File.Tests + ../../assets/Serilog.snk + true + true + Serilog.Sinks.RollingFile.Tests true $(PackageTargetFallback);dnxcore50;portable-net45+win8 1.0.4 - - - - - - - + + + @@ -28,4 +28,9 @@ + + + + + diff --git a/test/Serilog.Sinks.File.Tests/Support/CollectingSink.cs b/test/Serilog.Sinks.File.Tests/Support/CollectingSink.cs new file mode 100644 index 0000000..244ae5c --- /dev/null +++ b/test/Serilog.Sinks.File.Tests/Support/CollectingSink.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; +using System.Linq; +using Serilog.Core; +using Serilog.Events; + +namespace Serilog.Sinks.File.Tests.Support +{ + class CollectingSink : ILogEventSink + { + readonly List _events = new List(); + + public List Events { get { return _events; } } + + public LogEvent SingleEvent { get { return _events.Single(); } } + + public void Emit(LogEvent logEvent) + { + _events.Add(logEvent); + } + } +} diff --git a/test/Serilog.Sinks.File.Tests/Support/DelegateDisposable.cs b/test/Serilog.Sinks.File.Tests/Support/DelegateDisposable.cs new file mode 100644 index 0000000..3ac9974 --- /dev/null +++ b/test/Serilog.Sinks.File.Tests/Support/DelegateDisposable.cs @@ -0,0 +1,24 @@ +using System; + +namespace Serilog.Sinks.File.Tests.Support +{ + public class DelegateDisposable : IDisposable + { + private readonly Action _disposeAction; + private bool _disposed; + + public DelegateDisposable(Action disposeAction) + { + _disposeAction = disposeAction; + } + + public void Dispose() + { + if (_disposed) + return; + + _disposeAction(); + _disposed = true; + } + } +} diff --git a/test/Serilog.Sinks.File.Tests/Support/DelegatingEnricher.cs b/test/Serilog.Sinks.File.Tests/Support/DelegatingEnricher.cs new file mode 100644 index 0000000..0a480fb --- /dev/null +++ b/test/Serilog.Sinks.File.Tests/Support/DelegatingEnricher.cs @@ -0,0 +1,22 @@ +using System; +using Serilog.Core; +using Serilog.Events; + +namespace Serilog.Sinks.File.Tests.Support +{ + class DelegatingEnricher : ILogEventEnricher + { + readonly Action _enrich; + + public DelegatingEnricher(Action enrich) + { + if (enrich == null) throw new ArgumentNullException(nameof(enrich)); + _enrich = enrich; + } + + public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory) + { + _enrich(logEvent, propertyFactory); + } + } +} diff --git a/test/Serilog.Sinks.File.Tests/Support/DelegatingSink.cs b/test/Serilog.Sinks.File.Tests/Support/DelegatingSink.cs new file mode 100644 index 0000000..9d81cc2 --- /dev/null +++ b/test/Serilog.Sinks.File.Tests/Support/DelegatingSink.cs @@ -0,0 +1,33 @@ +using System; +using Serilog.Core; +using Serilog.Events; + +namespace Serilog.Sinks.File.Tests.Support +{ + public class DelegatingSink : ILogEventSink + { + readonly Action _write; + + public DelegatingSink(Action write) + { + if (write == null) throw new ArgumentNullException(nameof(write)); + _write = write; + } + + public void Emit(LogEvent logEvent) + { + _write(logEvent); + } + + public static LogEvent GetLogEvent(Action writeAction) + { + LogEvent result = null; + var l = new LoggerConfiguration() + .WriteTo.Sink(new DelegatingSink(le => result = le)) + .CreateLogger(); + + writeAction(l); + return result; + } + } +} diff --git a/test/Serilog.Sinks.File.Tests/Support/DisposableLogger.cs b/test/Serilog.Sinks.File.Tests/Support/DisposableLogger.cs new file mode 100644 index 0000000..befcbd4 --- /dev/null +++ b/test/Serilog.Sinks.File.Tests/Support/DisposableLogger.cs @@ -0,0 +1,422 @@ +using System; +using System.Collections.Generic; +using Serilog.Core; +using Serilog.Events; + +namespace Serilog.Sinks.File.Tests.Support +{ + public class DisposableLogger : ILogger, IDisposable + { + public bool Disposed { get; set; } + + public void Dispose() + { + Disposed = true; + } + + public ILogger ForContext(ILogEventEnricher enricher) + { + throw new NotImplementedException(); + } + + public ILogger ForContext(IEnumerable enrichers) + { + throw new NotImplementedException(); + } + + public ILogger ForContext(string propertyName, object value, bool destructureObjects = false) + { + throw new NotImplementedException(); + } + + public ILogger ForContext() + { + throw new NotImplementedException(); + } + + public ILogger ForContext(Type source) + { + throw new NotImplementedException(); + } + + public void Write(LogEvent logEvent) + { + throw new NotImplementedException(); + } + + public void Write(LogEventLevel level, string messageTemplate) + { + throw new NotImplementedException(); + } + + public void Write(LogEventLevel level, string messageTemplate, T propertyValue) + { + throw new NotImplementedException(); + } + + public void Write(LogEventLevel level, string messageTemplate, T0 propertyValue0, T1 propertyValue1) + { + throw new NotImplementedException(); + } + + public void Write(LogEventLevel level, string messageTemplate, T0 propertyValue0, T1 propertyValue1, + T2 propertyValue2) + { + throw new NotImplementedException(); + } + + public void Write(LogEventLevel level, string messageTemplate, params object[] propertyValues) + { + throw new NotImplementedException(); + } + + public void Write(LogEventLevel level, Exception exception, string messageTemplate) + { + throw new NotImplementedException(); + } + + public void Write(LogEventLevel level, Exception exception, string messageTemplate, T propertyValue) + { + throw new NotImplementedException(); + } + + public void Write(LogEventLevel level, Exception exception, string messageTemplate, T0 propertyValue0, + T1 propertyValue1) + { + throw new NotImplementedException(); + } + + public void Write(LogEventLevel level, Exception exception, string messageTemplate, T0 propertyValue0, + T1 propertyValue1, T2 propertyValue2) + { + throw new NotImplementedException(); + } + + public void Write(LogEventLevel level, Exception exception, string messageTemplate, params object[] propertyValues) + { + throw new NotImplementedException(); + } + + public bool IsEnabled(LogEventLevel level) + { + throw new NotImplementedException(); + } + + public void Verbose(string messageTemplate) + { + throw new NotImplementedException(); + } + + public void Verbose(string messageTemplate, T propertyValue) + { + throw new NotImplementedException(); + } + + public void Verbose(string messageTemplate, T0 propertyValue0, T1 propertyValue1) + { + throw new NotImplementedException(); + } + + public void Verbose(string messageTemplate, T0 propertyValue0, T1 propertyValue1, T2 propertyValue2) + { + throw new NotImplementedException(); + } + + public void Verbose(string messageTemplate, params object[] propertyValues) + { + throw new NotImplementedException(); + } + + public void Verbose(Exception exception, string messageTemplate) + { + throw new NotImplementedException(); + } + + public void Verbose(Exception exception, string messageTemplate, T propertyValue) + { + throw new NotImplementedException(); + } + + public void Verbose(Exception exception, string messageTemplate, T0 propertyValue0, T1 propertyValue1) + { + throw new NotImplementedException(); + } + + public void Verbose(Exception exception, string messageTemplate, T0 propertyValue0, T1 propertyValue1, + T2 propertyValue2) + { + throw new NotImplementedException(); + } + + public void Verbose(Exception exception, string messageTemplate, params object[] propertyValues) + { + throw new NotImplementedException(); + } + + public void Debug(string messageTemplate) + { + throw new NotImplementedException(); + } + + public void Debug(string messageTemplate, T propertyValue) + { + throw new NotImplementedException(); + } + + public void Debug(string messageTemplate, T0 propertyValue0, T1 propertyValue1) + { + throw new NotImplementedException(); + } + + public void Debug(string messageTemplate, T0 propertyValue0, T1 propertyValue1, T2 propertyValue2) + { + throw new NotImplementedException(); + } + + public void Debug(string messageTemplate, params object[] propertyValues) + { + throw new NotImplementedException(); + } + + public void Debug(Exception exception, string messageTemplate) + { + throw new NotImplementedException(); + } + + public void Debug(Exception exception, string messageTemplate, T propertyValue) + { + throw new NotImplementedException(); + } + + public void Debug(Exception exception, string messageTemplate, T0 propertyValue0, T1 propertyValue1) + { + throw new NotImplementedException(); + } + + public void Debug(Exception exception, string messageTemplate, T0 propertyValue0, T1 propertyValue1, + T2 propertyValue2) + { + throw new NotImplementedException(); + } + + public void Debug(Exception exception, string messageTemplate, params object[] propertyValues) + { + throw new NotImplementedException(); + } + + public void Information(string messageTemplate) + { + throw new NotImplementedException(); + } + + public void Information(string messageTemplate, T propertyValue) + { + throw new NotImplementedException(); + } + + public void Information(string messageTemplate, T0 propertyValue0, T1 propertyValue1) + { + throw new NotImplementedException(); + } + + public void Information(string messageTemplate, T0 propertyValue0, T1 propertyValue1, T2 propertyValue2) + { + throw new NotImplementedException(); + } + + public void Information(string messageTemplate, params object[] propertyValues) + { + throw new NotImplementedException(); + } + + public void Information(Exception exception, string messageTemplate) + { + throw new NotImplementedException(); + } + + public void Information(Exception exception, string messageTemplate, T propertyValue) + { + throw new NotImplementedException(); + } + + public void Information(Exception exception, string messageTemplate, T0 propertyValue0, T1 propertyValue1) + { + throw new NotImplementedException(); + } + + public void Information(Exception exception, string messageTemplate, T0 propertyValue0, T1 propertyValue1, + T2 propertyValue2) + { + throw new NotImplementedException(); + } + + public void Information(Exception exception, string messageTemplate, params object[] propertyValues) + { + throw new NotImplementedException(); + } + + public void Warning(string messageTemplate) + { + throw new NotImplementedException(); + } + + public void Warning(string messageTemplate, T propertyValue) + { + throw new NotImplementedException(); + } + + public void Warning(string messageTemplate, T0 propertyValue0, T1 propertyValue1) + { + throw new NotImplementedException(); + } + + public void Warning(string messageTemplate, T0 propertyValue0, T1 propertyValue1, T2 propertyValue2) + { + throw new NotImplementedException(); + } + + public void Warning(string messageTemplate, params object[] propertyValues) + { + throw new NotImplementedException(); + } + + public void Warning(Exception exception, string messageTemplate) + { + throw new NotImplementedException(); + } + + public void Warning(Exception exception, string messageTemplate, T propertyValue) + { + throw new NotImplementedException(); + } + + public void Warning(Exception exception, string messageTemplate, T0 propertyValue0, T1 propertyValue1) + { + throw new NotImplementedException(); + } + + public void Warning(Exception exception, string messageTemplate, T0 propertyValue0, T1 propertyValue1, + T2 propertyValue2) + { + throw new NotImplementedException(); + } + + public void Warning(Exception exception, string messageTemplate, params object[] propertyValues) + { + throw new NotImplementedException(); + } + + public void Error(string messageTemplate) + { + throw new NotImplementedException(); + } + + public void Error(string messageTemplate, T propertyValue) + { + throw new NotImplementedException(); + } + + public void Error(string messageTemplate, T0 propertyValue0, T1 propertyValue1) + { + throw new NotImplementedException(); + } + + public void Error(string messageTemplate, T0 propertyValue0, T1 propertyValue1, T2 propertyValue2) + { + throw new NotImplementedException(); + } + + public void Error(string messageTemplate, params object[] propertyValues) + { + throw new NotImplementedException(); + } + + public void Error(Exception exception, string messageTemplate) + { + throw new NotImplementedException(); + } + + public void Error(Exception exception, string messageTemplate, T propertyValue) + { + throw new NotImplementedException(); + } + + public void Error(Exception exception, string messageTemplate, T0 propertyValue0, T1 propertyValue1) + { + throw new NotImplementedException(); + } + + public void Error(Exception exception, string messageTemplate, T0 propertyValue0, T1 propertyValue1, + T2 propertyValue2) + { + throw new NotImplementedException(); + } + + public void Error(Exception exception, string messageTemplate, params object[] propertyValues) + { + throw new NotImplementedException(); + } + + public void Fatal(string messageTemplate) + { + throw new NotImplementedException(); + } + + public void Fatal(string messageTemplate, T propertyValue) + { + throw new NotImplementedException(); + } + + public void Fatal(string messageTemplate, T0 propertyValue0, T1 propertyValue1) + { + throw new NotImplementedException(); + } + + public void Fatal(string messageTemplate, T0 propertyValue0, T1 propertyValue1, T2 propertyValue2) + { + throw new NotImplementedException(); + } + + public void Fatal(string messageTemplate, params object[] propertyValues) + { + throw new NotImplementedException(); + } + + public void Fatal(Exception exception, string messageTemplate) + { + throw new NotImplementedException(); + } + + public void Fatal(Exception exception, string messageTemplate, T propertyValue) + { + throw new NotImplementedException(); + } + + public void Fatal(Exception exception, string messageTemplate, T0 propertyValue0, T1 propertyValue1) + { + throw new NotImplementedException(); + } + + public void Fatal(Exception exception, string messageTemplate, T0 propertyValue0, T1 propertyValue1, + T2 propertyValue2) + { + throw new NotImplementedException(); + } + + public void Fatal(Exception exception, string messageTemplate, params object[] propertyValues) + { + throw new NotImplementedException(); + } + + public bool BindMessageTemplate(string messageTemplate, object[] propertyValues, out MessageTemplate parsedTemplate, + out IEnumerable boundProperties) + { + throw new NotImplementedException(); + } + + public bool BindProperty(string propertyName, object value, bool destructureObjects, out LogEventProperty property) + { + throw new NotImplementedException(); + } + } +} diff --git a/test/Serilog.Sinks.File.Tests/Support/DisposeTrackingSink.cs b/test/Serilog.Sinks.File.Tests/Support/DisposeTrackingSink.cs new file mode 100644 index 0000000..29cac56 --- /dev/null +++ b/test/Serilog.Sinks.File.Tests/Support/DisposeTrackingSink.cs @@ -0,0 +1,20 @@ +using System; +using Serilog.Core; +using Serilog.Events; + +namespace Serilog.Sinks.File.Tests.Support +{ + class DisposeTrackingSink : ILogEventSink, IDisposable + { + public bool IsDisposed { get; set; } + + public void Emit(LogEvent logEvent) + { + } + + public void Dispose() + { + IsDisposed = true; + } + } +} diff --git a/test/Serilog.Sinks.File.Tests/Support/Extensions.cs b/test/Serilog.Sinks.File.Tests/Support/Extensions.cs new file mode 100644 index 0000000..a31122d --- /dev/null +++ b/test/Serilog.Sinks.File.Tests/Support/Extensions.cs @@ -0,0 +1,12 @@ +using Serilog.Events; + +namespace Serilog.Sinks.File.Tests.Support +{ + public static class Extensions + { + public static object LiteralValue(this LogEventPropertyValue @this) + { + return ((ScalarValue)@this).Value; + } + } +} diff --git a/test/Serilog.Sinks.File.Tests/Support/Some.cs b/test/Serilog.Sinks.File.Tests/Support/Some.cs index a831492..8ab9cd0 100644 --- a/test/Serilog.Sinks.File.Tests/Support/Some.cs +++ b/test/Serilog.Sinks.File.Tests/Support/Some.cs @@ -1,24 +1,87 @@ using System; -using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; using Serilog.Events; -using Xunit.Sdk; +using Serilog.Parsing; -namespace Serilog.Tests.Support +namespace Serilog.Sinks.File.Tests.Support { static class Some { - public static LogEvent LogEvent(string messageTemplate, params object[] propertyValues) - { - var log = new LoggerConfiguration().CreateLogger(); - MessageTemplate template; - IEnumerable properties; -#pragma warning disable Serilog004 // Constant MessageTemplate verifier - if (!log.BindMessageTemplate(messageTemplate, propertyValues, out template, out properties)) -#pragma warning restore Serilog004 // Constant MessageTemplate verifier - { - throw new XunitException("Template could not be bound."); - } - return new LogEvent(DateTimeOffset.Now, LogEventLevel.Information, null, template, properties); + static int _counter; + + public static int Int() + { + return Interlocked.Increment(ref _counter); + } + + public static decimal Decimal() + { + return Int() + 0.123m; + } + + public static string String(string tag = null) + { + return (tag ?? "") + "__" + Int(); + } + + public static TimeSpan TimeSpan() + { + return System.TimeSpan.FromMinutes(Int()); + } + + public static DateTime Instant() + { + return new DateTime(2012, 10, 28) + TimeSpan(); + } + + public static DateTimeOffset OffsetInstant() + { + return new DateTimeOffset(Instant()); + } + + public static LogEvent LogEvent(DateTimeOffset? timestamp = null, LogEventLevel level = LogEventLevel.Information) + { + return new LogEvent(timestamp ?? OffsetInstant(), level, + null, MessageTemplate(), Enumerable.Empty()); + } + + public static LogEvent InformationEvent(DateTimeOffset? timestamp = null) + { + return LogEvent(timestamp, LogEventLevel.Information); + } + + public static LogEvent DebugEvent(DateTimeOffset? timestamp = null) + { + return LogEvent(timestamp, LogEventLevel.Debug); + } + + public static LogEventProperty LogEventProperty() + { + return new LogEventProperty(String(), new ScalarValue(Int())); + } + + public static string NonexistentTempFilePath() + { + return Path.Combine(Path.GetTempPath(), Guid.NewGuid() + ".txt"); + } + + public static string TempFilePath() + { + return Path.GetTempFileName(); + } + + public static string TempFolderPath() + { + var dir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + Directory.CreateDirectory(dir); + return dir; + } + + public static MessageTemplate MessageTemplate() + { + return new MessageTemplateParser().Parse(String()); } } } diff --git a/test/Serilog.Sinks.File.Tests/TemplatedPathRollerTests.cs b/test/Serilog.Sinks.File.Tests/TemplatedPathRollerTests.cs new file mode 100644 index 0000000..d003736 --- /dev/null +++ b/test/Serilog.Sinks.File.Tests/TemplatedPathRollerTests.cs @@ -0,0 +1,126 @@ +using System; +using System.IO; +using System.Linq; +using Xunit; + +namespace Serilog.Sinks.RollingFile.Tests +{ + public class TemplatedPathRollerTests + { + [Fact] + public void SpecifierCannotBeProvidedInDirectory() + { + var ex = Assert.Throws(() => new TemplatedPathRoller("{Date}\\log.txt")); + Assert.True(ex.Message.Contains("directory")); + } + + [Fact] + public void TheLogFileIncludesDateToken() + { + var roller = new TemplatedPathRoller("Logs\\log.{Date}.txt"); + var now = new DateTime(2013, 7, 14, 3, 24, 9, 980); + string path; + roller.GetLogFilePath(now, 0, out path); + AssertEqualAbsolute("Logs\\log.20130714.txt", path); + } + + [Fact] + public void ANonZeroIncrementIsIncludedAndPadded() + { + var roller = new TemplatedPathRoller("Logs\\log.{Date}.txt"); + var now = new DateTime(2013, 7, 14, 3, 24, 9, 980); + string path; + roller.GetLogFilePath(now, 12, out path); + AssertEqualAbsolute("Logs\\log.20130714_012.txt", path); + } + + static void AssertEqualAbsolute(string path1, string path2) + { + var abs1 = Path.GetFullPath(path1); + var abs2 = Path.GetFullPath(path2); + Assert.Equal(abs1, abs2); + } + + [Fact] + public void TheRollerReturnsTheLogFileDirectory() + { + var roller = new TemplatedPathRoller("Logs\\log.{Date}.txt"); + AssertEqualAbsolute("Logs", roller.LogFileDirectory); + } + + [Fact] + public void IfNoTokenIsSpecifiedDashFollowedByTheDateIsImplied() + { + var roller = new TemplatedPathRoller("Logs\\log.txt"); + var now = new DateTime(2013, 7, 14, 3, 24, 9, 980); + string path; + roller.GetLogFilePath(now, 0, out path); + AssertEqualAbsolute("Logs\\log-20130714.txt", path); + } + + [Fact] + public void TheLogFileIsNotRequiredToIncludeAnExtension() + { + var roller = new TemplatedPathRoller("Logs\\log-{Date}"); + var now = new DateTime(2013, 7, 14, 3, 24, 9, 980); + string path; + roller.GetLogFilePath(now, 0, out path); + AssertEqualAbsolute("Logs\\log-20130714", path); + } + + [Fact] + public void TheLogFileIsNotRequiredToIncludeADirectory() + { + var roller = new TemplatedPathRoller("log-{Date}"); + var now = new DateTime(2013, 7, 14, 3, 24, 9, 980); + string path; + roller.GetLogFilePath(now, 0, out path); + AssertEqualAbsolute("log-20130714", path); + } + + [Fact] + public void MatchingExcludesSimilarButNonmatchingFiles() + { + var roller = new TemplatedPathRoller("log-{Date}.txt"); + const string similar1 = "log-0.txt"; + const string similar2 = "log-helloyou.txt"; + var matched = roller.SelectMatches(new[] { similar1, similar2 }); + Assert.Equal(0, matched.Count()); + } + + [Theory] + [InlineData("Logs\\log-{Date}.txt")] + [InlineData("Logs\\log-{Hour}.txt")] + [InlineData("Logs\\log-{HalfHour}.txt")] + public void TheDirectorSearchPatternUsesWildcardInPlaceOfDate(string template) + { + var roller = new TemplatedPathRoller(template); + Assert.Equal("log-*.txt", roller.DirectorySearchPattern); + } + + [Theory] + [InlineData("log-{Date}.txt", "log-20131210.txt", "log-20131210_031.txt")] + [InlineData("log-{Hour}.txt", "log-2013121013.txt", "log-2013121013_031.txt")] + [InlineData("log-{HalfHour}.txt", "log-201312100100.txt", "log-201312100230_031.txt")] + public void MatchingSelectsFiles(string template, string zeroth, string thirtyFirst) + { + var roller = new TemplatedPathRoller(template); + var matched = roller.SelectMatches(new[] { zeroth, thirtyFirst }).ToArray(); + Assert.Equal(2, matched.Count()); + Assert.Equal(0, matched[0].SequenceNumber); + Assert.Equal(31, matched[1].SequenceNumber); + } + + [Theory] + [InlineData("log-{Date}.txt", "log-20150101.txt", "log-20141231.txt")] + [InlineData("log-{Hour}.txt", "log-2015010110.txt", "log-2015010109.txt")] + [InlineData("log-{HalfHour}.txt", "log-201501011400.txt", "log-201501011330.txt")] + public void MatchingParsesSubstitutions(string template, string newer, string older) + { + var roller = new TemplatedPathRoller(template); + var matched = roller.SelectMatches(new[] { older, newer }).OrderByDescending(m => m.DateTime).Select(m => m.Filename).ToArray(); + Assert.Equal(new[] { newer, older }, matched); + } + } +} + From f04c5683902c03d00f2f6905bbfb9adf03b15bcb Mon Sep 17 00:00:00 2001 From: Nicholas Blumhardt Date: Fri, 13 Oct 2017 15:23:53 +1000 Subject: [PATCH 14/69] Tests pass --- example/Sample/Program.cs | 2 +- .../FileLoggerConfigurationExtensions.cs | 46 ++++++------ .../FileLoggerConfigurationExtensionsTests.cs | 2 +- .../Serilog.Sinks.File.Tests/FileSinkTests.cs | 12 ++-- .../RollingFileSinkTests.cs | 4 +- .../SharedFileSinkTests.cs | 3 +- test/Serilog.Sinks.File.Tests/Support/Some.cs | 16 +++++ .../TemplatedPathRollerTests.cs | 70 +++++++------------ 8 files changed, 75 insertions(+), 80 deletions(-) diff --git a/example/Sample/Program.cs b/example/Sample/Program.cs index 98bf4c5..89e6c95 100644 --- a/example/Sample/Program.cs +++ b/example/Sample/Program.cs @@ -14,7 +14,7 @@ public static void Main(string[] args) var sw = System.Diagnostics.Stopwatch.StartNew(); Log.Logger = new LoggerConfiguration() - .WriteTo.File("log.txt", fileSizeLimitBytes: 1000000, rollingInterval: RollingInterval.Day, rollOnFileSizeLimit: true) + .WriteTo.File("log.txt") .CreateLogger(); for (var i = 0; i < 1000000; ++i) diff --git a/src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs b/src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs index d6c645d..5ecf95a 100644 --- a/src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs +++ b/src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs @@ -69,12 +69,10 @@ public static LoggerConfiguration File( bool shared, TimeSpan? flushToDiskInterval) { - 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, flushToDiskInterval: flushToDiskInterval); + // ReSharper disable once RedundantArgumentDefaultValue + return File(sinkConfiguration, path, restrictedToMinimumLevel, outputTemplate, formatProvider, fileSizeLimitBytes, + levelSwitch, buffered, shared, flushToDiskInterval, RollingInterval.Infinite, false, + null, null); } /// @@ -112,7 +110,9 @@ public static LoggerConfiguration File( bool shared, TimeSpan? flushToDiskInterval) { - return ConfigureFile(sinkConfiguration.Sink, formatter, path, restrictedToMinimumLevel, fileSizeLimitBytes, levelSwitch, buffered: buffered, shared: shared, flushToDiskInterval: flushToDiskInterval); + // ReSharper disable once RedundantArgumentDefaultValue + return File(sinkConfiguration, formatter, path, restrictedToMinimumLevel, fileSizeLimitBytes, levelSwitch, + buffered, shared, flushToDiskInterval, RollingInterval.Infinite, false, null, null); } /// @@ -163,7 +163,9 @@ public static LoggerConfiguration File( 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, flushToDiskInterval: flushToDiskInterval, rollingInterval: rollingInterval, rollOnFileSizeLimit: rollOnFileSizeLimit); + return File(sinkConfiguration, formatter, path, restrictedToMinimumLevel, fileSizeLimitBytes, + levelSwitch, buffered, shared, flushToDiskInterval, + rollingInterval, rollOnFileSizeLimit, retainedFileCountLimit, encoding); } /// @@ -211,8 +213,7 @@ public static LoggerConfiguration File( Encoding encoding = null) { return ConfigureFile(sinkConfiguration.Sink, formatter, path, restrictedToMinimumLevel, fileSizeLimitBytes, levelSwitch, - buffered: buffered, shared: shared, flushToDiskInterval: flushToDiskInterval, rollingInterval: rollingInterval, - rollOnFileSizeLimit: rollOnFileSizeLimit, retainedFileCountLimit: retainedFileCountLimit, encoding: encoding); + buffered, false, shared, flushToDiskInterval, encoding, rollingInterval, rollOnFileSizeLimit, retainedFileCountLimit); } /// @@ -268,24 +269,25 @@ public static LoggerConfiguration File( LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum, LoggingLevelSwitch levelSwitch = null) { - return ConfigureFile(sinkConfiguration.Sink, formatter, path, restrictedToMinimumLevel, null, levelSwitch, false, true); + return ConfigureFile(sinkConfiguration.Sink, formatter, path, restrictedToMinimumLevel, null, levelSwitch, false, true, + false, null, null, RollingInterval.Infinite, false, null); } 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, - TimeSpan? flushToDiskInterval = null, - Encoding encoding = null, - RollingInterval rollingInterval = RollingInterval.Infinite, - bool rollOnFileSizeLimit = false, - int? retainedFileCountLimit = DefaultRetainedFileCountLimit) + LogEventLevel restrictedToMinimumLevel, + long? fileSizeLimitBytes, + LoggingLevelSwitch levelSwitch, + bool buffered, + bool propagateExceptions, + bool shared, + TimeSpan? flushToDiskInterval, + Encoding encoding, + RollingInterval rollingInterval, + bool rollOnFileSizeLimit, + int? retainedFileCountLimit) { if (addSink == null) throw new ArgumentNullException(nameof(addSink)); if (formatter == null) throw new ArgumentNullException(nameof(formatter)); diff --git a/test/Serilog.Sinks.File.Tests/FileLoggerConfigurationExtensionsTests.cs b/test/Serilog.Sinks.File.Tests/FileLoggerConfigurationExtensionsTests.cs index def8073..0515655 100644 --- a/test/Serilog.Sinks.File.Tests/FileLoggerConfigurationExtensionsTests.cs +++ b/test/Serilog.Sinks.File.Tests/FileLoggerConfigurationExtensionsTests.cs @@ -5,7 +5,7 @@ using Xunit; using System.IO; -namespace Serilog.Tests +namespace Serilog.Sinks.File.Tests { public class FileLoggerConfigurationExtensionsTests { diff --git a/test/Serilog.Sinks.File.Tests/FileSinkTests.cs b/test/Serilog.Sinks.File.Tests/FileSinkTests.cs index 0c0a13d..ea9a5d4 100644 --- a/test/Serilog.Sinks.File.Tests/FileSinkTests.cs +++ b/test/Serilog.Sinks.File.Tests/FileSinkTests.cs @@ -1,11 +1,11 @@ -using System; -using System.IO; +using System.IO; using Xunit; using Serilog.Formatting.Json; using Serilog.Sinks.File.Tests.Support; using Serilog.Tests.Support; using System.Text; -using Serilog.Tests; + +#pragma warning disable 618 namespace Serilog.Sinks.File.Tests { @@ -116,11 +116,10 @@ public void WhenLimitIsSpecifiedAndEncodingHasPreambleDataIsCorrectlyAppendedToF [Fact] public void WhenLimitIsNotSpecifiedAndEncodingHasPreambleDataIsCorrectlyAppendedToFileSink() { - long? maxBytes = null; var encoding = Encoding.UTF8; Assert.True(encoding.GetPreamble().Length > 0); - WriteTwoEventsAndCheckOutputFileLength(maxBytes, encoding); + WriteTwoEventsAndCheckOutputFileLength(null, encoding); } [Fact] @@ -136,11 +135,10 @@ public void WhenLimitIsSpecifiedAndEncodingHasNoPreambleDataIsCorrectlyAppendedT [Fact] public void WhenLimitIsNotSpecifiedAndEncodingHasNoPreambleDataIsCorrectlyAppendedToFileSink() { - long? maxBytes = null; var encoding = new UTF8Encoding(false); Assert.Equal(0, encoding.GetPreamble().Length); - WriteTwoEventsAndCheckOutputFileLength(maxBytes, encoding); + WriteTwoEventsAndCheckOutputFileLength(null, encoding); } static void WriteTwoEventsAndCheckOutputFileLength(long? maxBytes, Encoding encoding) diff --git a/test/Serilog.Sinks.File.Tests/RollingFileSinkTests.cs b/test/Serilog.Sinks.File.Tests/RollingFileSinkTests.cs index f9348bc..43c31ca 100644 --- a/test/Serilog.Sinks.File.Tests/RollingFileSinkTests.cs +++ b/test/Serilog.Sinks.File.Tests/RollingFileSinkTests.cs @@ -116,7 +116,7 @@ static void TestRollingEventSequence( IEnumerable events, Action> verifyWritten = null) { - var fileName = Some.String() + "-{Date}.txt"; + var fileName = Some.String() + "-.txt"; var folder = Some.TempFolderPath(); var pathFormat = Path.Combine(folder, fileName); @@ -133,7 +133,7 @@ static void TestRollingEventSequence( Clock.SetTestDateTimeNow(@event.Timestamp.DateTime); log.Write(@event); - var expected = pathFormat.Replace("{Date}", @event.Timestamp.ToString("yyyyMMdd")); + var expected = pathFormat.Replace(".txt", @event.Timestamp.ToString("yyyyMMdd") + ".txt"); Assert.True(System.IO.File.Exists(expected)); verified.Add(expected); diff --git a/test/Serilog.Sinks.File.Tests/SharedFileSinkTests.cs b/test/Serilog.Sinks.File.Tests/SharedFileSinkTests.cs index f63eac1..565be9b 100644 --- a/test/Serilog.Sinks.File.Tests/SharedFileSinkTests.cs +++ b/test/Serilog.Sinks.File.Tests/SharedFileSinkTests.cs @@ -2,7 +2,8 @@ using Xunit; using Serilog.Formatting.Json; using Serilog.Sinks.File.Tests.Support; -using Serilog.Tests.Support; + +#pragma warning disable 618 namespace Serilog.Sinks.File.Tests { diff --git a/test/Serilog.Sinks.File.Tests/Support/Some.cs b/test/Serilog.Sinks.File.Tests/Support/Some.cs index 8ab9cd0..2d29d4d 100644 --- a/test/Serilog.Sinks.File.Tests/Support/Some.cs +++ b/test/Serilog.Sinks.File.Tests/Support/Some.cs @@ -1,9 +1,11 @@ using System; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading; using Serilog.Events; using Serilog.Parsing; +using Xunit.Sdk; namespace Serilog.Sinks.File.Tests.Support { @@ -41,6 +43,20 @@ public static DateTimeOffset OffsetInstant() return new DateTimeOffset(Instant()); } + public static LogEvent LogEvent(string messageTemplate, params object[] propertyValues) + { + var log = new LoggerConfiguration().CreateLogger(); + MessageTemplate template; + IEnumerable properties; +#pragma warning disable Serilog004 // Constant MessageTemplate verifier + if (!log.BindMessageTemplate(messageTemplate, propertyValues, out template, out properties)) +#pragma warning restore Serilog004 // Constant MessageTemplate verifier + { + throw new XunitException("Template could not be bound."); + } + return new LogEvent(DateTimeOffset.Now, LogEventLevel.Information, null, template, properties); + } + public static LogEvent LogEvent(DateTimeOffset? timestamp = null, LogEventLevel level = LogEventLevel.Information) { return new LogEvent(timestamp ?? OffsetInstant(), level, diff --git a/test/Serilog.Sinks.File.Tests/TemplatedPathRollerTests.cs b/test/Serilog.Sinks.File.Tests/TemplatedPathRollerTests.cs index d003736..ffb4f2e 100644 --- a/test/Serilog.Sinks.File.Tests/TemplatedPathRollerTests.cs +++ b/test/Serilog.Sinks.File.Tests/TemplatedPathRollerTests.cs @@ -3,31 +3,24 @@ using System.Linq; using Xunit; -namespace Serilog.Sinks.RollingFile.Tests +namespace Serilog.Sinks.File.Tests { - public class TemplatedPathRollerTests + public class PathRollerTests { - [Fact] - public void SpecifierCannotBeProvidedInDirectory() - { - var ex = Assert.Throws(() => new TemplatedPathRoller("{Date}\\log.txt")); - Assert.True(ex.Message.Contains("directory")); - } - [Fact] public void TheLogFileIncludesDateToken() { - var roller = new TemplatedPathRoller("Logs\\log.{Date}.txt"); + var roller = new PathRoller("Logs\\log..txt", RollingInterval.Day); var now = new DateTime(2013, 7, 14, 3, 24, 9, 980); string path; - roller.GetLogFilePath(now, 0, out path); + roller.GetLogFilePath(now, null, out path); AssertEqualAbsolute("Logs\\log.20130714.txt", path); } [Fact] public void ANonZeroIncrementIsIncludedAndPadded() { - var roller = new TemplatedPathRoller("Logs\\log.{Date}.txt"); + var roller = new PathRoller("Logs\\log..txt", RollingInterval.Day); var now = new DateTime(2013, 7, 14, 3, 24, 9, 980); string path; roller.GetLogFilePath(now, 12, out path); @@ -44,80 +37,65 @@ static void AssertEqualAbsolute(string path1, string path2) [Fact] public void TheRollerReturnsTheLogFileDirectory() { - var roller = new TemplatedPathRoller("Logs\\log.{Date}.txt"); + var roller = new PathRoller("Logs\\log..txt", RollingInterval.Day); AssertEqualAbsolute("Logs", roller.LogFileDirectory); } - [Fact] - public void IfNoTokenIsSpecifiedDashFollowedByTheDateIsImplied() - { - var roller = new TemplatedPathRoller("Logs\\log.txt"); - var now = new DateTime(2013, 7, 14, 3, 24, 9, 980); - string path; - roller.GetLogFilePath(now, 0, out path); - AssertEqualAbsolute("Logs\\log-20130714.txt", path); - } - [Fact] public void TheLogFileIsNotRequiredToIncludeAnExtension() { - var roller = new TemplatedPathRoller("Logs\\log-{Date}"); + var roller = new PathRoller("Logs\\log-", RollingInterval.Day); var now = new DateTime(2013, 7, 14, 3, 24, 9, 980); string path; - roller.GetLogFilePath(now, 0, out path); + roller.GetLogFilePath(now, null, out path); AssertEqualAbsolute("Logs\\log-20130714", path); } [Fact] public void TheLogFileIsNotRequiredToIncludeADirectory() { - var roller = new TemplatedPathRoller("log-{Date}"); + var roller = new PathRoller("log-", RollingInterval.Day); var now = new DateTime(2013, 7, 14, 3, 24, 9, 980); string path; - roller.GetLogFilePath(now, 0, out path); + roller.GetLogFilePath(now, null, out path); AssertEqualAbsolute("log-20130714", path); } [Fact] public void MatchingExcludesSimilarButNonmatchingFiles() { - var roller = new TemplatedPathRoller("log-{Date}.txt"); + var roller = new PathRoller("log-.txt", RollingInterval.Day); const string similar1 = "log-0.txt"; const string similar2 = "log-helloyou.txt"; var matched = roller.SelectMatches(new[] { similar1, similar2 }); Assert.Equal(0, matched.Count()); } - [Theory] - [InlineData("Logs\\log-{Date}.txt")] - [InlineData("Logs\\log-{Hour}.txt")] - [InlineData("Logs\\log-{HalfHour}.txt")] - public void TheDirectorSearchPatternUsesWildcardInPlaceOfDate(string template) + [Fact] + public void TheDirectorSearchPatternUsesWildcardInPlaceOfDate() { - var roller = new TemplatedPathRoller(template); + var roller = new PathRoller("Logs\\log-.txt", RollingInterval.Day); Assert.Equal("log-*.txt", roller.DirectorySearchPattern); } [Theory] - [InlineData("log-{Date}.txt", "log-20131210.txt", "log-20131210_031.txt")] - [InlineData("log-{Hour}.txt", "log-2013121013.txt", "log-2013121013_031.txt")] - [InlineData("log-{HalfHour}.txt", "log-201312100100.txt", "log-201312100230_031.txt")] - public void MatchingSelectsFiles(string template, string zeroth, string thirtyFirst) + [InlineData("log-.txt", "log-20131210.txt", "log-20131210_031.txt", RollingInterval.Day)] + [InlineData("log-.txt", "log-2013121013.txt", "log-2013121013_031.txt", RollingInterval.Hour)] + public void MatchingSelectsFiles(string template, string zeroth, string thirtyFirst, RollingInterval interval) { - var roller = new TemplatedPathRoller(template); + var roller = new PathRoller(template, interval); var matched = roller.SelectMatches(new[] { zeroth, thirtyFirst }).ToArray(); - Assert.Equal(2, matched.Count()); - Assert.Equal(0, matched[0].SequenceNumber); + Assert.Equal(2, matched.Length); + Assert.Equal(null, matched[0].SequenceNumber); Assert.Equal(31, matched[1].SequenceNumber); } [Theory] - [InlineData("log-{Date}.txt", "log-20150101.txt", "log-20141231.txt")] - [InlineData("log-{Hour}.txt", "log-2015010110.txt", "log-2015010109.txt")] - [InlineData("log-{HalfHour}.txt", "log-201501011400.txt", "log-201501011330.txt")] - public void MatchingParsesSubstitutions(string template, string newer, string older) + [InlineData("log-.txt", "log-20150101.txt", "log-20141231.txt", RollingInterval.Day)] + [InlineData("log-.txt", "log-2015010110.txt", "log-2015010109.txt", RollingInterval.Hour)] + public void MatchingParsesSubstitutions(string template, string newer, string older, RollingInterval interval) { - var roller = new TemplatedPathRoller(template); + var roller = new PathRoller(template, interval); var matched = roller.SelectMatches(new[] { older, newer }).OrderByDescending(m => m.DateTime).Select(m => m.Filename).ToArray(); Assert.Equal(new[] { newer, older }, matched); } From 937cdfe65c7b6305afa0cd69af67205af10c8d3f Mon Sep 17 00:00:00 2001 From: Nicholas Blumhardt Date: Fri, 13 Oct 2017 15:43:29 +1000 Subject: [PATCH 15/69] Updated build scripts --- Build.ps1 | 31 +++++++++++++++++++++++++------ appveyor.yml | 6 +----- 2 files changed, 26 insertions(+), 11 deletions(-) diff --git a/Build.ps1 b/Build.ps1 index 0515652..ee4117d 100644 --- a/Build.ps1 +++ b/Build.ps1 @@ -1,27 +1,46 @@ +echo "build: Build started" + Push-Location $PSScriptRoot -if(Test-Path .\artifacts) { Remove-Item .\artifacts -Force -Recurse } +if(Test-Path .\artifacts) { + echo "build: Cleaning .\artifacts" + Remove-Item .\artifacts -Force -Recurse +} & dotnet restore --no-cache $branch = @{ $true = $env:APPVEYOR_REPO_BRANCH; $false = $(git symbolic-ref --short -q HEAD) }[$env:APPVEYOR_REPO_BRANCH -ne $NULL]; $revision = @{ $true = "{0:00000}" -f [convert]::ToInt32("0" + $env:APPVEYOR_BUILD_NUMBER, 10); $false = "local" }[$env:APPVEYOR_BUILD_NUMBER -ne $NULL]; -$suffix = @{ $true = ""; $false = "$branch-$revision"}[$branch -eq "master" -and $revision -ne "local"] +$suffix = @{ $true = ""; $false = "$($branch.Substring(0, [math]::Min(10,$branch.Length)))-$revision"}[$branch -eq "master" -and $revision -ne "local"] +$commitHash = $(git rev-parse --short HEAD) +$buildSuffix = @{ $true = "$($suffix)-$($commitHash)"; $false = "$($branch)-$($commitHash)" }[$suffix -ne ""] + +echo "build: Package version suffix is $suffix" +echo "build: Build version suffix is $buildSuffix" -foreach ($src in ls src/Serilog.*) { +foreach ($src in ls src/*) { Push-Location $src - & dotnet pack -c Release -o ..\..\.\artifacts --version-suffix=$suffix + echo "build: Packaging project in $src" + + & dotnet build -c Release --version-suffix=$buildSuffix + if ($suffix) { + & dotnet pack -c Release --include-source -o ..\..\artifacts --version-suffix=$suffix --no-build + } else { + & dotnet pack -c Release --include-source -o ..\..\artifacts --no-build + } if($LASTEXITCODE -ne 0) { exit 1 } Pop-Location } -foreach ($test in ls test/Serilog.*.Tests) { +foreach ($test in ls test/*.Tests) { Push-Location $test + echo "build: Testing project in $test" + & dotnet test -c Release - if($LASTEXITCODE -ne 0) { exit 2 } + if($LASTEXITCODE -ne 0) { exit 3 } Pop-Location } diff --git a/appveyor.yml b/appveyor.yml index 19d0d28..7e5f9b5 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,13 +1,9 @@ version: '{build}' skip_tags: true -image: Visual Studio 2015 +image: Visual Studio 2017 configuration: Release install: - ps: mkdir -Force ".\build\" | Out-Null - - ps: Invoke-WebRequest "https://raw.githubusercontent.com/dotnet/cli/rel/1.0.0-preview2/scripts/obtain/dotnet-install.ps1" -OutFile ".\build\installcli.ps1" - - ps: $env:DOTNET_INSTALL_DIR = "$pwd\.dotnetcli" - - ps: '& .\build\installcli.ps1 -InstallDir "$env:DOTNET_INSTALL_DIR" -NoPath -Version 1.0.0-preview2-003121' - - ps: $env:Path = "$env:DOTNET_INSTALL_DIR;$env:Path" build_script: - ps: ./Build.ps1 test: off From b2f2c26558742434aaca0908f80b6de898a6e0f1 Mon Sep 17 00:00:00 2001 From: Nicholas Blumhardt Date: Sat, 14 Oct 2017 07:22:27 +1000 Subject: [PATCH 16/69] Attempt to fix Travis build by blindly importing the scripts and config from serilog/serilog --- .travis.yml | 52 +++++++--------------------------------------------- build.sh | 15 +++++++-------- 2 files changed, 14 insertions(+), 53 deletions(-) diff --git a/.travis.yml b/.travis.yml index e9daee9..9427d5d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,49 +1,11 @@ language: csharp -#dotnet cli require Ubuntu 14.04 -sudo: required -dist: trusty - -#dotnet cli require OSX 10.10 -osx_image: xcode7.1 - -addons: - apt: - packages: - - gettext - - libcurl4-openssl-dev - - libicu-dev - - libssl-dev - - libunwind8 - - zlib1g - -os: - - linux - -env: - global: - - DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true - - TMP: /tmp - - matrix: - - CLI_VERSION=1.0.0-preview2-003121 - - CLI_VERSION=Latest - matrix: - allow_failures: - - env: CLI_VERSION=Latest - -before_install: - - if test "$TRAVIS_OS_NAME" == "osx"; then brew update; brew install openssl; brew link --force openssl; fi - # Download script to install dotnet cli - - if test "$CLI_OBTAIN_URL" == ""; then export CLI_OBTAIN_URL="https://raw.githubusercontent.com/dotnet/cli/rel/1.0.0-preview2/scripts/obtain/dotnet-install.sh"; fi - - curl -L --create-dirs $CLI_OBTAIN_URL -o ./scripts/obtain/install.sh - - find ./scripts -name "*.sh" -exec chmod +x {} \; - - export DOTNET_INSTALL_DIR="$PWD/.dotnetcli" - # use bash to workaround bug https://github.com/dotnet/cli/issues/1725 - - sudo bash ./scripts/obtain/install.sh --channel "preview" --version "$CLI_VERSION" --install-dir "$DOTNET_INSTALL_DIR" --no-path - # add dotnet to PATH - - export PATH="$DOTNET_INSTALL_DIR:$PATH" - + include: + - os: linux + dist: trusty + sudo: required + dotnet: 1.0.4 + group: edge script: - - ./build.sh \ No newline at end of file + - ./build.sh diff --git a/build.sh b/build.sh index 39408c4..931a99d 100755 --- a/build.sh +++ b/build.sh @@ -1,12 +1,11 @@ #!/bin/bash -dotnet restore --no-cache -for path in src/*/project.json; do - dirname="$(dirname "${path}")" - dotnet build ${dirname} -f netstandard1.3 -c Release +dotnet --info +dotnet restore + +for path in src/**/*.csproj; do + dotnet build -f netstandard1.3 -c Release ${path} done -for path in test/*.Tests/project.json; do - dirname="$(dirname "${path}")" - dotnet build ${dirname} -f netcoreapp1.0 -c Release - dotnet test ${dirname} -f netcoreapp1.0 -c Release +for path in test/*.Tests/*.csproj; do + dotnet test -f netcoreapp1.0 -c Release ${path} done From bf2060d1d73b58193ce08585d0f643f0861c7a51 Mon Sep 17 00:00:00 2001 From: Nicholas Blumhardt Date: Mon, 16 Oct 2017 11:27:36 +1000 Subject: [PATCH 17/69] Review feedback --- src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs | 6 +++--- src/Serilog.Sinks.File/Sinks/File/RollingFileSink.cs | 2 +- src/Serilog.Sinks.File/Sinks/File/RollingLogFile.cs | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs b/src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs index 5ecf95a..5cb19e9 100644 --- a/src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs +++ b/src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs @@ -1,4 +1,4 @@ -// Copyright 2013-2016 Serilog Contributors +// Copyright 2013-2017 Serilog Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -136,7 +136,7 @@ public static LoggerConfiguration File( /// If provided, a full disk flush will be performed periodically at the specified interval. /// The interval at which logging will roll over to a new file. /// If true, a new file will be created when the file size limit is reached. Filenames - /// will have a number appended in the format _NNNNN, with the first filename given no number. + /// will have a number appended in the format _NNN, with the first filename given no number. /// The maximum number of log files that will be retained, /// including the current log file. For unlimited retention, pass null. The default is 31. /// Character encoding used to write the text file. The default is UTF-8 without BOM. @@ -191,7 +191,7 @@ public static LoggerConfiguration File( /// If provided, a full disk flush will be performed periodically at the specified interval. /// The interval at which logging will roll over to a new file. /// If true, a new file will be created when the file size limit is reached. Filenames - /// will have a number appended in the format _NNNNN, with the first filename given no number. + /// will have a number appended in the format _NNN, with the first filename given no number. /// The maximum number of log files that will be retained, /// including the current log file. For unlimited retention, pass null. The default is 31. /// Character encoding used to write the text file. The default is UTF-8 without BOM. diff --git a/src/Serilog.Sinks.File/Sinks/File/RollingFileSink.cs b/src/Serilog.Sinks.File/Sinks/File/RollingFileSink.cs index cd91bc1..644176f 100644 --- a/src/Serilog.Sinks.File/Sinks/File/RollingFileSink.cs +++ b/src/Serilog.Sinks.File/Sinks/File/RollingFileSink.cs @@ -1,4 +1,4 @@ -// Copyright 2013-2016 Serilog Contributors +// Copyright 2013-2017 Serilog Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/src/Serilog.Sinks.File/Sinks/File/RollingLogFile.cs b/src/Serilog.Sinks.File/Sinks/File/RollingLogFile.cs index 3850e7d..be64c4e 100644 --- a/src/Serilog.Sinks.File/Sinks/File/RollingLogFile.cs +++ b/src/Serilog.Sinks.File/Sinks/File/RollingLogFile.cs @@ -1,4 +1,4 @@ -// Copyright 2013-2016 Serilog Contributors +// Copyright 2013-2017 Serilog Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. From 2c166196aeec99afaf44bf82108ef88097d742b7 Mon Sep 17 00:00:00 2001 From: Nicholas Blumhardt Date: Mon, 16 Oct 2017 12:04:48 +1000 Subject: [PATCH 18/69] Travis .NET tooling version --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 9427d5d..6a880da 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,7 +5,7 @@ matrix: - os: linux dist: trusty sudo: required - dotnet: 1.0.4 + dotnet: 2.0.0 group: edge script: - ./build.sh From ce534dcf4f221cf3577aa085d6f3bdcf783df636 Mon Sep 17 00:00:00 2001 From: Nicholas Blumhardt Date: Mon, 16 Oct 2017 12:25:07 +1000 Subject: [PATCH 19/69] Run .NET Core 2.0 as test target on Travis, since that's the tooling version required to build --- build.sh | 2 +- test/Serilog.Sinks.File.Tests/Serilog.Sinks.File.Tests.csproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build.sh b/build.sh index 931a99d..4e46f40 100755 --- a/build.sh +++ b/build.sh @@ -7,5 +7,5 @@ for path in src/**/*.csproj; do done for path in test/*.Tests/*.csproj; do - dotnet test -f netcoreapp1.0 -c Release ${path} + dotnet test -f netcoreapp2.0 -c Release ${path} done diff --git a/test/Serilog.Sinks.File.Tests/Serilog.Sinks.File.Tests.csproj b/test/Serilog.Sinks.File.Tests/Serilog.Sinks.File.Tests.csproj index ca40b9b..18ddce5 100644 --- a/test/Serilog.Sinks.File.Tests/Serilog.Sinks.File.Tests.csproj +++ b/test/Serilog.Sinks.File.Tests/Serilog.Sinks.File.Tests.csproj @@ -1,7 +1,7 @@ - net452;netcoreapp1.0 + net452;netcoreapp2.0 true Serilog.Sinks.File.Tests ../../assets/Serilog.snk From c36a0ea395eb1b140f52dc2a8e82039acce740dc Mon Sep 17 00:00:00 2001 From: Nicholas Blumhardt Date: Mon, 16 Oct 2017 12:37:22 +1000 Subject: [PATCH 20/69] Temp folder location on Linux --- test/Serilog.Sinks.File.Tests/Serilog.Sinks.File.Tests.csproj | 2 +- test/Serilog.Sinks.File.Tests/Support/TempFolder.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test/Serilog.Sinks.File.Tests/Serilog.Sinks.File.Tests.csproj b/test/Serilog.Sinks.File.Tests/Serilog.Sinks.File.Tests.csproj index 18ddce5..3491e32 100644 --- a/test/Serilog.Sinks.File.Tests/Serilog.Sinks.File.Tests.csproj +++ b/test/Serilog.Sinks.File.Tests/Serilog.Sinks.File.Tests.csproj @@ -1,7 +1,7 @@ - net452;netcoreapp2.0 + net452;netcoreapp1.0;netcoreapp2.0 true Serilog.Sinks.File.Tests ../../assets/Serilog.snk diff --git a/test/Serilog.Sinks.File.Tests/Support/TempFolder.cs b/test/Serilog.Sinks.File.Tests/Support/TempFolder.cs index f809c05..cea0b3f 100644 --- a/test/Serilog.Sinks.File.Tests/Support/TempFolder.cs +++ b/test/Serilog.Sinks.File.Tests/Support/TempFolder.cs @@ -14,7 +14,7 @@ class TempFolder : IDisposable public TempFolder(string name) { _tempFolder = System.IO.Path.Combine( - Environment.GetEnvironmentVariable("TMP"), + Environment.GetEnvironmentVariable("TMP") ?? Environment.GetEnvironmentVariable("TMPDIR") ?? "/tmp", "Serilog.Sinks.File.Tests", Session.ToString("n"), name); From 6988cb4ee9f46567005d0871a8d50cbbc88f719d Mon Sep 17 00:00:00 2001 From: Nicholas Blumhardt Date: Mon, 16 Oct 2017 12:55:56 +1000 Subject: [PATCH 21/69] Use test paths that are happy on Linux --- .../RollingFileSinkTests.cs | 30 +++++++++++++++++-- .../Support/TempFolder.cs | 4 +-- .../TemplatedPathRollerTests.cs | 10 +++---- 3 files changed, 34 insertions(+), 10 deletions(-) diff --git a/test/Serilog.Sinks.File.Tests/RollingFileSinkTests.cs b/test/Serilog.Sinks.File.Tests/RollingFileSinkTests.cs index 43c31ca..b41dd71 100644 --- a/test/Serilog.Sinks.File.Tests/RollingFileSinkTests.cs +++ b/test/Serilog.Sinks.File.Tests/RollingFileSinkTests.cs @@ -54,12 +54,12 @@ public void WhenTheDateChangesTheCorrectFileIsWritten() public void WhenRetentionCountIsSetOldFilesAreDeleted() { LogEvent e1 = Some.InformationEvent(), - e2 = Some.InformationEvent(e1.Timestamp.AddDays(1)), - e3 = Some.InformationEvent(e2.Timestamp.AddDays(5)); + e2 = Some.InformationEvent(e1.Timestamp.AddDays(1)), + e3 = Some.InformationEvent(e2.Timestamp.AddDays(5)); TestRollingEventSequence( (pf, wt) => wt.File(pf, retainedFileCountLimit: 2, rollingInterval: RollingInterval.Day), - new[] { e1, e2, e3 }, + new[] {e1, e2, e3}, files => { Assert.Equal(3, files.Count); @@ -69,6 +69,30 @@ public void WhenRetentionCountIsSetOldFilesAreDeleted() }); } + [Fact] + public void WhenSizeLimitIsBreachedNewFilesCreated() + { + var fileName = Some.String() + ".txt"; + using (var temp = new TempFolder()) + using (var log = new LoggerConfiguration() + .WriteTo.File(Path.Combine(temp.Path, fileName), rollOnFileSizeLimit: true, fileSizeLimitBytes: 1) + .CreateLogger()) + { + LogEvent e1 = Some.InformationEvent(), + e2 = Some.InformationEvent(e1.Timestamp), + e3 = Some.InformationEvent(e1.Timestamp); + + log.Write(e1); log.Write(e2); log.Write(e3); + + var files = Directory.GetFiles(temp.Path); + + Assert.Equal(3, files.Length); + Assert.False(files[0].Contains("_000.txt")); + Assert.True(files[1].Contains("_001.txt")); + Assert.True(files[2].Contains("_002.txt")); + } + } + [Fact] public void IfTheLogFolderDoesNotExistItWillBeCreated() { diff --git a/test/Serilog.Sinks.File.Tests/Support/TempFolder.cs b/test/Serilog.Sinks.File.Tests/Support/TempFolder.cs index cea0b3f..7ff90f8 100644 --- a/test/Serilog.Sinks.File.Tests/Support/TempFolder.cs +++ b/test/Serilog.Sinks.File.Tests/Support/TempFolder.cs @@ -11,13 +11,13 @@ class TempFolder : IDisposable readonly string _tempFolder; - public TempFolder(string name) + public TempFolder(string name = null) { _tempFolder = System.IO.Path.Combine( Environment.GetEnvironmentVariable("TMP") ?? Environment.GetEnvironmentVariable("TMPDIR") ?? "/tmp", "Serilog.Sinks.File.Tests", Session.ToString("n"), - name); + name ?? Guid.NewGuid().ToString("n")); Directory.CreateDirectory(_tempFolder); } diff --git a/test/Serilog.Sinks.File.Tests/TemplatedPathRollerTests.cs b/test/Serilog.Sinks.File.Tests/TemplatedPathRollerTests.cs index ffb4f2e..85685e2 100644 --- a/test/Serilog.Sinks.File.Tests/TemplatedPathRollerTests.cs +++ b/test/Serilog.Sinks.File.Tests/TemplatedPathRollerTests.cs @@ -10,21 +10,21 @@ public class PathRollerTests [Fact] public void TheLogFileIncludesDateToken() { - var roller = new PathRoller("Logs\\log..txt", RollingInterval.Day); + var roller = new PathRoller("Logs\\log-.txt", RollingInterval.Day); var now = new DateTime(2013, 7, 14, 3, 24, 9, 980); string path; roller.GetLogFilePath(now, null, out path); - AssertEqualAbsolute("Logs\\log.20130714.txt", path); + AssertEqualAbsolute("Logs\\log-20130714.txt", path); } [Fact] public void ANonZeroIncrementIsIncludedAndPadded() { - var roller = new PathRoller("Logs\\log..txt", RollingInterval.Day); + var roller = new PathRoller("Logs\\log-.txt", RollingInterval.Day); var now = new DateTime(2013, 7, 14, 3, 24, 9, 980); string path; roller.GetLogFilePath(now, 12, out path); - AssertEqualAbsolute("Logs\\log.20130714_012.txt", path); + AssertEqualAbsolute("Logs\\log-20130714_012.txt", path); } static void AssertEqualAbsolute(string path1, string path2) @@ -37,7 +37,7 @@ static void AssertEqualAbsolute(string path1, string path2) [Fact] public void TheRollerReturnsTheLogFileDirectory() { - var roller = new PathRoller("Logs\\log..txt", RollingInterval.Day); + var roller = new PathRoller("Logs\\log-.txt", RollingInterval.Day); AssertEqualAbsolute("Logs", roller.LogFileDirectory); } From 0b514f212aed8b5af76fd748ddc9a89d2892aa98 Mon Sep 17 00:00:00 2001 From: Nicholas Blumhardt Date: Mon, 16 Oct 2017 14:35:27 +1000 Subject: [PATCH 22/69] Proper path concatenation in tests --- .../TemplatedPathRollerTests.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/test/Serilog.Sinks.File.Tests/TemplatedPathRollerTests.cs b/test/Serilog.Sinks.File.Tests/TemplatedPathRollerTests.cs index 85685e2..5e1b015 100644 --- a/test/Serilog.Sinks.File.Tests/TemplatedPathRollerTests.cs +++ b/test/Serilog.Sinks.File.Tests/TemplatedPathRollerTests.cs @@ -10,21 +10,21 @@ public class PathRollerTests [Fact] public void TheLogFileIncludesDateToken() { - var roller = new PathRoller("Logs\\log-.txt", RollingInterval.Day); + var roller = new PathRoller(Path.Combine("Logs", "log-.txt"), RollingInterval.Day); var now = new DateTime(2013, 7, 14, 3, 24, 9, 980); string path; roller.GetLogFilePath(now, null, out path); - AssertEqualAbsolute("Logs\\log-20130714.txt", path); + AssertEqualAbsolute(Path.Combine("Logs", "log-20130714.txt"), path); } [Fact] public void ANonZeroIncrementIsIncludedAndPadded() { - var roller = new PathRoller("Logs\\log-.txt", RollingInterval.Day); + var roller = new PathRoller(Path.Combine("Logs", "log-.txt"), RollingInterval.Day); var now = new DateTime(2013, 7, 14, 3, 24, 9, 980); string path; roller.GetLogFilePath(now, 12, out path); - AssertEqualAbsolute("Logs\\log-20130714_012.txt", path); + AssertEqualAbsolute(Path.Combine("Logs", "log-20130714_012.txt"), path); } static void AssertEqualAbsolute(string path1, string path2) @@ -37,18 +37,18 @@ static void AssertEqualAbsolute(string path1, string path2) [Fact] public void TheRollerReturnsTheLogFileDirectory() { - var roller = new PathRoller("Logs\\log-.txt", RollingInterval.Day); + var roller = new PathRoller(Path.Combine("Logs", "log-.txt"), RollingInterval.Day); AssertEqualAbsolute("Logs", roller.LogFileDirectory); } [Fact] public void TheLogFileIsNotRequiredToIncludeAnExtension() { - var roller = new PathRoller("Logs\\log-", RollingInterval.Day); + var roller = new PathRoller(Path.Combine("Logs", "log-"), RollingInterval.Day); var now = new DateTime(2013, 7, 14, 3, 24, 9, 980); string path; roller.GetLogFilePath(now, null, out path); - AssertEqualAbsolute("Logs\\log-20130714", path); + AssertEqualAbsolute(Path.Combine("Logs", "log-20130714"), path); } [Fact] @@ -74,7 +74,7 @@ public void MatchingExcludesSimilarButNonmatchingFiles() [Fact] public void TheDirectorSearchPatternUsesWildcardInPlaceOfDate() { - var roller = new PathRoller("Logs\\log-.txt", RollingInterval.Day); + var roller = new PathRoller(Path.Combine("Logs", "log-.txt"), RollingInterval.Day); Assert.Equal("log-*.txt", roller.DirectorySearchPattern); } From e17e37ed600ed720a72edb980f3ff47feb415fc3 Mon Sep 17 00:00:00 2001 From: Nicholas Blumhardt Date: Mon, 16 Oct 2017 15:06:07 +1000 Subject: [PATCH 23/69] Directory listing ordering --- test/Serilog.Sinks.File.Tests/RollingFileSinkTests.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/Serilog.Sinks.File.Tests/RollingFileSinkTests.cs b/test/Serilog.Sinks.File.Tests/RollingFileSinkTests.cs index b41dd71..cf2dd0c 100644 --- a/test/Serilog.Sinks.File.Tests/RollingFileSinkTests.cs +++ b/test/Serilog.Sinks.File.Tests/RollingFileSinkTests.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Reflection; using Xunit; using Serilog.Events; @@ -84,7 +85,7 @@ public void WhenSizeLimitIsBreachedNewFilesCreated() log.Write(e1); log.Write(e2); log.Write(e3); - var files = Directory.GetFiles(temp.Path); + var files = Directory.GetFiles(temp.Path).OrderBy(p => p).ToArray(); Assert.Equal(3, files.Length); Assert.False(files[0].Contains("_000.txt")); From 638a9325199e2bab7f9240e0b7cb89abcccc8f4f Mon Sep 17 00:00:00 2001 From: Nicholas Blumhardt Date: Mon, 16 Oct 2017 15:16:21 +1000 Subject: [PATCH 24/69] Return some more useful info on test failure --- test/Serilog.Sinks.File.Tests/RollingFileSinkTests.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/Serilog.Sinks.File.Tests/RollingFileSinkTests.cs b/test/Serilog.Sinks.File.Tests/RollingFileSinkTests.cs index cf2dd0c..b5c0c2e 100644 --- a/test/Serilog.Sinks.File.Tests/RollingFileSinkTests.cs +++ b/test/Serilog.Sinks.File.Tests/RollingFileSinkTests.cs @@ -88,9 +88,9 @@ public void WhenSizeLimitIsBreachedNewFilesCreated() var files = Directory.GetFiles(temp.Path).OrderBy(p => p).ToArray(); Assert.Equal(3, files.Length); - Assert.False(files[0].Contains("_000.txt")); - Assert.True(files[1].Contains("_001.txt")); - Assert.True(files[2].Contains("_002.txt")); + Assert.False(files[0].Contains("_000.txt"), files[0]); + Assert.True(files[1].Contains("_001.txt"), files[1]); + Assert.True(files[2].Contains("_002.txt"), files[2]); } } From 6f207426fea30dae53791486ed1fa8754719e02e Mon Sep 17 00:00:00 2001 From: Nicholas Blumhardt Date: Mon, 16 Oct 2017 15:17:18 +1000 Subject: [PATCH 25/69] Might as well be more exact on this assertion --- test/Serilog.Sinks.File.Tests/RollingFileSinkTests.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/Serilog.Sinks.File.Tests/RollingFileSinkTests.cs b/test/Serilog.Sinks.File.Tests/RollingFileSinkTests.cs index b5c0c2e..55b1737 100644 --- a/test/Serilog.Sinks.File.Tests/RollingFileSinkTests.cs +++ b/test/Serilog.Sinks.File.Tests/RollingFileSinkTests.cs @@ -88,9 +88,9 @@ public void WhenSizeLimitIsBreachedNewFilesCreated() var files = Directory.GetFiles(temp.Path).OrderBy(p => p).ToArray(); Assert.Equal(3, files.Length); - Assert.False(files[0].Contains("_000.txt"), files[0]); - Assert.True(files[1].Contains("_001.txt"), files[1]); - Assert.True(files[2].Contains("_002.txt"), files[2]); + Assert.False(files[0].EndsWith("_000.txt"), files[0]); + Assert.True(files[1].EndsWith("_001.txt"), files[1]); + Assert.True(files[2].EndsWith("_002.txt"), files[2]); } } From 4605f0c03261d2700b79dc097e096a0155099b1e Mon Sep 17 00:00:00 2001 From: Nicholas Blumhardt Date: Mon, 16 Oct 2017 15:30:33 +1000 Subject: [PATCH 26/69] More test twiddling --- test/Serilog.Sinks.File.Tests/RollingFileSinkTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/Serilog.Sinks.File.Tests/RollingFileSinkTests.cs b/test/Serilog.Sinks.File.Tests/RollingFileSinkTests.cs index 55b1737..a671198 100644 --- a/test/Serilog.Sinks.File.Tests/RollingFileSinkTests.cs +++ b/test/Serilog.Sinks.File.Tests/RollingFileSinkTests.cs @@ -88,7 +88,7 @@ public void WhenSizeLimitIsBreachedNewFilesCreated() var files = Directory.GetFiles(temp.Path).OrderBy(p => p).ToArray(); Assert.Equal(3, files.Length); - Assert.False(files[0].EndsWith("_000.txt"), files[0]); + Assert.True(files[0].EndsWith(fileName), files[0]); Assert.True(files[1].EndsWith("_001.txt"), files[1]); Assert.True(files[2].EndsWith("_002.txt"), files[2]); } From 20423c3775516968e2d31a26ae0b61ae78607589 Mon Sep 17 00:00:00 2001 From: Nicholas Blumhardt Date: Mon, 16 Oct 2017 16:19:12 +1000 Subject: [PATCH 27/69] Surprisingly, _ sorts before . by default on Ubuntu 16.04 --- test/Serilog.Sinks.File.Tests/RollingFileSinkTests.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/Serilog.Sinks.File.Tests/RollingFileSinkTests.cs b/test/Serilog.Sinks.File.Tests/RollingFileSinkTests.cs index a671198..3efe3f9 100644 --- a/test/Serilog.Sinks.File.Tests/RollingFileSinkTests.cs +++ b/test/Serilog.Sinks.File.Tests/RollingFileSinkTests.cs @@ -85,7 +85,9 @@ public void WhenSizeLimitIsBreachedNewFilesCreated() log.Write(e1); log.Write(e2); log.Write(e3); - var files = Directory.GetFiles(temp.Path).OrderBy(p => p).ToArray(); + var files = Directory.GetFiles(temp.Path) + .OrderBy(p => p, StringComparer.OrdinalIgnoreCase) + .ToArray(); Assert.Equal(3, files.Length); Assert.True(files[0].EndsWith(fileName), files[0]); From b183ea4ae4429c1d52c64b63e6d4eb27ee86aa87 Mon Sep 17 00:00:00 2001 From: Nicholas Blumhardt Date: Mon, 16 Oct 2017 16:23:33 +1000 Subject: [PATCH 28/69] Use VB.NET indexing for months and days ;-) --- .../Sinks/File/RollingIntervalExtensions.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Serilog.Sinks.File/Sinks/File/RollingIntervalExtensions.cs b/src/Serilog.Sinks.File/Sinks/File/RollingIntervalExtensions.cs index 364153a..2c9e2fd 100644 --- a/src/Serilog.Sinks.File/Sinks/File/RollingIntervalExtensions.cs +++ b/src/Serilog.Sinks.File/Sinks/File/RollingIntervalExtensions.cs @@ -46,9 +46,9 @@ public static string GetFormat(this RollingInterval interval) case RollingInterval.Infinite: return null; case RollingInterval.Year: - return new DateTime(instant.Year, 0, 0, 0, 0, 0, instant.Kind); + return new DateTime(instant.Year, 1, 1, 0, 0, 0, instant.Kind); case RollingInterval.Month: - return new DateTime(instant.Year, instant.Month, 0, 0, 0, 0, instant.Kind); + return new DateTime(instant.Year, instant.Month, 1, 0, 0, 0, instant.Kind); case RollingInterval.Day: return new DateTime(instant.Year, instant.Month, instant.Day, 0, 0, 0, instant.Kind); case RollingInterval.Hour: From 483e4e8dd61e228ec6fd95ccd04cd1d67ba28add Mon Sep 17 00:00:00 2001 From: Nicholas Blumhardt Date: Mon, 16 Oct 2017 16:38:57 +1000 Subject: [PATCH 29/69] Tests for interval/checkpoint calculation --- .../RollingIntervalExtensionsTests.cs | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 test/Serilog.Sinks.File.Tests/RollingIntervalExtensionsTests.cs diff --git a/test/Serilog.Sinks.File.Tests/RollingIntervalExtensionsTests.cs b/test/Serilog.Sinks.File.Tests/RollingIntervalExtensionsTests.cs new file mode 100644 index 0000000..2d97d1b --- /dev/null +++ b/test/Serilog.Sinks.File.Tests/RollingIntervalExtensionsTests.cs @@ -0,0 +1,34 @@ +using System; +using Xunit; + +namespace Serilog.Sinks.File.Tests +{ + public class RollingIntervalExtensionsTests + { + public static object[][] IntervalInstantCurrentNextCheckpoint => new[] + { + new object[]{ RollingInterval.Infinite, new DateTime(2018, 01, 01), null, null }, + new object[]{ RollingInterval.Year, new DateTime(2018, 01, 01), new DateTime(2018, 01, 01), new DateTime(2019, 01, 01) }, + new object[]{ RollingInterval.Year, new DateTime(2018, 06, 01), new DateTime(2018, 01, 01), new DateTime(2019, 01, 01) }, + new object[]{ RollingInterval.Month, new DateTime(2018, 01, 01), new DateTime(2018, 01, 01), new DateTime(2018, 02, 01) }, + new object[]{ RollingInterval.Month, new DateTime(2018, 01, 14), new DateTime(2018, 01, 01), new DateTime(2018, 02, 01) }, + new object[]{ RollingInterval.Day, new DateTime(2018, 01, 01), new DateTime(2018, 01, 01), new DateTime(2018, 01, 02) }, + new object[]{ RollingInterval.Day, new DateTime(2018, 01, 01, 12, 0, 0), new DateTime(2018, 01, 01), new DateTime(2018, 01, 02) }, + new object[]{ RollingInterval.Hour, new DateTime(2018, 01, 01, 0, 0, 0), new DateTime(2018, 01, 01), new DateTime(2018, 01, 01, 1, 0, 0) }, + new object[]{ RollingInterval.Hour, new DateTime(2018, 01, 01, 0, 30, 0), new DateTime(2018, 01, 01), new DateTime(2018, 01, 01, 1, 0, 0) }, + new object[]{ RollingInterval.Minute, new DateTime(2018, 01, 01, 0, 0, 0), new DateTime(2018, 01, 01), new DateTime(2018, 01, 01, 0, 1, 0) }, + new object[]{ RollingInterval.Minute, new DateTime(2018, 01, 01, 0, 0, 30), new DateTime(2018, 01, 01), new DateTime(2018, 01, 01, 0, 1, 0) } + }; + + [Theory] + [MemberData(nameof(IntervalInstantCurrentNextCheckpoint))] + public void NextIntervalTests(RollingInterval interval, DateTime instant, DateTime? currentCheckpoint, DateTime? nextCheckpoint) + { + var current = interval.GetCurrentCheckpoint(instant); + Assert.Equal(currentCheckpoint, current); + + var next = interval.GetNextCheckpoint(instant); + Assert.Equal(nextCheckpoint, next); + } + } +} From 6ad69c1ede74a4d1c787dc99cb85cfd38006e668 Mon Sep 17 00:00:00 2001 From: Nicholas Blumhardt Date: Mon, 16 Oct 2017 17:15:42 +1000 Subject: [PATCH 30/69] Bring the README quality into line with the rolling file sink's --- README.md | 166 +++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 153 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 7a6f5d4..683110c 100644 --- a/README.md +++ b/README.md @@ -1,49 +1,189 @@ # Serilog.Sinks.File [![Build status](https://ci.appveyor.com/api/projects/status/hh9gymy0n6tne46j?svg=true)](https://ci.appveyor.com/project/serilog/serilog-sinks-file) [![Travis build](https://travis-ci.org/serilog/serilog-sinks-file.svg)](https://travis-ci.org/serilog/serilog-sinks-file) [![NuGet Version](http://img.shields.io/nuget/v/Serilog.Sinks.File.svg?style=flat)](https://www.nuget.org/packages/Serilog.Sinks.File/) [![Documentation](https://img.shields.io/badge/docs-wiki-yellow.svg)](https://github.com/serilog/serilog/wiki) [![Join the chat at https://gitter.im/serilog/serilog](https://img.shields.io/gitter/room/serilog/serilog.svg)](https://gitter.im/serilog/serilog) -Writes [Serilog](https://serilog.net) events to a text file. +Writes [Serilog](https://serilog.net) events to one or more text files. + +### Getting started + +Install the [Serilog.Sinks.File](https://nuget.org/serilog/serilog-sinks-file) package from NuGet: + +```powershell +Install-Package Serilog.Sinks.File -Pre +``` + +To configure the sink in C# code, call `WriteTo.File()` during logger configuration: ```csharp var log = new LoggerConfiguration() - .WriteTo.File("log.txt") + .WriteTo.File("log.txt", rollingInterval: RollingInterval.Day) .CreateLogger(); ``` +This will append the time period to the filename, creating a file set like: + +``` +log20180631.txt +log20180701.txt +log20180702.txt +``` + +> **Important**: By default, only one process may write to a log file at a given time. See _Shared log files_ below for information on multi-process sharing. + +### Limits + To avoid bringing down apps with runaway disk usage the file sink **limits file size to 1GB by default**. The limit can be increased or removed using the `fileSizeLimitBytes` parameter. ```csharp .WriteTo.File("log.txt", fileSizeLimitBytes: null) ``` -> **Important:** By default only one process may use a log file at a given time. See _Shared log files_ below if multi-process logging is required. +For the same reason, only **the most recent 31 files** are retained by default (i.e. one long month). To change or remove this limit, pass the `retainedFileCountLimit` parameter. + +```csharp + .WriteTo.RollingFile("log.txt", rollingInterval: RollingInterval.Day, retainedFileCountLimit: null) +``` + +### Rolling policies + +To create a log file per day or other time period, specify a `rollingInterval` as shown in the examples above. + +To roll when the file reaches `fileSizeLimitBytes`, specify `rollOnFileSizeLimit`: + +```csharp + .WriteTo.File("log.txt", rollOnFileSizeLimit: true) +``` + +This will create a file set like: + +``` +log.txt +log_001.txt +log_002.txt +``` + +Specifying both `rollingInterval` and `rollOnFileSizeLimit` will cause both policies to be applied, while specifying neither will result in all events being written to a single file. + +Old files will be cleaned up as per `retainedFileCountLimit` - the default is 31. + +### XML `` configuration + +To use the file sink with the [Serilog.Settings.AppSettings](https://github.com/serilog/serilog-settings-appsettings) package, first install that package if you haven't already done so: + +```powershell +Install-Package Serilog.Settings.AppSettings +``` + +Instead of configuring the logger in code, call `ReadFrom.AppSettings()`: + +```csharp +var log = new LoggerConfiguration() + .ReadFrom.AppSettings() + .CreateLogger(); +``` + +In your application's `App.config` or `Web.config` file, specify the file sink assembly and required path format under the `` node: + +```xml + + + + +``` + +The parameters that can be set through the `serilog:write-to:File` keys are the method parameters accepted by the `WriteTo.File()` configuration method. This means, for example, that the `fileSizeLimitBytes` parameter can be set with: + +```xml + +``` + +Omitting the `value` will set the parameter to `null`: -### `` configuration +```xml + +``` -The sink can be configured in XML [app-settings format](https://github.com/serilog/serilog/wiki/AppSettings) if the _Serilog.Settings.AppSettings_ package is in use: +In XML and JSON configuration formats, environment variables can be used in setting values. This means, for instance, that the log file path can be based on `TMP` or `APPDATA`: ```xml - - - + ``` -### JSON formatting +### JSON `appsettings.json` configuration + +To use the file sink with _Microsoft.Extensions.Configuration_, for example with ASP.NET Core or .NET Core, use the [Serilog.Settings.Configuration](https://github.com/serilog/serilog-settings-configuration) package. First install that package if you have not already done so: + +```powershell +Install-Package Serilog.Settings.Configuration +``` -To emit JSON, rather than plain text, a formatter can be specified: +Instead of configuring the file directly in code, call `ReadFrom.Configuration()`: ```csharp - .WriteTo.File(new JsonFormatter(), "log.txt") +var configuration = new ConfigurationBuilder() + .AddJsonFile("appsettings.json") + .Build(); + +var logger = new LoggerConfiguration() + .ReadFrom.Configuration(configuration) + .CreateLogger(); +``` + +In your `appsettings.json` file, under the `Serilog` node, : + +```json +{ + "Serilog": { + "WriteTo": [ + { "Name": "File", "Args": { "path": "log.txt", "rollingInterval": "Day" } } + ] + } +} ``` -To configure an alternative formatter in XML ``, specify the formatter's assembly-qualified type name as the setting `value`. +See the XML `` example above for a discussion of available `Args` options. + +### Controlling event formatting + +The file sink creates events in a fixed text format by default: + +``` +2018-07-06 09:02:17.148 +10:00 [INF] HTTP GET / responded 200 in 1994 ms +``` + +The format is controlled using an _output template_, which the file configuration method accepts as an `outputTemplate` parameter. + +The default format above corresponds to an output template like: + +```csharp + .WriteTo.File("log.txt", + outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{u3}] {Message:lj}{NewLine}{Exception}") +``` + +##### JSON event formatting + +To write events to the file in an alternative format such as JSON, pass an `ITextFormatter` as the first argument: + +```csharp + .WriteTo.File(new JsonFormatter(), "log.txt") +``` ### Shared log files -Multiple processes can concurrently write to the same log file if the `shared` parameter is set to `true`: +To enable multi-process shared log files, set `shared` to `true`: ```csharp .WriteTo.File("log.txt", shared: true) ``` +### Auditing + +The file sink can operate as an audit file through `AuditTo`: + +```csharp + .AuditTo.File("audit.txt") +``` + +Only a limited subset of configuration options are currently available in this mode. + ### 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. From af88bceba9ef183e0e9520606541066c2fdf6d89 Mon Sep 17 00:00:00 2001 From: Nicholas Blumhardt Date: Mon, 16 Oct 2017 17:21:24 +1000 Subject: [PATCH 31/69] Might as well not specify -Pre in the installation instructions, unlikely this will be found in the short term --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 683110c..e04fcc4 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ Writes [Serilog](https://serilog.net) events to one or more text files. Install the [Serilog.Sinks.File](https://nuget.org/serilog/serilog-sinks-file) package from NuGet: ```powershell -Install-Package Serilog.Sinks.File -Pre +Install-Package Serilog.Sinks.File ``` To configure the sink in C# code, call `WriteTo.File()` during logger configuration: From 3e0510ccd57777f8641267fed7b17040d6ed2f2c Mon Sep 17 00:00:00 2001 From: Nicholas Blumhardt Date: Tue, 17 Oct 2017 09:44:30 +1000 Subject: [PATCH 32/69] Use Serilog.Formatting.Compact in JSON example [Skip CI] --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index e04fcc4..87596a7 100644 --- a/README.md +++ b/README.md @@ -160,10 +160,11 @@ The default format above corresponds to an output template like: ##### JSON event formatting -To write events to the file in an alternative format such as JSON, pass an `ITextFormatter` as the first argument: +To write events to the file in an alternative format such as [JSON](https://github.com/serilog/serilog-formatting-compact), pass an `ITextFormatter` as the first argument: ```csharp - .WriteTo.File(new JsonFormatter(), "log.txt") + // Install-Package Serilog.Formatting.Compact + .WriteTo.File(new CompactJsonFormatter(), "log.txt") ``` ### Shared log files From f197183bc3ae53d765c549151a6a41da3e01ac96 Mon Sep 17 00:00:00 2001 From: Nicholas Blumhardt Date: Mon, 30 Oct 2017 11:46:27 +1000 Subject: [PATCH 33/69] Dev version bump [Skip CI] --- src/Serilog.Sinks.File/Serilog.Sinks.File.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Serilog.Sinks.File/Serilog.Sinks.File.csproj b/src/Serilog.Sinks.File/Serilog.Sinks.File.csproj index 569e5b0..320ce07 100644 --- a/src/Serilog.Sinks.File/Serilog.Sinks.File.csproj +++ b/src/Serilog.Sinks.File/Serilog.Sinks.File.csproj @@ -2,7 +2,7 @@ Write Serilog events to text files in plain or JSON format. - 4.0.0 + 4.0.1 Serilog Contributors net45;netstandard1.3 true From 55c5aa2bc3d9d4c766293de23dc9d51895ec9501 Mon Sep 17 00:00:00 2001 From: Nicholas Blumhardt Date: Tue, 31 Oct 2017 07:48:37 +1000 Subject: [PATCH 34/69] Renamed `pathFormat` to correct `path` in example - #38 [Skip CI] --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 87596a7..9ee3bd0 100644 --- a/README.md +++ b/README.md @@ -86,7 +86,7 @@ In your application's `App.config` or `Web.config` file, specify the file sink a - + ``` The parameters that can be set through the `serilog:write-to:File` keys are the method parameters accepted by the `WriteTo.File()` configuration method. This means, for example, that the `fileSizeLimitBytes` parameter can be set with: From b8bb2cc556987fa065b0035d651bec8071561fb2 Mon Sep 17 00:00:00 2001 From: Thibaud Desodt Date: Wed, 29 Nov 2017 11:00:45 +0100 Subject: [PATCH 35/69] Fix link to nuget package page --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9ee3bd0..08f0050 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Writes [Serilog](https://serilog.net) events to one or more text files. ### Getting started -Install the [Serilog.Sinks.File](https://nuget.org/serilog/serilog-sinks-file) package from NuGet: +Install the [Serilog.Sinks.File](https://www.nuget.org/packages/Serilog.Sinks.File/) package from NuGet: ```powershell Install-Package Serilog.Sinks.File From 0c2e485ed04f0e27e88316571c75a3ae2c01e96b Mon Sep 17 00:00:00 2001 From: Ben Brandt Date: Wed, 21 Feb 2018 12:23:31 -0600 Subject: [PATCH 36/69] Docs: Fix level output in default format --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 08f0050..153a82d 100644 --- a/README.md +++ b/README.md @@ -155,7 +155,7 @@ The default format above corresponds to an output template like: ```csharp .WriteTo.File("log.txt", - outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{u3}] {Message:lj}{NewLine}{Exception}") + outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception}") ``` ##### JSON event formatting From 56b3753c9477dcc207392e0817932ad949c92d28 Mon Sep 17 00:00:00 2001 From: Nicholas Blumhardt Date: Thu, 22 Feb 2018 13:40:33 +1000 Subject: [PATCH 37/69] Try to fit the example output template within the margins [Skip CI] --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 153a82d..df54699 100644 --- a/README.md +++ b/README.md @@ -154,8 +154,8 @@ The format is controlled using an _output template_, which the file configuratio The default format above corresponds to an output template like: ```csharp - .WriteTo.File("log.txt", - outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception}") + .WriteTo.File("log.txt", + outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception}") ``` ##### JSON event formatting From 9fe9c0d765d7a566e4355ee9aa9608dc391aa2ea Mon Sep 17 00:00:00 2001 From: Thibaud Desodt Date: Mon, 5 Mar 2018 09:09:47 +0100 Subject: [PATCH 38/69] Fix remaining reference to RollingFile sink --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index df54699..9e4f19f 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ To avoid bringing down apps with runaway disk usage the file sink **limits file For the same reason, only **the most recent 31 files** are retained by default (i.e. one long month). To change or remove this limit, pass the `retainedFileCountLimit` parameter. ```csharp - .WriteTo.RollingFile("log.txt", rollingInterval: RollingInterval.Day, retainedFileCountLimit: null) + .WriteTo.File("log.txt", rollingInterval: RollingInterval.Day, retainedFileCountLimit: null) ``` ### Rolling policies From f18cb0e8aaf54a77c85cfd392028c94218888e4c Mon Sep 17 00:00:00 2001 From: Matthew Erbs Date: Wed, 9 May 2018 15:53:18 +1000 Subject: [PATCH 39/69] New NuGet API Key --- appveyor.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index 7e5f9b5..71f05e3 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -12,7 +12,7 @@ artifacts: deploy: - provider: NuGet api_key: - secure: nvZ/z+pMS91b3kG4DgfES5AcmwwGoBYQxr9kp4XiJHj25SAlgdIxFx++1N0lFH2x + secure: bd9z4P73oltOXudAjPehwp9iDKsPtC+HbgshOrSgoyQKr5xVK+bxJQngrDJkHdY8 skip_symbols: true on: branch: /^(master|dev)$/ From 37a2bfeaebbb9c7cc1a86758ac56dd4787c8b358 Mon Sep 17 00:00:00 2001 From: Maxime Rouiller Date: Fri, 21 Sep 2018 14:39:06 -0400 Subject: [PATCH 40/69] fixing repository url --- src/Serilog.Sinks.File/Serilog.Sinks.File.csproj | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Serilog.Sinks.File/Serilog.Sinks.File.csproj b/src/Serilog.Sinks.File/Serilog.Sinks.File.csproj index 320ce07..71ef799 100644 --- a/src/Serilog.Sinks.File/Serilog.Sinks.File.csproj +++ b/src/Serilog.Sinks.File/Serilog.Sinks.File.csproj @@ -15,6 +15,8 @@ http://serilog.net/images/serilog-sink-nuget.png http://serilog.net http://www.apache.org/licenses/LICENSE-2.0 + https://github.com/serilog/serilog-sinks-file + git false Serilog true From 99030369f480980507db2a73c9815204b1ba9dca Mon Sep 17 00:00:00 2001 From: Ruslan Shupoval Date: Sat, 3 Nov 2018 05:46:57 -0600 Subject: [PATCH 41/69] add SourceLink support --- .editorconfig | 2 +- .travis.yml | 2 +- Build.ps1 | 4 ++-- Directory.Build.props | 10 ++++++++++ serilog-sinks-file.sln | 1 + src/Serilog.Sinks.File/Serilog.Sinks.File.csproj | 2 +- 6 files changed, 16 insertions(+), 5 deletions(-) create mode 100644 Directory.Build.props diff --git a/.editorconfig b/.editorconfig index f16002a..102e19f 100644 --- a/.editorconfig +++ b/.editorconfig @@ -6,7 +6,7 @@ insert_final_newline = true indent_style = space indent_size = 4 -[*.{csproj,json,config,yml}] +[*.{csproj,json,config,yml,props}] indent_size = 2 [*.sh] diff --git a/.travis.yml b/.travis.yml index 6a880da..3b4b3bb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,7 +5,7 @@ matrix: - os: linux dist: trusty sudo: required - dotnet: 2.0.0 + dotnet: 2.1.300 group: edge script: - ./build.sh diff --git a/Build.ps1 b/Build.ps1 index ee4117d..00cc642 100644 --- a/Build.ps1 +++ b/Build.ps1 @@ -25,9 +25,9 @@ foreach ($src in ls src/*) { & dotnet build -c Release --version-suffix=$buildSuffix if ($suffix) { - & dotnet pack -c Release --include-source -o ..\..\artifacts --version-suffix=$suffix --no-build + & dotnet pack -c Release -o ..\..\artifacts --version-suffix=$suffix --no-build } else { - & dotnet pack -c Release --include-source -o ..\..\artifacts --no-build + & dotnet pack -c Release -o ..\..\artifacts --no-build } if($LASTEXITCODE -ne 0) { exit 1 } diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 0000000..f6e7b79 --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,10 @@ + + + + true + $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb + + + + + diff --git a/serilog-sinks-file.sln b/serilog-sinks-file.sln index 71527e4..989031c 100644 --- a/serilog-sinks-file.sln +++ b/serilog-sinks-file.sln @@ -12,6 +12,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "assets", "assets", "{E9D1B5 appveyor.yml = appveyor.yml Build.ps1 = Build.ps1 build.sh = build.sh + Directory.Build.props = Directory.Build.props NuGet.Config = NuGet.Config README.md = README.md assets\Serilog.snk = assets\Serilog.snk diff --git a/src/Serilog.Sinks.File/Serilog.Sinks.File.csproj b/src/Serilog.Sinks.File/Serilog.Sinks.File.csproj index 71ef799..4c543c8 100644 --- a/src/Serilog.Sinks.File/Serilog.Sinks.File.csproj +++ b/src/Serilog.Sinks.File/Serilog.Sinks.File.csproj @@ -1,4 +1,4 @@ - + Write Serilog events to text files in plain or JSON format. From 1270a765418cef32a906e6935d7d5b380bd317c1 Mon Sep 17 00:00:00 2001 From: Ruslan Shupoval Date: Wed, 14 Nov 2018 16:31:43 -0700 Subject: [PATCH 42/69] move SourceLink configuration from Directory.Build.prop into csproj; disable SourceLink by default; --- Build.ps1 | 2 +- Directory.Build.props | 10 ---------- build.sh | 2 +- serilog-sinks-file.sln | 1 - src/Serilog.Sinks.File/Serilog.Sinks.File.csproj | 5 +++++ 5 files changed, 7 insertions(+), 13 deletions(-) delete mode 100644 Directory.Build.props diff --git a/Build.ps1 b/Build.ps1 index 00cc642..f7ed285 100644 --- a/Build.ps1 +++ b/Build.ps1 @@ -23,7 +23,7 @@ foreach ($src in ls src/*) { echo "build: Packaging project in $src" - & dotnet build -c Release --version-suffix=$buildSuffix + & dotnet build -c Release --version-suffix=$buildSuffix -p:EnableSourceLink=true if ($suffix) { & dotnet pack -c Release -o ..\..\artifacts --version-suffix=$suffix --no-build } else { diff --git a/Directory.Build.props b/Directory.Build.props deleted file mode 100644 index f6e7b79..0000000 --- a/Directory.Build.props +++ /dev/null @@ -1,10 +0,0 @@ - - - - true - $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb - - - - - diff --git a/build.sh b/build.sh index 4e46f40..56d265b 100755 --- a/build.sh +++ b/build.sh @@ -3,7 +3,7 @@ dotnet --info dotnet restore for path in src/**/*.csproj; do - dotnet build -f netstandard1.3 -c Release ${path} + dotnet build -f netstandard1.3 -c Release ${path} -p:EnableSourceLink=true done for path in test/*.Tests/*.csproj; do diff --git a/serilog-sinks-file.sln b/serilog-sinks-file.sln index 989031c..71527e4 100644 --- a/serilog-sinks-file.sln +++ b/serilog-sinks-file.sln @@ -12,7 +12,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "assets", "assets", "{E9D1B5 appveyor.yml = appveyor.yml Build.ps1 = Build.ps1 build.sh = build.sh - Directory.Build.props = Directory.Build.props NuGet.Config = NuGet.Config README.md = README.md assets\Serilog.snk = assets\Serilog.snk diff --git a/src/Serilog.Sinks.File/Serilog.Sinks.File.csproj b/src/Serilog.Sinks.File/Serilog.Sinks.File.csproj index 4c543c8..0594db1 100644 --- a/src/Serilog.Sinks.File/Serilog.Sinks.File.csproj +++ b/src/Serilog.Sinks.File/Serilog.Sinks.File.csproj @@ -23,9 +23,14 @@ Serilog.Sinks.File true + + false + true + $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb + From b952e33fb6fc6ae746ace855080251624a8227df Mon Sep 17 00:00:00 2001 From: Vadim Hatsura Date: Thu, 10 Jan 2019 15:48:27 +0300 Subject: [PATCH 43/69] Add target for .net standard 2.0 --- src/Serilog.Sinks.File/Serilog.Sinks.File.csproj | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/Serilog.Sinks.File/Serilog.Sinks.File.csproj b/src/Serilog.Sinks.File/Serilog.Sinks.File.csproj index 0594db1..1c2716b 100644 --- a/src/Serilog.Sinks.File/Serilog.Sinks.File.csproj +++ b/src/Serilog.Sinks.File/Serilog.Sinks.File.csproj @@ -4,7 +4,7 @@ Write Serilog events to text files in plain or JSON format. 4.0.1 Serilog Contributors - net45;netstandard1.3 + net45;netstandard1.3;netstandard2.0 true Serilog.Sinks.File ../../assets/Serilog.snk @@ -48,6 +48,10 @@ $(DefineConstants);OS_MUTEX + + $(DefineConstants);OS_MUTEX + + @@ -58,4 +62,11 @@ + + + + + + + From 195f95963e38d753b0fbd72e628bc659bff63d74 Mon Sep 17 00:00:00 2001 From: Vadim Hatsura Date: Mon, 14 Jan 2019 11:37:00 +0300 Subject: [PATCH 44/69] Remove System.IO and System.IO.FileSystem.Primitives from dependencies --- src/Serilog.Sinks.File/Serilog.Sinks.File.csproj | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Serilog.Sinks.File/Serilog.Sinks.File.csproj b/src/Serilog.Sinks.File/Serilog.Sinks.File.csproj index 1c2716b..6d7babf 100644 --- a/src/Serilog.Sinks.File/Serilog.Sinks.File.csproj +++ b/src/Serilog.Sinks.File/Serilog.Sinks.File.csproj @@ -30,7 +30,7 @@ - + @@ -63,9 +63,7 @@ - - From b472fc867140adc09e3b262049eb375e89213e38 Mon Sep 17 00:00:00 2001 From: Nicholas Blumhardt Date: Thu, 17 Jan 2019 10:45:22 +1000 Subject: [PATCH 45/69] Update minor version to reflect added capability --- src/Serilog.Sinks.File/Serilog.Sinks.File.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Serilog.Sinks.File/Serilog.Sinks.File.csproj b/src/Serilog.Sinks.File/Serilog.Sinks.File.csproj index 6d7babf..f8640f6 100644 --- a/src/Serilog.Sinks.File/Serilog.Sinks.File.csproj +++ b/src/Serilog.Sinks.File/Serilog.Sinks.File.csproj @@ -1,8 +1,8 @@ - + Write Serilog events to text files in plain or JSON format. - 4.0.1 + 4.1.0 Serilog Contributors net45;netstandard1.3;netstandard2.0 true From f7bfe4e672cddbb578c948d0e5f93153d712de8e Mon Sep 17 00:00:00 2001 From: cocowalla <800977+cocowalla@users.noreply.github.com> Date: Fri, 1 Feb 2019 14:57:12 +0000 Subject: [PATCH 46/69] Enabled FileSink and RollingFileSink's output stream in another stream, such as a GZipStream --- .../FileLoggerConfigurationExtensions.cs | 33 +++++++---- src/Serilog.Sinks.File/Sinks/File/FileSink.cs | 9 ++- .../Sinks/File/RollingFileSink.cs | 7 ++- src/Serilog.Sinks.File/StreamWrapper.cs | 17 ++++++ .../Serilog.Sinks.File.Tests/FileSinkTests.cs | 43 +++++++++++++- .../RollingFileSinkTests.cs | 59 +++++++++++++++++++ .../Support/Extensions.cs | 20 ++++++- .../Support/GZipStreamWrapper.cs | 25 ++++++++ 8 files changed, 195 insertions(+), 18 deletions(-) create mode 100644 src/Serilog.Sinks.File/StreamWrapper.cs create mode 100644 test/Serilog.Sinks.File.Tests/Support/GZipStreamWrapper.cs diff --git a/src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs b/src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs index 5cb19e9..bf2d5a4 100644 --- a/src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs +++ b/src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs @@ -135,11 +135,12 @@ public static LoggerConfiguration File( /// 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. /// The interval at which logging will roll over to a new file. - /// If true, a new file will be created when the file size limit is reached. Filenames + /// If true, a new file will be created when the file size limit is reached. Filenames /// will have a number appended in the format _NNN, with the first filename given no number. /// The maximum number of log files that will be retained, /// including the current log file. For unlimited retention, pass null. The default is 31. /// Character encoding used to write the text file. The default is UTF-8 without BOM. + /// Optionally enables wrapping the output stream in another stream, such as a GZipStream. /// Configuration object allowing method chaining. /// The file will be written using the UTF-8 character set. public static LoggerConfiguration File( @@ -156,7 +157,8 @@ public static LoggerConfiguration File( RollingInterval rollingInterval = RollingInterval.Infinite, bool rollOnFileSizeLimit = false, int? retainedFileCountLimit = DefaultRetainedFileCountLimit, - Encoding encoding = null) + Encoding encoding = null, + StreamWrapper wrapper = null) { if (sinkConfiguration == null) throw new ArgumentNullException(nameof(sinkConfiguration)); if (path == null) throw new ArgumentNullException(nameof(path)); @@ -165,7 +167,7 @@ public static LoggerConfiguration File( var formatter = new MessageTemplateTextFormatter(outputTemplate, formatProvider); return File(sinkConfiguration, formatter, path, restrictedToMinimumLevel, fileSizeLimitBytes, levelSwitch, buffered, shared, flushToDiskInterval, - rollingInterval, rollOnFileSizeLimit, retainedFileCountLimit, encoding); + rollingInterval, rollOnFileSizeLimit, retainedFileCountLimit, encoding, wrapper); } /// @@ -174,7 +176,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. @@ -190,11 +192,12 @@ public static LoggerConfiguration File( /// 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. /// The interval at which logging will roll over to a new file. - /// If true, a new file will be created when the file size limit is reached. Filenames + /// If true, a new file will be created when the file size limit is reached. Filenames /// will have a number appended in the format _NNN, with the first filename given no number. /// The maximum number of log files that will be retained, /// including the current log file. For unlimited retention, pass null. The default is 31. /// Character encoding used to write the text file. The default is UTF-8 without BOM. + /// Optionally enables wrapping the output stream in another stream, such as a GZipStream. /// Configuration object allowing method chaining. /// The file will be written using the UTF-8 character set. public static LoggerConfiguration File( @@ -210,10 +213,12 @@ public static LoggerConfiguration File( RollingInterval rollingInterval = RollingInterval.Infinite, bool rollOnFileSizeLimit = false, int? retainedFileCountLimit = DefaultRetainedFileCountLimit, - Encoding encoding = null) + Encoding encoding = null, + StreamWrapper wrapper = null) { return ConfigureFile(sinkConfiguration.Sink, formatter, path, restrictedToMinimumLevel, fileSizeLimitBytes, levelSwitch, - buffered, false, shared, flushToDiskInterval, encoding, rollingInterval, rollOnFileSizeLimit, retainedFileCountLimit); + buffered, false, shared, flushToDiskInterval, encoding, rollingInterval, rollOnFileSizeLimit, + retainedFileCountLimit, wrapper); } /// @@ -270,7 +275,7 @@ public static LoggerConfiguration File( LoggingLevelSwitch levelSwitch = null) { return ConfigureFile(sinkConfiguration.Sink, formatter, path, restrictedToMinimumLevel, null, levelSwitch, false, true, - false, null, null, RollingInterval.Infinite, false, null); + false, null, null, RollingInterval.Infinite, false, null, null); } static LoggerConfiguration ConfigureFile( @@ -287,7 +292,8 @@ static LoggerConfiguration ConfigureFile( Encoding encoding, RollingInterval rollingInterval, bool rollOnFileSizeLimit, - int? retainedFileCountLimit) + int? retainedFileCountLimit, + StreamWrapper wrapper) { if (addSink == null) throw new ArgumentNullException(nameof(addSink)); if (formatter == null) throw new ArgumentNullException(nameof(formatter)); @@ -300,7 +306,7 @@ static LoggerConfiguration ConfigureFile( if (rollOnFileSizeLimit || rollingInterval != RollingInterval.Infinite) { - sink = new RollingFileSink(path, formatter, fileSizeLimitBytes, retainedFileCountLimit, encoding, buffered, shared, rollingInterval, rollOnFileSizeLimit); + sink = new RollingFileSink(path, formatter, fileSizeLimitBytes, retainedFileCountLimit, encoding, buffered, shared, rollingInterval, rollOnFileSizeLimit, wrapper); } else { @@ -309,11 +315,16 @@ static LoggerConfiguration ConfigureFile( #pragma warning disable 618 if (shared) { + if (wrapper != null) + { + SelfLog.WriteLine("Unable to use output stream wrapper - these are not supported for shared log files"); + } + sink = new SharedFileSink(path, formatter, fileSizeLimitBytes); } else { - sink = new FileSink(path, formatter, fileSizeLimitBytes, buffered: buffered); + sink = new FileSink(path, formatter, fileSizeLimitBytes, buffered: buffered, wrapper: wrapper); } #pragma warning restore 618 } diff --git a/src/Serilog.Sinks.File/Sinks/File/FileSink.cs b/src/Serilog.Sinks.File/Sinks/File/FileSink.cs index bfd288f..c896034 100644 --- a/src/Serilog.Sinks.File/Sinks/File/FileSink.cs +++ b/src/Serilog.Sinks.File/Sinks/File/FileSink.cs @@ -43,10 +43,12 @@ public sealed class FileSink : IFileSink, IDisposable /// 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. + /// Optionally enables wrapping the output stream in another stream, such as a GZipStream. /// Configuration object allowing method chaining. /// The file will be written using the UTF-8 character set. /// - public FileSink(string path, ITextFormatter textFormatter, long? fileSizeLimitBytes, Encoding encoding = null, bool buffered = false) + public FileSink(string path, ITextFormatter textFormatter, long? fileSizeLimitBytes, Encoding encoding = null, bool buffered = false, + StreamWrapper wrapper = null) { if (path == null) throw new ArgumentNullException(nameof(path)); if (textFormatter == null) throw new ArgumentNullException(nameof(textFormatter)); @@ -68,6 +70,11 @@ public FileSink(string path, ITextFormatter textFormatter, long? fileSizeLimitBy outputStream = _countingStreamWrapper = new WriteCountingStream(_underlyingStream); } + if (wrapper != null) + { + outputStream = wrapper.Wrap(outputStream); + } + _output = new StreamWriter(outputStream, encoding ?? new UTF8Encoding(encoderShouldEmitUTF8Identifier: false)); } diff --git a/src/Serilog.Sinks.File/Sinks/File/RollingFileSink.cs b/src/Serilog.Sinks.File/Sinks/File/RollingFileSink.cs index 644176f..73415b2 100644 --- a/src/Serilog.Sinks.File/Sinks/File/RollingFileSink.cs +++ b/src/Serilog.Sinks.File/Sinks/File/RollingFileSink.cs @@ -35,6 +35,7 @@ sealed class RollingFileSink : ILogEventSink, IFlushableFileSink, IDisposable readonly bool _buffered; readonly bool _shared; readonly bool _rollOnFileSizeLimit; + readonly StreamWrapper _wrapper; readonly object _syncRoot = new object(); bool _isDisposed; @@ -50,7 +51,8 @@ public RollingFileSink(string path, bool buffered, bool shared, RollingInterval rollingInterval, - bool rollOnFileSizeLimit) + bool rollOnFileSizeLimit, + StreamWrapper wrapper = null) { 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"); @@ -64,6 +66,7 @@ public RollingFileSink(string path, _buffered = buffered; _shared = shared; _rollOnFileSizeLimit = rollOnFileSizeLimit; + _wrapper = wrapper; } public void Emit(LogEvent logEvent) @@ -144,7 +147,7 @@ void OpenFile(DateTime now, int? minSequence = null) { _currentFile = _shared ? (IFileSink)new SharedFileSink(path, _textFormatter, _fileSizeLimitBytes, _encoding) : - new FileSink(path, _textFormatter, _fileSizeLimitBytes, _encoding, _buffered); + new FileSink(path, _textFormatter, _fileSizeLimitBytes, _encoding, _buffered, _wrapper); _currentFileSequence = sequence; } catch (IOException ex) diff --git a/src/Serilog.Sinks.File/StreamWrapper.cs b/src/Serilog.Sinks.File/StreamWrapper.cs new file mode 100644 index 0000000..bccdd22 --- /dev/null +++ b/src/Serilog.Sinks.File/StreamWrapper.cs @@ -0,0 +1,17 @@ +using System.IO; + +namespace Serilog +{ + /// + /// Wraps the log file's output stream in another stream, such as a GZipStream + /// + public abstract class StreamWrapper + { + /// + /// Wraps in another stream, such as a GZipStream, then returns the wrapped stream + /// + /// The source log file stream + /// The wrapped stream + public abstract Stream Wrap(Stream sourceStream); + } +} diff --git a/test/Serilog.Sinks.File.Tests/FileSinkTests.cs b/test/Serilog.Sinks.File.Tests/FileSinkTests.cs index ea9a5d4..3cc83f4 100644 --- a/test/Serilog.Sinks.File.Tests/FileSinkTests.cs +++ b/test/Serilog.Sinks.File.Tests/FileSinkTests.cs @@ -1,9 +1,11 @@ -using System.IO; +using System.Collections.Generic; +using System.IO; +using System.IO.Compression; +using System.Text; using Xunit; using Serilog.Formatting.Json; using Serilog.Sinks.File.Tests.Support; using Serilog.Tests.Support; -using System.Text; #pragma warning disable 618 @@ -141,6 +143,42 @@ public void WhenLimitIsNotSpecifiedAndEncodingHasNoPreambleDataIsCorrectlyAppend WriteTwoEventsAndCheckOutputFileLength(null, encoding); } + [Fact] + public void WhenStreamWrapperIsSpecifiedOutputStreamIsWrapped() + { + var gzipWrapper = new GZipStreamWrapper(); + + using (var tmp = TempFolder.ForCaller()) + { + var nonexistent = tmp.AllocateFilename("txt"); + var evt = Some.LogEvent("Hello, world!"); + + using (var sink = new FileSink(nonexistent, new JsonFormatter(), null, wrapper: gzipWrapper)) + { + sink.Emit(evt); + sink.Emit(evt); + } + + // Ensure the data was written through the wrapping GZipStream, by decompressing and comparing against + // what we wrote + var lines = new List(); + using (var textStream = new MemoryStream()) + { + using (var fs = System.IO.File.OpenRead(nonexistent)) + using (var decompressStream = new GZipStream(fs, CompressionMode.Decompress)) + { + decompressStream.CopyTo(textStream); + } + + textStream.Position = 0; + lines = textStream.ReadAllLines(); + } + + Assert.Equal(2, lines.Count); + Assert.Contains("Hello, world!", lines[0]); + } + } + static void WriteTwoEventsAndCheckOutputFileLength(long? maxBytes, Encoding encoding) { using (var tmp = TempFolder.ForCaller()) @@ -170,4 +208,3 @@ static void WriteTwoEventsAndCheckOutputFileLength(long? maxBytes, Encoding enco } } } - diff --git a/test/Serilog.Sinks.File.Tests/RollingFileSinkTests.cs b/test/Serilog.Sinks.File.Tests/RollingFileSinkTests.cs index 3efe3f9..c2521df 100644 --- a/test/Serilog.Sinks.File.Tests/RollingFileSinkTests.cs +++ b/test/Serilog.Sinks.File.Tests/RollingFileSinkTests.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.IO.Compression; using System.Linq; using System.Reflection; using Xunit; @@ -96,6 +97,64 @@ public void WhenSizeLimitIsBreachedNewFilesCreated() } } + [Fact] + public void WhenStreamWrapperSpecifiedIsUsedForRolledFiles() + { + var gzipWrapper = new GZipStreamWrapper(); + var fileName = Some.String() + ".txt"; + + using (var temp = new TempFolder()) + { + string[] files; + var logEvents = new[] + { + Some.InformationEvent(), + Some.InformationEvent(), + Some.InformationEvent() + }; + + using (var log = new LoggerConfiguration() + .WriteTo.File(Path.Combine(temp.Path, fileName), rollOnFileSizeLimit: true, fileSizeLimitBytes: 1, wrapper: gzipWrapper) + .CreateLogger()) + { + + foreach (var logEvent in logEvents) + { + log.Write(logEvent); + } + + files = Directory.GetFiles(temp.Path) + .OrderBy(p => p, StringComparer.OrdinalIgnoreCase) + .ToArray(); + + Assert.Equal(3, files.Length); + Assert.True(files[0].EndsWith(fileName), files[0]); + Assert.True(files[1].EndsWith("_001.txt"), files[1]); + Assert.True(files[2].EndsWith("_002.txt"), files[2]); + } + + // Ensure the data was written through the wrapping GZipStream, by decompressing and comparing against + // what we wrote + for (var i = 0; i < files.Length; i++) + { + using (var textStream = new MemoryStream()) + { + using (var fs = System.IO.File.OpenRead(files[i])) + using (var decompressStream = new GZipStream(fs, CompressionMode.Decompress)) + { + decompressStream.CopyTo(textStream); + } + + textStream.Position = 0; + var lines = textStream.ReadAllLines(); + + Assert.Equal(1, lines.Count); + Assert.True(lines[0].EndsWith(logEvents[i].MessageTemplate.Text)); + } + } + } + } + [Fact] public void IfTheLogFolderDoesNotExistItWillBeCreated() { diff --git a/test/Serilog.Sinks.File.Tests/Support/Extensions.cs b/test/Serilog.Sinks.File.Tests/Support/Extensions.cs index a31122d..f7fb775 100644 --- a/test/Serilog.Sinks.File.Tests/Support/Extensions.cs +++ b/test/Serilog.Sinks.File.Tests/Support/Extensions.cs @@ -1,4 +1,6 @@ -using Serilog.Events; +using System.Collections.Generic; +using System.IO; +using Serilog.Events; namespace Serilog.Sinks.File.Tests.Support { @@ -8,5 +10,21 @@ public static object LiteralValue(this LogEventPropertyValue @this) { return ((ScalarValue)@this).Value; } + + public static List ReadAllLines(this Stream @this) + { + var lines = new List(); + + using (var reader = new StreamReader(@this)) + { + string line; + while ((line = reader.ReadLine()) != null) + { + lines.Add(line); + } + } + + return lines; + } } } diff --git a/test/Serilog.Sinks.File.Tests/Support/GZipStreamWrapper.cs b/test/Serilog.Sinks.File.Tests/Support/GZipStreamWrapper.cs new file mode 100644 index 0000000..30c316c --- /dev/null +++ b/test/Serilog.Sinks.File.Tests/Support/GZipStreamWrapper.cs @@ -0,0 +1,25 @@ +using System.IO; +using System.IO.Compression; + +namespace Serilog.Sinks.File.Tests.Support +{ + /// + /// + /// Demonstrates the use of , by compressing log output using GZip + /// + public class GZipStreamWrapper : StreamWrapper + { + readonly int _bufferSize; + + public GZipStreamWrapper(int bufferSize = 1024 * 32) + { + _bufferSize = bufferSize; + } + + public override Stream Wrap(Stream sourceStream) + { + var compressStream = new GZipStream(sourceStream, CompressionMode.Compress); + return new BufferedStream(compressStream, _bufferSize); + } + } +} From 9eee731a4ccf6f3338a20acebc2d5a3d64199fb5 Mon Sep 17 00:00:00 2001 From: cocowalla <800977+cocowalla@users.noreply.github.com> Date: Thu, 14 Feb 2019 20:56:16 +0000 Subject: [PATCH 47/69] Rename StreamWrapper to FileLifecycleHooks --- ...StreamWrapper.cs => FileLifecycleHooks.cs} | 7 +++--- .../FileLoggerConfigurationExtensions.cs | 22 +++++++++---------- src/Serilog.Sinks.File/Sinks/File/FileSink.cs | 8 +++---- .../Sinks/File/RollingFileSink.cs | 8 +++---- .../Serilog.Sinks.File.Tests/FileSinkTests.cs | 4 ++-- .../RollingFileSinkTests.cs | 4 ++-- .../{GZipStreamWrapper.cs => GZipHooks.cs} | 6 ++--- 7 files changed, 30 insertions(+), 29 deletions(-) rename src/Serilog.Sinks.File/{StreamWrapper.cs => FileLifecycleHooks.cs} (75%) rename test/Serilog.Sinks.File.Tests/Support/{GZipStreamWrapper.cs => GZipHooks.cs} (68%) diff --git a/src/Serilog.Sinks.File/StreamWrapper.cs b/src/Serilog.Sinks.File/FileLifecycleHooks.cs similarity index 75% rename from src/Serilog.Sinks.File/StreamWrapper.cs rename to src/Serilog.Sinks.File/FileLifecycleHooks.cs index bccdd22..bdfe941 100644 --- a/src/Serilog.Sinks.File/StreamWrapper.cs +++ b/src/Serilog.Sinks.File/FileLifecycleHooks.cs @@ -1,11 +1,12 @@ -using System.IO; namespace Serilog { + using System.IO; + /// - /// Wraps the log file's output stream in another stream, such as a GZipStream + /// Enables hooking into log file lifecycle events /// - public abstract class StreamWrapper + public abstract class FileLifecycleHooks { /// /// Wraps in another stream, such as a GZipStream, then returns the wrapped stream diff --git a/src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs b/src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs index bf2d5a4..aa43c50 100644 --- a/src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs +++ b/src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs @@ -140,7 +140,7 @@ public static LoggerConfiguration File( /// The maximum number of log files that will be retained, /// including the current log file. For unlimited retention, pass null. The default is 31. /// Character encoding used to write the text file. The default is UTF-8 without BOM. - /// Optionally enables wrapping the output stream in another stream, such as a GZipStream. + /// Optionally enables hooking into log file lifecycle events. /// Configuration object allowing method chaining. /// The file will be written using the UTF-8 character set. public static LoggerConfiguration File( @@ -158,7 +158,7 @@ public static LoggerConfiguration File( bool rollOnFileSizeLimit = false, int? retainedFileCountLimit = DefaultRetainedFileCountLimit, Encoding encoding = null, - StreamWrapper wrapper = null) + FileLifecycleHooks hooks = null) { if (sinkConfiguration == null) throw new ArgumentNullException(nameof(sinkConfiguration)); if (path == null) throw new ArgumentNullException(nameof(path)); @@ -167,7 +167,7 @@ public static LoggerConfiguration File( var formatter = new MessageTemplateTextFormatter(outputTemplate, formatProvider); return File(sinkConfiguration, formatter, path, restrictedToMinimumLevel, fileSizeLimitBytes, levelSwitch, buffered, shared, flushToDiskInterval, - rollingInterval, rollOnFileSizeLimit, retainedFileCountLimit, encoding, wrapper); + rollingInterval, rollOnFileSizeLimit, retainedFileCountLimit, encoding, hooks); } /// @@ -176,7 +176,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. @@ -197,7 +197,7 @@ public static LoggerConfiguration File( /// The maximum number of log files that will be retained, /// including the current log file. For unlimited retention, pass null. The default is 31. /// Character encoding used to write the text file. The default is UTF-8 without BOM. - /// Optionally enables wrapping the output stream in another stream, such as a GZipStream. + /// Optionally enables hooking into log file lifecycle events. /// Configuration object allowing method chaining. /// The file will be written using the UTF-8 character set. public static LoggerConfiguration File( @@ -214,11 +214,11 @@ public static LoggerConfiguration File( bool rollOnFileSizeLimit = false, int? retainedFileCountLimit = DefaultRetainedFileCountLimit, Encoding encoding = null, - StreamWrapper wrapper = null) + FileLifecycleHooks hooks = null) { return ConfigureFile(sinkConfiguration.Sink, formatter, path, restrictedToMinimumLevel, fileSizeLimitBytes, levelSwitch, buffered, false, shared, flushToDiskInterval, encoding, rollingInterval, rollOnFileSizeLimit, - retainedFileCountLimit, wrapper); + retainedFileCountLimit, hooks); } /// @@ -293,7 +293,7 @@ static LoggerConfiguration ConfigureFile( RollingInterval rollingInterval, bool rollOnFileSizeLimit, int? retainedFileCountLimit, - StreamWrapper wrapper) + FileLifecycleHooks hooks) { if (addSink == null) throw new ArgumentNullException(nameof(addSink)); if (formatter == null) throw new ArgumentNullException(nameof(formatter)); @@ -306,7 +306,7 @@ static LoggerConfiguration ConfigureFile( if (rollOnFileSizeLimit || rollingInterval != RollingInterval.Infinite) { - sink = new RollingFileSink(path, formatter, fileSizeLimitBytes, retainedFileCountLimit, encoding, buffered, shared, rollingInterval, rollOnFileSizeLimit, wrapper); + sink = new RollingFileSink(path, formatter, fileSizeLimitBytes, retainedFileCountLimit, encoding, buffered, shared, rollingInterval, rollOnFileSizeLimit, hooks); } else { @@ -315,7 +315,7 @@ static LoggerConfiguration ConfigureFile( #pragma warning disable 618 if (shared) { - if (wrapper != null) + if (hooks != null) { SelfLog.WriteLine("Unable to use output stream wrapper - these are not supported for shared log files"); } @@ -324,7 +324,7 @@ static LoggerConfiguration ConfigureFile( } else { - sink = new FileSink(path, formatter, fileSizeLimitBytes, buffered: buffered, wrapper: wrapper); + sink = new FileSink(path, formatter, fileSizeLimitBytes, buffered: buffered, hooks: hooks); } #pragma warning restore 618 } diff --git a/src/Serilog.Sinks.File/Sinks/File/FileSink.cs b/src/Serilog.Sinks.File/Sinks/File/FileSink.cs index c896034..f97817a 100644 --- a/src/Serilog.Sinks.File/Sinks/File/FileSink.cs +++ b/src/Serilog.Sinks.File/Sinks/File/FileSink.cs @@ -43,12 +43,12 @@ public sealed class FileSink : IFileSink, IDisposable /// 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. - /// Optionally enables wrapping the output stream in another stream, such as a GZipStream. + /// Optionally enables hooking into log file lifecycle events. /// Configuration object allowing method chaining. /// The file will be written using the UTF-8 character set. /// public FileSink(string path, ITextFormatter textFormatter, long? fileSizeLimitBytes, Encoding encoding = null, bool buffered = false, - StreamWrapper wrapper = null) + FileLifecycleHooks hooks = null) { if (path == null) throw new ArgumentNullException(nameof(path)); if (textFormatter == null) throw new ArgumentNullException(nameof(textFormatter)); @@ -70,9 +70,9 @@ public FileSink(string path, ITextFormatter textFormatter, long? fileSizeLimitBy outputStream = _countingStreamWrapper = new WriteCountingStream(_underlyingStream); } - if (wrapper != null) + if (hooks != null) { - outputStream = wrapper.Wrap(outputStream); + outputStream = hooks.Wrap(outputStream); } _output = new StreamWriter(outputStream, encoding ?? new UTF8Encoding(encoderShouldEmitUTF8Identifier: false)); diff --git a/src/Serilog.Sinks.File/Sinks/File/RollingFileSink.cs b/src/Serilog.Sinks.File/Sinks/File/RollingFileSink.cs index 73415b2..993239f 100644 --- a/src/Serilog.Sinks.File/Sinks/File/RollingFileSink.cs +++ b/src/Serilog.Sinks.File/Sinks/File/RollingFileSink.cs @@ -35,7 +35,7 @@ sealed class RollingFileSink : ILogEventSink, IFlushableFileSink, IDisposable readonly bool _buffered; readonly bool _shared; readonly bool _rollOnFileSizeLimit; - readonly StreamWrapper _wrapper; + readonly FileLifecycleHooks _hooks; readonly object _syncRoot = new object(); bool _isDisposed; @@ -52,7 +52,7 @@ public RollingFileSink(string path, bool shared, RollingInterval rollingInterval, bool rollOnFileSizeLimit, - StreamWrapper wrapper = null) + FileLifecycleHooks hooks = null) { 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"); @@ -66,7 +66,7 @@ public RollingFileSink(string path, _buffered = buffered; _shared = shared; _rollOnFileSizeLimit = rollOnFileSizeLimit; - _wrapper = wrapper; + _hooks = hooks; } public void Emit(LogEvent logEvent) @@ -147,7 +147,7 @@ void OpenFile(DateTime now, int? minSequence = null) { _currentFile = _shared ? (IFileSink)new SharedFileSink(path, _textFormatter, _fileSizeLimitBytes, _encoding) : - new FileSink(path, _textFormatter, _fileSizeLimitBytes, _encoding, _buffered, _wrapper); + new FileSink(path, _textFormatter, _fileSizeLimitBytes, _encoding, _buffered, _hooks); _currentFileSequence = sequence; } catch (IOException ex) diff --git a/test/Serilog.Sinks.File.Tests/FileSinkTests.cs b/test/Serilog.Sinks.File.Tests/FileSinkTests.cs index 3cc83f4..e701594 100644 --- a/test/Serilog.Sinks.File.Tests/FileSinkTests.cs +++ b/test/Serilog.Sinks.File.Tests/FileSinkTests.cs @@ -146,14 +146,14 @@ public void WhenLimitIsNotSpecifiedAndEncodingHasNoPreambleDataIsCorrectlyAppend [Fact] public void WhenStreamWrapperIsSpecifiedOutputStreamIsWrapped() { - var gzipWrapper = new GZipStreamWrapper(); + var gzipWrapper = new GZipHooks(); using (var tmp = TempFolder.ForCaller()) { var nonexistent = tmp.AllocateFilename("txt"); var evt = Some.LogEvent("Hello, world!"); - using (var sink = new FileSink(nonexistent, new JsonFormatter(), null, wrapper: gzipWrapper)) + using (var sink = new FileSink(nonexistent, new JsonFormatter(), null, hooks: gzipWrapper)) { sink.Emit(evt); sink.Emit(evt); diff --git a/test/Serilog.Sinks.File.Tests/RollingFileSinkTests.cs b/test/Serilog.Sinks.File.Tests/RollingFileSinkTests.cs index c2521df..2e9f613 100644 --- a/test/Serilog.Sinks.File.Tests/RollingFileSinkTests.cs +++ b/test/Serilog.Sinks.File.Tests/RollingFileSinkTests.cs @@ -100,7 +100,7 @@ public void WhenSizeLimitIsBreachedNewFilesCreated() [Fact] public void WhenStreamWrapperSpecifiedIsUsedForRolledFiles() { - var gzipWrapper = new GZipStreamWrapper(); + var gzipWrapper = new GZipHooks(); var fileName = Some.String() + ".txt"; using (var temp = new TempFolder()) @@ -114,7 +114,7 @@ public void WhenStreamWrapperSpecifiedIsUsedForRolledFiles() }; using (var log = new LoggerConfiguration() - .WriteTo.File(Path.Combine(temp.Path, fileName), rollOnFileSizeLimit: true, fileSizeLimitBytes: 1, wrapper: gzipWrapper) + .WriteTo.File(Path.Combine(temp.Path, fileName), rollOnFileSizeLimit: true, fileSizeLimitBytes: 1, hooks: gzipWrapper) .CreateLogger()) { diff --git a/test/Serilog.Sinks.File.Tests/Support/GZipStreamWrapper.cs b/test/Serilog.Sinks.File.Tests/Support/GZipHooks.cs similarity index 68% rename from test/Serilog.Sinks.File.Tests/Support/GZipStreamWrapper.cs rename to test/Serilog.Sinks.File.Tests/Support/GZipHooks.cs index 30c316c..efb2319 100644 --- a/test/Serilog.Sinks.File.Tests/Support/GZipStreamWrapper.cs +++ b/test/Serilog.Sinks.File.Tests/Support/GZipHooks.cs @@ -5,13 +5,13 @@ namespace Serilog.Sinks.File.Tests.Support { /// /// - /// Demonstrates the use of , by compressing log output using GZip + /// Demonstrates the use of , by compressing log output using GZip /// - public class GZipStreamWrapper : StreamWrapper + public class GZipHooks : FileLifecycleHooks { readonly int _bufferSize; - public GZipStreamWrapper(int bufferSize = 1024 * 32) + public GZipHooks(int bufferSize = 1024 * 32) { _bufferSize = bufferSize; } From 83d817f7e3d117a68007420a25585bf2c065d19a Mon Sep 17 00:00:00 2001 From: cocowalla <800977+cocowalla@users.noreply.github.com> Date: Thu, 14 Feb 2019 20:56:31 +0000 Subject: [PATCH 48/69] Ignore Rider cache/options files --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 483adfa..d8704ba 100644 --- a/.gitignore +++ b/.gitignore @@ -26,6 +26,9 @@ bld/ # Uncomment if you have tasks that create the project's static files in wwwroot #wwwroot/ +# Rider cache/options directory +.idea + # MSTest test Results [Tt]est[Rr]esult*/ [Bb]uild[Ll]og.* From 63c36013142aba3b13e7cd38ec470eb5f87aeb8a Mon Sep 17 00:00:00 2001 From: cocowalla <800977+cocowalla@users.noreply.github.com> Date: Thu, 14 Feb 2019 21:22:04 +0000 Subject: [PATCH 49/69] Add docs re wrapped stream ownership --- src/Serilog.Sinks.File/FileLifecycleHooks.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/Serilog.Sinks.File/FileLifecycleHooks.cs b/src/Serilog.Sinks.File/FileLifecycleHooks.cs index bdfe941..3abe448 100644 --- a/src/Serilog.Sinks.File/FileLifecycleHooks.cs +++ b/src/Serilog.Sinks.File/FileLifecycleHooks.cs @@ -9,10 +9,13 @@ namespace Serilog public abstract class FileLifecycleHooks { /// - /// Wraps in another stream, such as a GZipStream, then returns the wrapped stream + /// Wraps in another stream, such as a GZipStream, then returns the wrapped stream /// - /// The source log file stream + /// + /// Serilog is responsible for disposing of the wrapped stream + /// + /// The underlying log file stream /// The wrapped stream - public abstract Stream Wrap(Stream sourceStream); + public abstract Stream Wrap(Stream underlyingStream); } } From fca6eb93fabdedc2fcbd9dce1ff95611597ad726 Mon Sep 17 00:00:00 2001 From: BillRob Date: Tue, 12 Mar 2019 17:33:00 -0500 Subject: [PATCH 50/69] Check for directory existence before attempting access. Stops exception from being thrown and tripping breakpoints when debugging application code. --- src/Serilog.Sinks.File/Sinks/File/RollingFileSink.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Serilog.Sinks.File/Sinks/File/RollingFileSink.cs b/src/Serilog.Sinks.File/Sinks/File/RollingFileSink.cs index 644176f..6593e68 100644 --- a/src/Serilog.Sinks.File/Sinks/File/RollingFileSink.cs +++ b/src/Serilog.Sinks.File/Sinks/File/RollingFileSink.cs @@ -117,8 +117,11 @@ void OpenFile(DateTime now, int? minSequence = null) var existingFiles = Enumerable.Empty(); try { - existingFiles = Directory.GetFiles(_roller.LogFileDirectory, _roller.DirectorySearchPattern) + if (Directory.Exists(_roller.LogFileDirectory)) + { + existingFiles = Directory.GetFiles(_roller.LogFileDirectory, _roller.DirectorySearchPattern) .Select(Path.GetFileName); + } } catch (DirectoryNotFoundException) { } From e604418ccaad879556a01b33aa6d34b6dac1c759 Mon Sep 17 00:00:00 2001 From: cocowalla <800977+cocowalla@users.noreply.github.com> Date: Sat, 20 Apr 2019 19:39:36 +0100 Subject: [PATCH 51/69] Improve log message when FileLifecycleHooks provided for a shared log file --- src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs b/src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs index aa43c50..c716c1d 100644 --- a/src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs +++ b/src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs @@ -317,7 +317,7 @@ static LoggerConfiguration ConfigureFile( { if (hooks != null) { - SelfLog.WriteLine("Unable to use output stream wrapper - these are not supported for shared log files"); + SelfLog.WriteLine($"Unable to use {hooks.GetType().Name} FileLifecycleHooks output stream wrapper - these are not supported for shared log files"); } sink = new SharedFileSink(path, formatter, fileSizeLimitBytes); From 873f6d453950e33daf58a6dd38b2a0a60bfb39b6 Mon Sep 17 00:00:00 2001 From: cocowalla <800977+cocowalla@users.noreply.github.com> Date: Sat, 20 Apr 2019 20:32:20 +0100 Subject: [PATCH 52/69] Throw clear exception when wrapping the output stream returns null --- src/Serilog.Sinks.File/Sinks/File/FileSink.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Serilog.Sinks.File/Sinks/File/FileSink.cs b/src/Serilog.Sinks.File/Sinks/File/FileSink.cs index f97817a..5b167cc 100644 --- a/src/Serilog.Sinks.File/Sinks/File/FileSink.cs +++ b/src/Serilog.Sinks.File/Sinks/File/FileSink.cs @@ -1,4 +1,4 @@ -// Copyright 2013-2016 Serilog Contributors +// 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. @@ -73,6 +73,11 @@ public FileSink(string path, ITextFormatter textFormatter, long? fileSizeLimitBy if (hooks != null) { outputStream = hooks.Wrap(outputStream); + + if (outputStream == null) + { + throw new InvalidOperationException($"{hooks.GetType().Name}.Wrap returned null when wrapping the output stream"); + } } _output = new StreamWriter(outputStream, encoding ?? new UTF8Encoding(encoderShouldEmitUTF8Identifier: false)); From e310ab9bd2ab7648d4629b994714240be53a24d6 Mon Sep 17 00:00:00 2001 From: cocowalla <800977+cocowalla@users.noreply.github.com> Date: Sat, 20 Apr 2019 22:37:24 +0100 Subject: [PATCH 53/69] Throw when using hooks with a shared file --- src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs | 6 +----- .../FileLoggerConfigurationExtensionsTests.cs | 6 +++--- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs b/src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs index c716c1d..2f7ab7a 100644 --- a/src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs +++ b/src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs @@ -301,6 +301,7 @@ static LoggerConfiguration ConfigureFile( if (fileSizeLimitBytes.HasValue && fileSizeLimitBytes < 0) throw new ArgumentException("Negative value provided; file size limit must be non-negative.", nameof(fileSizeLimitBytes)); if (retainedFileCountLimit.HasValue && retainedFileCountLimit < 1) throw new ArgumentException("At least one file must be retained.", nameof(retainedFileCountLimit)); if (shared && buffered) throw new ArgumentException("Buffered writes are not available when file sharing is enabled.", nameof(buffered)); + if (shared && hooks != null) throw new ArgumentException("Unable to use {hooks.GetType().Name} FileLifecycleHooks output stream wrapper - these are not supported for shared log files", nameof(hooks)); ILogEventSink sink; @@ -315,11 +316,6 @@ static LoggerConfiguration ConfigureFile( #pragma warning disable 618 if (shared) { - if (hooks != null) - { - SelfLog.WriteLine($"Unable to use {hooks.GetType().Name} FileLifecycleHooks output stream wrapper - these are not supported for shared log files"); - } - sink = new SharedFileSink(path, formatter, fileSizeLimitBytes); } else diff --git a/test/Serilog.Sinks.File.Tests/FileLoggerConfigurationExtensionsTests.cs b/test/Serilog.Sinks.File.Tests/FileLoggerConfigurationExtensionsTests.cs index 0515655..cfa7ad7 100644 --- a/test/Serilog.Sinks.File.Tests/FileLoggerConfigurationExtensionsTests.cs +++ b/test/Serilog.Sinks.File.Tests/FileLoggerConfigurationExtensionsTests.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Threading; using Serilog.Sinks.File.Tests.Support; using Serilog.Tests.Support; @@ -80,11 +80,11 @@ public void WhenFlushingToDiskReportedSharedFileSinkCanBeCreatedAndDisposed() } [Fact] - public void BufferingIsNotAvailableWhenSharingEnabled() + public void HooksAreNotAvailableWhenSharingEnabled() { Assert.Throws(() => new LoggerConfiguration() - .WriteTo.File("logs", buffered: true, shared: true)); + .WriteTo.File("logs", shared: true, hooks: new GZipHooks())); } } } From b660b51289599f2e6c9ce83dc6fdb5e8c81f1369 Mon Sep 17 00:00:00 2001 From: Nicholas Blumhardt Date: Mon, 22 Apr 2019 11:01:54 +1000 Subject: [PATCH 54/69] Make FileSink constructor changes non-breaking; fix a bug whereby a specified encoding is not used; clean and tidy --- src/Serilog.Sinks.File/FileLifecycleHooks.cs | 32 ++++++++++++++----- .../FileLoggerConfigurationExtensions.cs | 10 +++--- src/Serilog.Sinks.File/Sinks/File/FileSink.cs | 32 +++++++++++-------- .../Sinks/File/RollingFileSink.cs | 11 ++++--- .../Sinks/File/SharedFileSink.AtomicAppend.cs | 8 ++--- .../Serilog.Sinks.File.Tests/FileSinkTests.cs | 6 ++-- .../Support/GZipHooks.cs | 4 +-- 7 files changed, 60 insertions(+), 43 deletions(-) diff --git a/src/Serilog.Sinks.File/FileLifecycleHooks.cs b/src/Serilog.Sinks.File/FileLifecycleHooks.cs index 3abe448..879f496 100644 --- a/src/Serilog.Sinks.File/FileLifecycleHooks.cs +++ b/src/Serilog.Sinks.File/FileLifecycleHooks.cs @@ -1,21 +1,37 @@ +// Copyright 2019 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.IO; namespace Serilog { - using System.IO; - /// - /// Enables hooking into log file lifecycle events + /// Enables hooking into log file lifecycle events. /// public abstract class FileLifecycleHooks { /// - /// Wraps in another stream, such as a GZipStream, then returns the wrapped stream + /// Initialize or wrap the opened on the log file. This can be used to write + /// file headers, or wrap the stream in another that adds buffering, compression, encryption, etc. The underlying + /// file may or may not be empty when this method is called. /// /// - /// Serilog is responsible for disposing of the wrapped stream + /// A value must be returned from overrides of this method. Serilog will flush and/or dispose the returned value, but will not + /// dispose the stream initially passed in unless it is itself returned. /// - /// The underlying log file stream - /// The wrapped stream - public abstract Stream Wrap(Stream underlyingStream); + /// The underlying opened on the log file. + /// The Serilog should use when writing events to the log file. + public virtual Stream OnOpened(Stream underlyingStream) => underlyingStream; } } diff --git a/src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs b/src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs index 2f7ab7a..88cf2a9 100644 --- a/src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs +++ b/src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs @@ -301,7 +301,7 @@ static LoggerConfiguration ConfigureFile( if (fileSizeLimitBytes.HasValue && fileSizeLimitBytes < 0) throw new ArgumentException("Negative value provided; file size limit must be non-negative.", nameof(fileSizeLimitBytes)); if (retainedFileCountLimit.HasValue && retainedFileCountLimit < 1) throw new ArgumentException("At least one file must be retained.", nameof(retainedFileCountLimit)); if (shared && buffered) throw new ArgumentException("Buffered writes are not available when file sharing is enabled.", nameof(buffered)); - if (shared && hooks != null) throw new ArgumentException("Unable to use {hooks.GetType().Name} FileLifecycleHooks output stream wrapper - these are not supported for shared log files", nameof(hooks)); + if (shared && hooks != null) throw new ArgumentException("File lifecycle hooks are not currently supported for shared log files.", nameof(hooks)); ILogEventSink sink; @@ -313,16 +313,16 @@ static LoggerConfiguration ConfigureFile( { try { -#pragma warning disable 618 if (shared) { - sink = new SharedFileSink(path, formatter, fileSizeLimitBytes); +#pragma warning disable 618 + sink = new SharedFileSink(path, formatter, fileSizeLimitBytes, encoding); +#pragma warning restore 618 } else { - sink = new FileSink(path, formatter, fileSizeLimitBytes, buffered: buffered, hooks: hooks); + sink = new FileSink(path, formatter, fileSizeLimitBytes, encoding, buffered, hooks); } -#pragma warning restore 618 } catch (Exception ex) { diff --git a/src/Serilog.Sinks.File/Sinks/File/FileSink.cs b/src/Serilog.Sinks.File/Sinks/File/FileSink.cs index 5b167cc..8d29eab 100644 --- a/src/Serilog.Sinks.File/Sinks/File/FileSink.cs +++ b/src/Serilog.Sinks.File/Sinks/File/FileSink.cs @@ -23,7 +23,6 @@ namespace Serilog.Sinks.File /// /// Write log events to a disk file. /// - [Obsolete("This type will be removed from the public API in a future version; use `WriteTo.File()` instead.")] public sealed class FileSink : IFileSink, IDisposable { readonly TextWriter _output; @@ -43,18 +42,27 @@ public sealed class FileSink : IFileSink, IDisposable /// 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. - /// Optionally enables hooking into log file lifecycle events. /// Configuration object allowing method chaining. - /// The file will be written using the UTF-8 character set. + /// This constructor preserves compatibility with early versions of the public API. New code should not depend on this type. /// - public FileSink(string path, ITextFormatter textFormatter, long? fileSizeLimitBytes, Encoding encoding = null, bool buffered = false, - FileLifecycleHooks hooks = null) + [Obsolete("This type and constructor will be removed from the public API in a future version; use `WriteTo.File()` instead.")] + public FileSink(string path, ITextFormatter textFormatter, long? fileSizeLimitBytes, Encoding encoding = null, bool buffered = false) + : this(path, textFormatter, fileSizeLimitBytes, encoding, buffered, null) + { + } + + // This overload should be used internally; the overload above maintains compatibility with the earlier public API. + internal FileSink( + string path, + ITextFormatter textFormatter, + long? fileSizeLimitBytes, + Encoding encoding, + bool buffered, + FileLifecycleHooks hooks) { 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."); - - _textFormatter = textFormatter; + _textFormatter = textFormatter ?? throw new ArgumentNullException(nameof(textFormatter)); _fileSizeLimitBytes = fileSizeLimitBytes; _buffered = buffered; @@ -72,12 +80,8 @@ public FileSink(string path, ITextFormatter textFormatter, long? fileSizeLimitBy if (hooks != null) { - outputStream = hooks.Wrap(outputStream); - - if (outputStream == null) - { - throw new InvalidOperationException($"{hooks.GetType().Name}.Wrap returned null when wrapping the output stream"); - } + outputStream = hooks.OnOpened(outputStream) ?? + throw new InvalidOperationException($"The file lifecycle hooks `{nameof(FileLifecycleHooks.OnOpened)}()` returned `null` when called with the output stream."); } _output = new StreamWriter(outputStream, encoding ?? new UTF8Encoding(encoderShouldEmitUTF8Identifier: false)); diff --git a/src/Serilog.Sinks.File/Sinks/File/RollingFileSink.cs b/src/Serilog.Sinks.File/Sinks/File/RollingFileSink.cs index bcb87f0..2db6f24 100644 --- a/src/Serilog.Sinks.File/Sinks/File/RollingFileSink.cs +++ b/src/Serilog.Sinks.File/Sinks/File/RollingFileSink.cs @@ -12,8 +12,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -#pragma warning disable 618 - using System; using System.IO; using System.Linq; @@ -52,11 +50,11 @@ public RollingFileSink(string path, bool shared, RollingInterval rollingInterval, bool rollOnFileSizeLimit, - FileLifecycleHooks hooks = null) + FileLifecycleHooks hooks) { 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"); - if (retainedFileCountLimit.HasValue && retainedFileCountLimit < 1) throw new ArgumentException("Zero or negative value provided; retained file count limit must be at least 1"); + if (fileSizeLimitBytes.HasValue && fileSizeLimitBytes < 0) throw new ArgumentException("Negative value provided; file size limit must be non-negative."); + if (retainedFileCountLimit.HasValue && retainedFileCountLimit < 1) throw new ArgumentException("Zero or negative value provided; retained file count limit must be at least 1."); _roller = new PathRoller(path, rollingInterval); _textFormatter = textFormatter; @@ -149,8 +147,11 @@ void OpenFile(DateTime now, int? minSequence = null) try { _currentFile = _shared ? +#pragma warning disable 618 (IFileSink)new SharedFileSink(path, _textFormatter, _fileSizeLimitBytes, _encoding) : +#pragma warning restore 618 new FileSink(path, _textFormatter, _fileSizeLimitBytes, _encoding, _buffered, _hooks); + _currentFileSequence = sequence; } catch (IOException ex) diff --git a/src/Serilog.Sinks.File/Sinks/File/SharedFileSink.AtomicAppend.cs b/src/Serilog.Sinks.File/Sinks/File/SharedFileSink.AtomicAppend.cs index 805e786..9934687 100644 --- a/src/Serilog.Sinks.File/Sinks/File/SharedFileSink.AtomicAppend.cs +++ b/src/Serilog.Sinks.File/Sinks/File/SharedFileSink.AtomicAppend.cs @@ -18,7 +18,6 @@ using System.IO; using System.Security.AccessControl; using System.Text; -using Serilog.Core; using Serilog.Events; using Serilog.Formatting; @@ -51,17 +50,14 @@ public sealed class SharedFileSink : IFileSink, IDisposable /// 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; + _path = path ?? throw new ArgumentNullException(nameof(path)); + _textFormatter = textFormatter ?? throw new ArgumentNullException(nameof(textFormatter)); _fileSizeLimitBytes = fileSizeLimitBytes; var directory = Path.GetDirectoryName(path); diff --git a/test/Serilog.Sinks.File.Tests/FileSinkTests.cs b/test/Serilog.Sinks.File.Tests/FileSinkTests.cs index e701594..94e0c63 100644 --- a/test/Serilog.Sinks.File.Tests/FileSinkTests.cs +++ b/test/Serilog.Sinks.File.Tests/FileSinkTests.cs @@ -153,7 +153,7 @@ public void WhenStreamWrapperIsSpecifiedOutputStreamIsWrapped() var nonexistent = tmp.AllocateFilename("txt"); var evt = Some.LogEvent("Hello, world!"); - using (var sink = new FileSink(nonexistent, new JsonFormatter(), null, hooks: gzipWrapper)) + using (var sink = new FileSink(nonexistent, new JsonFormatter(), null, null, false, gzipWrapper)) { sink.Emit(evt); sink.Emit(evt); @@ -161,7 +161,7 @@ public void WhenStreamWrapperIsSpecifiedOutputStreamIsWrapped() // Ensure the data was written through the wrapping GZipStream, by decompressing and comparing against // what we wrote - var lines = new List(); + List lines; using (var textStream = new MemoryStream()) { using (var fs = System.IO.File.OpenRead(nonexistent)) @@ -185,7 +185,7 @@ static void WriteTwoEventsAndCheckOutputFileLength(long? maxBytes, Encoding enco { var path = tmp.AllocateFilename("txt"); var evt = Some.LogEvent("Irrelevant as it will be replaced by the formatter"); - var actualEventOutput = "x"; + const string actualEventOutput = "x"; var formatter = new FixedOutputFormatter(actualEventOutput); var eventOuputLength = encoding.GetByteCount(actualEventOutput); diff --git a/test/Serilog.Sinks.File.Tests/Support/GZipHooks.cs b/test/Serilog.Sinks.File.Tests/Support/GZipHooks.cs index efb2319..330f97c 100644 --- a/test/Serilog.Sinks.File.Tests/Support/GZipHooks.cs +++ b/test/Serilog.Sinks.File.Tests/Support/GZipHooks.cs @@ -16,9 +16,9 @@ public GZipHooks(int bufferSize = 1024 * 32) _bufferSize = bufferSize; } - public override Stream Wrap(Stream sourceStream) + public override Stream OnOpened(Stream underlyingStream) { - var compressStream = new GZipStream(sourceStream, CompressionMode.Compress); + var compressStream = new GZipStream(underlyingStream, CompressionMode.Compress); return new BufferedStream(compressStream, _bufferSize); } } From b904968e0128ef2823b10ec52694eddc0a17d4e0 Mon Sep 17 00:00:00 2001 From: Nicholas Blumhardt Date: Mon, 22 Apr 2019 11:17:41 +1000 Subject: [PATCH 55/69] Reenable an old test that was lost --- .../FileLoggerConfigurationExtensionsTests.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/test/Serilog.Sinks.File.Tests/FileLoggerConfigurationExtensionsTests.cs b/test/Serilog.Sinks.File.Tests/FileLoggerConfigurationExtensionsTests.cs index cfa7ad7..6bc6a54 100644 --- a/test/Serilog.Sinks.File.Tests/FileLoggerConfigurationExtensionsTests.cs +++ b/test/Serilog.Sinks.File.Tests/FileLoggerConfigurationExtensionsTests.cs @@ -79,6 +79,14 @@ public void WhenFlushingToDiskReportedSharedFileSinkCanBeCreatedAndDisposed() } } + [Fact] + public void BufferingIsNotAvailableWhenSharingEnabled() + { + Assert.Throws(() => + new LoggerConfiguration() + .WriteTo.File("logs", buffered: true, shared: true)); + } + [Fact] public void HooksAreNotAvailableWhenSharingEnabled() { From b6090cc359df26b46bf65b9b23065ed9a9ed7c32 Mon Sep 17 00:00:00 2001 From: Nicholas Blumhardt Date: Mon, 22 Apr 2019 11:32:48 +1000 Subject: [PATCH 56/69] A test for the encoding fix --- .../FileLoggerConfigurationExtensionsTests.cs | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/test/Serilog.Sinks.File.Tests/FileLoggerConfigurationExtensionsTests.cs b/test/Serilog.Sinks.File.Tests/FileLoggerConfigurationExtensionsTests.cs index 6bc6a54..3dde37a 100644 --- a/test/Serilog.Sinks.File.Tests/FileLoggerConfigurationExtensionsTests.cs +++ b/test/Serilog.Sinks.File.Tests/FileLoggerConfigurationExtensionsTests.cs @@ -4,6 +4,7 @@ using Serilog.Tests.Support; using Xunit; using System.IO; +using System.Text; namespace Serilog.Sinks.File.Tests { @@ -94,5 +95,26 @@ public void HooksAreNotAvailableWhenSharingEnabled() new LoggerConfiguration() .WriteTo.File("logs", shared: true, hooks: new GZipHooks())); } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void SpecifiedEncodingIsPropagated(bool shared) + { + using (var tmp = TempFolder.ForCaller()) + { + var filename = tmp.AllocateFilename("txt"); + + using (var log = new LoggerConfiguration() + .WriteTo.File(filename, outputTemplate: "{Message}", encoding: Encoding.Unicode, shared: shared) + .CreateLogger()) + { + log.Information("ten chars."); + } + + // Don't forget the two-byte BOM :-) + Assert.Equal(22, System.IO.File.ReadAllBytes(filename).Length); + } + } } } From 34344ad386abc568f4691bc4094891bb1b3b9823 Mon Sep 17 00:00:00 2001 From: Nicholas Blumhardt Date: Mon, 22 Apr 2019 11:52:51 +1000 Subject: [PATCH 57/69] Move FileLifecycleHooks down under Serilog.Sinks.File - avoids namespace pollution when using Serilog logging APIs; mark PeriodicFlushToDiskSink as obsolete in line with other public types shared with Serilog.Sinks.RollingFile --- .../FileLoggerConfigurationExtensions.cs | 2 ++ .../{ => Sinks/File}/FileLifecycleHooks.cs | 2 +- .../Sinks/File/PeriodicFlushToDiskSink.cs | 26 ++++++++++++++----- .../Sinks/File/SharedFileSink.AtomicAppend.cs | 2 +- .../Sinks/File/SharedFileSink.OSMutex.cs | 8 +++--- .../Sinks/File/WriteCountingStream.cs | 18 ++++++------- 6 files changed, 34 insertions(+), 24 deletions(-) rename src/Serilog.Sinks.File/{ => Sinks/File}/FileLifecycleHooks.cs (98%) diff --git a/src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs b/src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs index 88cf2a9..944d32e 100644 --- a/src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs +++ b/src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs @@ -337,7 +337,9 @@ static LoggerConfiguration ConfigureFile( if (flushToDiskInterval.HasValue) { +#pragma warning disable 618 sink = new PeriodicFlushToDiskSink(sink, flushToDiskInterval.Value); +#pragma warning restore 618 } return addSink(sink, restrictedToMinimumLevel, levelSwitch); diff --git a/src/Serilog.Sinks.File/FileLifecycleHooks.cs b/src/Serilog.Sinks.File/Sinks/File/FileLifecycleHooks.cs similarity index 98% rename from src/Serilog.Sinks.File/FileLifecycleHooks.cs rename to src/Serilog.Sinks.File/Sinks/File/FileLifecycleHooks.cs index 879f496..d5e2219 100644 --- a/src/Serilog.Sinks.File/FileLifecycleHooks.cs +++ b/src/Serilog.Sinks.File/Sinks/File/FileLifecycleHooks.cs @@ -14,7 +14,7 @@ using System.IO; -namespace Serilog +namespace Serilog.Sinks.File { /// /// Enables hooking into log file lifecycle events. diff --git a/src/Serilog.Sinks.File/Sinks/File/PeriodicFlushToDiskSink.cs b/src/Serilog.Sinks.File/Sinks/File/PeriodicFlushToDiskSink.cs index cafb72e..66b0868 100644 --- a/src/Serilog.Sinks.File/Sinks/File/PeriodicFlushToDiskSink.cs +++ b/src/Serilog.Sinks.File/Sinks/File/PeriodicFlushToDiskSink.cs @@ -1,4 +1,18 @@ -using System; +// Copyright 2016-2019 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.Threading; using Serilog.Core; using Serilog.Debugging; @@ -9,6 +23,7 @@ namespace Serilog.Sinks.File /// /// A sink wrapper that periodically flushes the wrapped sink to disk. /// + [Obsolete("This type will be removed from the public API in a future version; use `WriteTo.File(flushToDiskInterval:)` instead.")] public class PeriodicFlushToDiskSink : ILogEventSink, IDisposable { readonly ILogEventSink _sink; @@ -21,15 +36,12 @@ public class PeriodicFlushToDiskSink : ILogEventSink, IDisposable /// /// 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; + _sink = sink ?? throw new ArgumentNullException(nameof(sink)); - var flushable = sink as IFlushableFileSink; - if (flushable != null) + if (sink is IFlushableFileSink flushable) { _timer = new Timer(_ => FlushToDisk(flushable), null, flushInterval, flushInterval); } diff --git a/src/Serilog.Sinks.File/Sinks/File/SharedFileSink.AtomicAppend.cs b/src/Serilog.Sinks.File/Sinks/File/SharedFileSink.AtomicAppend.cs index 9934687..866f807 100644 --- a/src/Serilog.Sinks.File/Sinks/File/SharedFileSink.AtomicAppend.cs +++ b/src/Serilog.Sinks.File/Sinks/File/SharedFileSink.AtomicAppend.cs @@ -1,4 +1,4 @@ -// Copyright 2013-2016 Serilog Contributors +// Copyright 2013-2019 Serilog Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/src/Serilog.Sinks.File/Sinks/File/SharedFileSink.OSMutex.cs b/src/Serilog.Sinks.File/Sinks/File/SharedFileSink.OSMutex.cs index a779bda..41a19ef 100644 --- a/src/Serilog.Sinks.File/Sinks/File/SharedFileSink.OSMutex.cs +++ b/src/Serilog.Sinks.File/Sinks/File/SharedFileSink.OSMutex.cs @@ -1,4 +1,4 @@ -// Copyright 2013-2016 Serilog Contributors +// Copyright 2013-2019 Serilog Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -17,7 +17,6 @@ using System; using System.IO; using System.Text; -using Serilog.Core; using Serilog.Events; using Serilog.Formatting; using System.Threading; @@ -28,6 +27,7 @@ namespace Serilog.Sinks.File /// /// Write log events to a disk file. /// + [Obsolete("This type will be removed from the public API in a future version; use `WriteTo.File(shared: true)` instead.")] public sealed class SharedFileSink : IFileSink, IDisposable { readonly TextWriter _output; @@ -53,11 +53,9 @@ public sealed class SharedFileSink : IFileSink, IDisposable 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"); - - _textFormatter = textFormatter; + _textFormatter = textFormatter ?? throw new ArgumentNullException(nameof(textFormatter)); _fileSizeLimitBytes = fileSizeLimitBytes; var directory = Path.GetDirectoryName(path); diff --git a/src/Serilog.Sinks.File/Sinks/File/WriteCountingStream.cs b/src/Serilog.Sinks.File/Sinks/File/WriteCountingStream.cs index da8f0dd..fe0d5d3 100644 --- a/src/Serilog.Sinks.File/Sinks/File/WriteCountingStream.cs +++ b/src/Serilog.Sinks.File/Sinks/File/WriteCountingStream.cs @@ -1,4 +1,4 @@ -// Copyright 2013-2016 Serilog Contributors +// Copyright 2013-2019 Serilog Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -20,16 +20,14 @@ 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; + _stream = stream ?? throw new ArgumentNullException(nameof(stream)); + CountedLength = stream.Length; } - public long CountedLength => _countedLength; + public long CountedLength { get; private set; } protected override void Dispose(bool disposing) { @@ -42,7 +40,7 @@ protected override void Dispose(bool disposing) public override void Write(byte[] buffer, int offset, int count) { _stream.Write(buffer, offset, count); - _countedLength += count; + CountedLength += count; } public override void Flush() => _stream.Flush(); @@ -54,8 +52,8 @@ public override void Write(byte[] buffer, int offset, int count) public override long Position { - get { return _stream.Position; } - set { throw new NotSupportedException(); } + get => _stream.Position; + set => throw new NotSupportedException(); } public override long Seek(long offset, SeekOrigin origin) @@ -73,4 +71,4 @@ public override int Read(byte[] buffer, int offset, int count) throw new NotSupportedException(); } } -} \ No newline at end of file +} From 0ae234e2e9ed044733fe70d7d573fc64dd769eb0 Mon Sep 17 00:00:00 2001 From: Nicholas Blumhardt Date: Mon, 22 Apr 2019 12:05:04 +1000 Subject: [PATCH 58/69] Enable hooks and encoding for auditing methods --- .../FileLoggerConfigurationExtensions.cs | 81 +++++++++++++++++-- 1 file changed, 76 insertions(+), 5 deletions(-) diff --git a/src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs b/src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs index 944d32e..2a66a79 100644 --- a/src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs +++ b/src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs @@ -199,7 +199,6 @@ public static LoggerConfiguration File( /// Character encoding used to write the text file. The default is UTF-8 without BOM. /// Optionally enables hooking into log file lifecycle events. /// Configuration object allowing method chaining. - /// The file will be written using the UTF-8 character set. public static LoggerConfiguration File( this LoggerSinkConfiguration sinkConfiguration, ITextFormatter formatter, @@ -235,20 +234,54 @@ public static LoggerConfiguration File( /// the default is "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception}". /// Configuration object allowing method chaining. /// The file will be written using the UTF-8 character set. + [Obsolete("New code should not be compiled against this obsolete overload"), EditorBrowsable(EditorBrowsableState.Never)] + public static LoggerConfiguration File( + this LoggerAuditSinkConfiguration sinkConfiguration, + string path, + LogEventLevel restrictedToMinimumLevel, + string outputTemplate, + IFormatProvider formatProvider, + LoggingLevelSwitch levelSwitch) + { + 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, null); + } + + /// + /// 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:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception}". + /// Character encoding used to write the text file. The default is UTF-8 without BOM. + /// Optionally enables hooking into log file lifecycle events. + /// Configuration object allowing method chaining. public static LoggerConfiguration File( this LoggerAuditSinkConfiguration sinkConfiguration, string path, LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum, string outputTemplate = DefaultOutputTemplate, IFormatProvider formatProvider = null, - LoggingLevelSwitch levelSwitch = null) + LoggingLevelSwitch levelSwitch = null, + Encoding encoding = null, + FileLifecycleHooks hooks = 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); + return File(sinkConfiguration, formatter, path, restrictedToMinimumLevel, levelSwitch, encoding, hooks); } /// @@ -267,15 +300,53 @@ public static LoggerConfiguration File( /// to be changed at runtime. /// Configuration object allowing method chaining. /// The file will be written using the UTF-8 character set. + [Obsolete("New code should not be compiled against this obsolete overload"), EditorBrowsable(EditorBrowsableState.Never)] + public static LoggerConfiguration File( + this LoggerAuditSinkConfiguration sinkConfiguration, + ITextFormatter formatter, + string path, + LogEventLevel restrictedToMinimumLevel, + LoggingLevelSwitch levelSwitch) + { + if (sinkConfiguration == null) throw new ArgumentNullException(nameof(sinkConfiguration)); + if (formatter == null) throw new ArgumentNullException(nameof(formatter)); + if (path == null) throw new ArgumentNullException(nameof(path)); + + return File(sinkConfiguration, formatter, path, restrictedToMinimumLevel, levelSwitch, null); + } + + /// + /// 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. + /// Character encoding used to write the text file. The default is UTF-8 without BOM. + /// Optionally enables hooking into log file lifecycle events. + /// Configuration object allowing method chaining. public static LoggerConfiguration File( this LoggerAuditSinkConfiguration sinkConfiguration, ITextFormatter formatter, string path, LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum, - LoggingLevelSwitch levelSwitch = null) + LoggingLevelSwitch levelSwitch = null, + Encoding encoding = null, + FileLifecycleHooks hooks = null) { + if (sinkConfiguration == null) throw new ArgumentNullException(nameof(sinkConfiguration)); + if (formatter == null) throw new ArgumentNullException(nameof(formatter)); + if (path == null) throw new ArgumentNullException(nameof(path)); + return ConfigureFile(sinkConfiguration.Sink, formatter, path, restrictedToMinimumLevel, null, levelSwitch, false, true, - false, null, null, RollingInterval.Infinite, false, null, null); + false, null, encoding, RollingInterval.Infinite, false, null, hooks); } static LoggerConfiguration ConfigureFile( From 55fcb2b32ece5f765b27dccfe1c6609c1415a34e Mon Sep 17 00:00:00 2001 From: Nicholas Blumhardt Date: Mon, 22 Apr 2019 12:26:48 +1000 Subject: [PATCH 59/69] Add backwards-compatible configuration overloads --- .../FileLoggerConfigurationExtensions.cs | 176 +++++++++++++----- 1 file changed, 131 insertions(+), 45 deletions(-) diff --git a/src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs b/src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs index 2a66a79..490803d 100644 --- a/src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs +++ b/src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs @@ -24,7 +24,7 @@ using Serilog.Formatting.Json; using Serilog.Sinks.File; -// ReSharper disable MethodOverloadWithOptionalParameter +// ReSharper disable RedundantArgumentDefaultValue, MethodOverloadWithOptionalParameter namespace Serilog { @@ -69,10 +69,8 @@ public static LoggerConfiguration File( bool shared, TimeSpan? flushToDiskInterval) { - // ReSharper disable once RedundantArgumentDefaultValue return File(sinkConfiguration, path, restrictedToMinimumLevel, outputTemplate, formatProvider, fileSizeLimitBytes, - levelSwitch, buffered, shared, flushToDiskInterval, RollingInterval.Infinite, false, - null, null); + levelSwitch, buffered, shared, flushToDiskInterval, RollingInterval.Infinite, false, null, null, null); } /// @@ -110,9 +108,103 @@ public static LoggerConfiguration File( bool shared, TimeSpan? flushToDiskInterval) { - // ReSharper disable once RedundantArgumentDefaultValue return File(sinkConfiguration, formatter, path, restrictedToMinimumLevel, fileSizeLimitBytes, levelSwitch, - buffered, shared, flushToDiskInterval, RollingInterval.Infinite, false, null, null); + buffered, shared, flushToDiskInterval, RollingInterval.Infinite, false, null, null, null); + } + + /// + /// 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:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception}". + /// 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. + /// If provided, a full disk flush will be performed periodically at the specified interval. + /// The interval at which logging will roll over to a new file. + /// If true, a new file will be created when the file size limit is reached. Filenames + /// will have a number appended in the format _NNN, with the first filename given no number. + /// The maximum number of log files that will be retained, + /// including the current log file. For unlimited retention, pass null. The default is 31. + /// Character encoding used to write the text file. The default is UTF-8 without BOM. + /// Configuration object allowing method chaining. + [Obsolete("New code should not be compiled against this obsolete overload"), EditorBrowsable(EditorBrowsableState.Never)] + public static LoggerConfiguration File( + this LoggerSinkConfiguration sinkConfiguration, + string path, + LogEventLevel restrictedToMinimumLevel, + string outputTemplate, + IFormatProvider formatProvider, + long? fileSizeLimitBytes, + LoggingLevelSwitch levelSwitch, + bool buffered, + bool shared, + TimeSpan? flushToDiskInterval, + RollingInterval rollingInterval, + bool rollOnFileSizeLimit, + int? retainedFileCountLimit, + Encoding encoding) + { + return File(sinkConfiguration, path, restrictedToMinimumLevel, outputTemplate, formatProvider, fileSizeLimitBytes, levelSwitch, buffered, + shared, flushToDiskInterval, rollingInterval, rollOnFileSizeLimit, retainedFileCountLimit, encoding, null); + } + + /// + /// 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. + /// 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. + /// If provided, a full disk flush will be performed periodically at the specified interval. + /// The interval at which logging will roll over to a new file. + /// If true, a new file will be created when the file size limit is reached. Filenames + /// will have a number appended in the format _NNN, with the first filename given no number. + /// The maximum number of log files that will be retained, + /// including the current log file. For unlimited retention, pass null. The default is 31. + /// Character encoding used to write the text file. The default is UTF-8 without BOM. + /// Configuration object allowing method chaining. + [Obsolete("New code should not be compiled against this obsolete overload"), EditorBrowsable(EditorBrowsableState.Never)] + public static LoggerConfiguration File( + this LoggerSinkConfiguration sinkConfiguration, + ITextFormatter formatter, + string path, + LogEventLevel restrictedToMinimumLevel, + long? fileSizeLimitBytes, + LoggingLevelSwitch levelSwitch, + bool buffered, + bool shared, + TimeSpan? flushToDiskInterval, + RollingInterval rollingInterval, + bool rollOnFileSizeLimit, + int? retainedFileCountLimit, + Encoding encoding) + { + return File(sinkConfiguration, formatter, path, restrictedToMinimumLevel, fileSizeLimitBytes, levelSwitch, buffered, + shared, flushToDiskInterval, rollingInterval, rollOnFileSizeLimit, retainedFileCountLimit, encoding, null); } /// @@ -142,7 +234,6 @@ public static LoggerConfiguration File( /// Character encoding used to write the text file. The default is UTF-8 without BOM. /// Optionally enables hooking into log file lifecycle events. /// Configuration object allowing method chaining. - /// The file will be written using the UTF-8 character set. public static LoggerConfiguration File( this LoggerSinkConfiguration sinkConfiguration, string path, @@ -215,6 +306,10 @@ public static LoggerConfiguration File( Encoding encoding = null, FileLifecycleHooks hooks = null) { + if (sinkConfiguration == null) throw new ArgumentNullException(nameof(sinkConfiguration)); + if (formatter == null) throw new ArgumentNullException(nameof(formatter)); + if (path == null) throw new ArgumentNullException(nameof(path)); + return ConfigureFile(sinkConfiguration.Sink, formatter, path, restrictedToMinimumLevel, fileSizeLimitBytes, levelSwitch, buffered, false, shared, flushToDiskInterval, encoding, rollingInterval, rollOnFileSizeLimit, retainedFileCountLimit, hooks); @@ -243,80 +338,71 @@ public static LoggerConfiguration File( IFormatProvider formatProvider, LoggingLevelSwitch levelSwitch) { - 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, null); + return File(sinkConfiguration, path, restrictedToMinimumLevel, outputTemplate, formatProvider, levelSwitch, null, null); } /// /// 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. - /// Supplies culture-specific formatting information, or null. - /// A message template describing the format used to write to the sink. - /// the default is "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception}". - /// Character encoding used to write the text file. The default is UTF-8 without BOM. - /// Optionally enables hooking into log file lifecycle events. /// Configuration object allowing method chaining. + /// The file will be written using the UTF-8 character set. + [Obsolete("New code should not be compiled against this obsolete overload"), EditorBrowsable(EditorBrowsableState.Never)] public static LoggerConfiguration File( this LoggerAuditSinkConfiguration sinkConfiguration, + ITextFormatter formatter, string path, - LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum, - string outputTemplate = DefaultOutputTemplate, - IFormatProvider formatProvider = null, - LoggingLevelSwitch levelSwitch = null, - Encoding encoding = null, - FileLifecycleHooks hooks = null) + LogEventLevel restrictedToMinimumLevel, + LoggingLevelSwitch levelSwitch) { - 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, encoding, hooks); + return File(sinkConfiguration, formatter, path, restrictedToMinimumLevel, levelSwitch, null, null); } - + /// - /// Write log events to the specified file. + /// Write audit 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. + /// Supplies culture-specific formatting information, or null. + /// A message template describing the format used to write to the sink. + /// the default is "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception}". + /// Character encoding used to write the text file. The default is UTF-8 without BOM. + /// Optionally enables hooking into log file lifecycle events. /// Configuration object allowing method chaining. - /// The file will be written using the UTF-8 character set. - [Obsolete("New code should not be compiled against this obsolete overload"), EditorBrowsable(EditorBrowsableState.Never)] public static LoggerConfiguration File( this LoggerAuditSinkConfiguration sinkConfiguration, - ITextFormatter formatter, string path, - LogEventLevel restrictedToMinimumLevel, - LoggingLevelSwitch levelSwitch) + LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum, + string outputTemplate = DefaultOutputTemplate, + IFormatProvider formatProvider = null, + LoggingLevelSwitch levelSwitch = null, + Encoding encoding = null, + FileLifecycleHooks hooks = null) { if (sinkConfiguration == null) throw new ArgumentNullException(nameof(sinkConfiguration)); - if (formatter == null) throw new ArgumentNullException(nameof(formatter)); if (path == null) throw new ArgumentNullException(nameof(path)); + if (outputTemplate == null) throw new ArgumentNullException(nameof(outputTemplate)); - return File(sinkConfiguration, formatter, path, restrictedToMinimumLevel, levelSwitch, null); + var formatter = new MessageTemplateTextFormatter(outputTemplate, formatProvider); + return File(sinkConfiguration, formatter, path, restrictedToMinimumLevel, levelSwitch, encoding, hooks); } /// - /// Write log events to the specified file. + /// Write audit log events to the specified file. /// /// Logger sink configuration. /// A formatter, such as , to convert the log events into From d4fa80a83ffad45781b79d416e9bf0df2d3f8a55 Mon Sep 17 00:00:00 2001 From: Nicholas Blumhardt Date: Mon, 22 Apr 2019 13:32:19 +1000 Subject: [PATCH 60/69] Expose encoding to OnOpened() hook; switch to AppVeyor Linux builds --- README.md | 2 +- appveyor.yml | 13 ++++++++++--- build.sh | 10 ++++++++-- serilog-sinks-file.sln | 1 - .../Sinks/File/FileLifecycleHooks.cs | 4 +++- src/Serilog.Sinks.File/Sinks/File/FileSink.cs | 9 ++++++--- test/Serilog.Sinks.File.Tests/Support/GZipHooks.cs | 3 ++- 7 files changed, 30 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 9e4f19f..4267d32 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Serilog.Sinks.File [![Build status](https://ci.appveyor.com/api/projects/status/hh9gymy0n6tne46j?svg=true)](https://ci.appveyor.com/project/serilog/serilog-sinks-file) [![Travis build](https://travis-ci.org/serilog/serilog-sinks-file.svg)](https://travis-ci.org/serilog/serilog-sinks-file) [![NuGet Version](http://img.shields.io/nuget/v/Serilog.Sinks.File.svg?style=flat)](https://www.nuget.org/packages/Serilog.Sinks.File/) [![Documentation](https://img.shields.io/badge/docs-wiki-yellow.svg)](https://github.com/serilog/serilog/wiki) [![Join the chat at https://gitter.im/serilog/serilog](https://img.shields.io/gitter/room/serilog/serilog.svg)](https://gitter.im/serilog/serilog) +# Serilog.Sinks.File [![Build status](https://ci.appveyor.com/api/projects/status/hh9gymy0n6tne46j?svg=true)](https://ci.appveyor.com/project/serilog/serilog-sinks-file) [![NuGet Version](http://img.shields.io/nuget/v/Serilog.Sinks.File.svg?style=flat)](https://www.nuget.org/packages/Serilog.Sinks.File/) [![Documentation](https://img.shields.io/badge/docs-wiki-yellow.svg)](https://github.com/serilog/serilog/wiki) [![Join the chat at https://gitter.im/serilog/serilog](https://img.shields.io/gitter/room/serilog/serilog.svg)](https://gitter.im/serilog/serilog) Writes [Serilog](https://serilog.net) events to one or more text files. diff --git a/appveyor.yml b/appveyor.yml index 71f05e3..67a6f2b 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,11 +1,18 @@ version: '{build}' skip_tags: true -image: Visual Studio 2017 +image: + - Visual Studio 2017 + - Ubuntu configuration: Release -install: - - ps: mkdir -Force ".\build\" | Out-Null build_script: - ps: ./Build.ps1 +for: +- + matrix: + only: + - image: Ubuntu + build_script: + - sh build.sh test: off artifacts: - path: artifacts/Serilog.*.nupkg diff --git a/build.sh b/build.sh index 56d265b..bd380e7 100755 --- a/build.sh +++ b/build.sh @@ -1,11 +1,17 @@ -#!/bin/bash +#!/bin/bash + +set -e dotnet --info +dotnet --list-sdks dotnet restore +echo "🤖 Attempting to build..." for path in src/**/*.csproj; do - dotnet build -f netstandard1.3 -c Release ${path} -p:EnableSourceLink=true + dotnet build -f netstandard1.0 -c Release ${path} + dotnet build -f netstandard1.3 -c Release ${path} done +echo "🤖 Running tests..." for path in test/*.Tests/*.csproj; do dotnet test -f netcoreapp2.0 -c Release ${path} done diff --git a/serilog-sinks-file.sln b/serilog-sinks-file.sln index 71527e4..9c33a2b 100644 --- a/serilog-sinks-file.sln +++ b/serilog-sinks-file.sln @@ -8,7 +8,6 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "assets", "assets", "{E9D1B5E1-DEB9-4A04-8BAB-24EC7240ADAF}" ProjectSection(SolutionItems) = preProject .editorconfig = .editorconfig - .travis.yml = .travis.yml appveyor.yml = appveyor.yml Build.ps1 = Build.ps1 build.sh = build.sh diff --git a/src/Serilog.Sinks.File/Sinks/File/FileLifecycleHooks.cs b/src/Serilog.Sinks.File/Sinks/File/FileLifecycleHooks.cs index d5e2219..cae8538 100644 --- a/src/Serilog.Sinks.File/Sinks/File/FileLifecycleHooks.cs +++ b/src/Serilog.Sinks.File/Sinks/File/FileLifecycleHooks.cs @@ -13,6 +13,7 @@ // limitations under the License. using System.IO; +using System.Text; namespace Serilog.Sinks.File { @@ -31,7 +32,8 @@ public abstract class FileLifecycleHooks /// dispose the stream initially passed in unless it is itself returned. /// /// The underlying opened on the log file. + /// The encoding to use when reading/writing to the stream. /// The Serilog should use when writing events to the log file. - public virtual Stream OnOpened(Stream underlyingStream) => underlyingStream; + public virtual Stream OnOpened(Stream underlyingStream, Encoding encoding) => underlyingStream; } } diff --git a/src/Serilog.Sinks.File/Sinks/File/FileSink.cs b/src/Serilog.Sinks.File/Sinks/File/FileSink.cs index 8d29eab..0b0e497 100644 --- a/src/Serilog.Sinks.File/Sinks/File/FileSink.cs +++ b/src/Serilog.Sinks.File/Sinks/File/FileSink.cs @@ -78,13 +78,16 @@ internal FileSink( outputStream = _countingStreamWrapper = new WriteCountingStream(_underlyingStream); } + // Parameter reassignment. + encoding = encoding ?? new UTF8Encoding(encoderShouldEmitUTF8Identifier: false); + if (hooks != null) { - outputStream = hooks.OnOpened(outputStream) ?? - throw new InvalidOperationException($"The file lifecycle hooks `{nameof(FileLifecycleHooks.OnOpened)}()` returned `null` when called with the output stream."); + outputStream = hooks.OnOpened(outputStream, encoding) ?? + throw new InvalidOperationException($"The file lifecycle hook `{nameof(FileLifecycleHooks.OnOpened)}(...)` returned `null`."); } - _output = new StreamWriter(outputStream, encoding ?? new UTF8Encoding(encoderShouldEmitUTF8Identifier: false)); + _output = new StreamWriter(outputStream, encoding); } bool IFileSink.EmitOrOverflow(LogEvent logEvent) diff --git a/test/Serilog.Sinks.File.Tests/Support/GZipHooks.cs b/test/Serilog.Sinks.File.Tests/Support/GZipHooks.cs index 330f97c..64f57e1 100644 --- a/test/Serilog.Sinks.File.Tests/Support/GZipHooks.cs +++ b/test/Serilog.Sinks.File.Tests/Support/GZipHooks.cs @@ -1,5 +1,6 @@ using System.IO; using System.IO.Compression; +using System.Text; namespace Serilog.Sinks.File.Tests.Support { @@ -16,7 +17,7 @@ public GZipHooks(int bufferSize = 1024 * 32) _bufferSize = bufferSize; } - public override Stream OnOpened(Stream underlyingStream) + public override Stream OnOpened(Stream underlyingStream, Encoding _) { var compressStream = new GZipStream(underlyingStream, CompressionMode.Compress); return new BufferedStream(compressStream, _bufferSize); From 9f7352d7da81c1f84974e90b59766e3edfd9bd69 Mon Sep 17 00:00:00 2001 From: Nicholas Blumhardt Date: Mon, 22 Apr 2019 13:37:41 +1000 Subject: [PATCH 61/69] Fix Linux build script targets --- build.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sh b/build.sh index bd380e7..3bb5fec 100755 --- a/build.sh +++ b/build.sh @@ -7,8 +7,8 @@ dotnet restore echo "🤖 Attempting to build..." for path in src/**/*.csproj; do - dotnet build -f netstandard1.0 -c Release ${path} dotnet build -f netstandard1.3 -c Release ${path} + dotnet build -f netstandard2.0 -c Release ${path} done echo "🤖 Running tests..." From ca0ac8c9f56d722f29c8dd512fb342c138b819fb Mon Sep 17 00:00:00 2001 From: Nicholas Blumhardt Date: Mon, 22 Apr 2019 14:00:00 +1000 Subject: [PATCH 62/69] Test for header writing --- .../Serilog.Sinks.File.Tests/FileSinkTests.cs | 35 ++++++++++++++++--- .../Support/FileHeaderWriter.cs | 28 +++++++++++++++ 2 files changed, 59 insertions(+), 4 deletions(-) create mode 100644 test/Serilog.Sinks.File.Tests/Support/FileHeaderWriter.cs diff --git a/test/Serilog.Sinks.File.Tests/FileSinkTests.cs b/test/Serilog.Sinks.File.Tests/FileSinkTests.cs index 94e0c63..2d0f210 100644 --- a/test/Serilog.Sinks.File.Tests/FileSinkTests.cs +++ b/test/Serilog.Sinks.File.Tests/FileSinkTests.cs @@ -144,16 +144,16 @@ public void WhenLimitIsNotSpecifiedAndEncodingHasNoPreambleDataIsCorrectlyAppend } [Fact] - public void WhenStreamWrapperIsSpecifiedOutputStreamIsWrapped() + public void OnOpenedLifecycleHookCanWrapUnderlyingStream() { var gzipWrapper = new GZipHooks(); using (var tmp = TempFolder.ForCaller()) { - var nonexistent = tmp.AllocateFilename("txt"); + var path = tmp.AllocateFilename("txt"); var evt = Some.LogEvent("Hello, world!"); - using (var sink = new FileSink(nonexistent, new JsonFormatter(), null, null, false, gzipWrapper)) + using (var sink = new FileSink(path, new JsonFormatter(), null, null, false, gzipWrapper)) { sink.Emit(evt); sink.Emit(evt); @@ -164,7 +164,7 @@ public void WhenStreamWrapperIsSpecifiedOutputStreamIsWrapped() List lines; using (var textStream = new MemoryStream()) { - using (var fs = System.IO.File.OpenRead(nonexistent)) + using (var fs = System.IO.File.OpenRead(path)) using (var decompressStream = new GZipStream(fs, CompressionMode.Decompress)) { decompressStream.CopyTo(textStream); @@ -179,6 +179,33 @@ public void WhenStreamWrapperIsSpecifiedOutputStreamIsWrapped() } } + [Fact] + public static void OnOpenedLifecycleHookCanWriteFileHeader() + { + using (var tmp = TempFolder.ForCaller()) + { + var headerWriter = new FileHeaderWriter("This is the file header"); + + var path = tmp.AllocateFilename("txt"); + using (new FileSink(path, new JsonFormatter(), null, new UTF8Encoding(false), false, headerWriter)) + { + // Open and write header + } + + using (var sink = new FileSink(path, new JsonFormatter(), null, new UTF8Encoding(false), false, headerWriter)) + { + // Length check should prevent duplicate header here + sink.Emit(Some.LogEvent()); + } + + var lines = System.IO.File.ReadAllLines(path); + + Assert.Equal(2, lines.Length); + Assert.Equal(headerWriter.Header, lines[0]); + Assert.Equal('{', lines[1][0]); + } + } + static void WriteTwoEventsAndCheckOutputFileLength(long? maxBytes, Encoding encoding) { using (var tmp = TempFolder.ForCaller()) diff --git a/test/Serilog.Sinks.File.Tests/Support/FileHeaderWriter.cs b/test/Serilog.Sinks.File.Tests/Support/FileHeaderWriter.cs new file mode 100644 index 0000000..83937c0 --- /dev/null +++ b/test/Serilog.Sinks.File.Tests/Support/FileHeaderWriter.cs @@ -0,0 +1,28 @@ +using System.IO; +using System.Text; + +namespace Serilog.Sinks.File.Tests.Support +{ + class FileHeaderWriter : FileLifecycleHooks + { + public string Header { get; } + + public FileHeaderWriter(string header) + { + Header = header; + } + + public override Stream OnOpened(Stream underlyingStream, Encoding encoding) + { + if (underlyingStream.Length == 0) + { + var writer = new StreamWriter(underlyingStream, encoding); + writer.WriteLine(Header); + writer.Flush(); + underlyingStream.Flush(); + } + + return base.OnOpened(underlyingStream, encoding); + } + } +} From 5b3d64aac5af752476a087c2f7d3e3e553dadb36 Mon Sep 17 00:00:00 2001 From: Nicholas Blumhardt Date: Mon, 22 Apr 2019 20:21:42 +1000 Subject: [PATCH 63/69] OnOpened() -> OnFileOpened() --- src/Serilog.Sinks.File/Sinks/File/FileLifecycleHooks.cs | 2 +- src/Serilog.Sinks.File/Sinks/File/FileSink.cs | 4 ++-- test/Serilog.Sinks.File.Tests/Support/FileHeaderWriter.cs | 4 ++-- test/Serilog.Sinks.File.Tests/Support/GZipHooks.cs | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Serilog.Sinks.File/Sinks/File/FileLifecycleHooks.cs b/src/Serilog.Sinks.File/Sinks/File/FileLifecycleHooks.cs index cae8538..3dbddeb 100644 --- a/src/Serilog.Sinks.File/Sinks/File/FileLifecycleHooks.cs +++ b/src/Serilog.Sinks.File/Sinks/File/FileLifecycleHooks.cs @@ -34,6 +34,6 @@ public abstract class FileLifecycleHooks /// The underlying opened on the log file. /// The encoding to use when reading/writing to the stream. /// The Serilog should use when writing events to the log file. - public virtual Stream OnOpened(Stream underlyingStream, Encoding encoding) => underlyingStream; + public virtual Stream OnFileOpened(Stream underlyingStream, Encoding encoding) => underlyingStream; } } diff --git a/src/Serilog.Sinks.File/Sinks/File/FileSink.cs b/src/Serilog.Sinks.File/Sinks/File/FileSink.cs index 0b0e497..8a913fa 100644 --- a/src/Serilog.Sinks.File/Sinks/File/FileSink.cs +++ b/src/Serilog.Sinks.File/Sinks/File/FileSink.cs @@ -83,8 +83,8 @@ internal FileSink( if (hooks != null) { - outputStream = hooks.OnOpened(outputStream, encoding) ?? - throw new InvalidOperationException($"The file lifecycle hook `{nameof(FileLifecycleHooks.OnOpened)}(...)` returned `null`."); + outputStream = hooks.OnFileOpened(outputStream, encoding) ?? + throw new InvalidOperationException($"The file lifecycle hook `{nameof(FileLifecycleHooks.OnFileOpened)}(...)` returned `null`."); } _output = new StreamWriter(outputStream, encoding); diff --git a/test/Serilog.Sinks.File.Tests/Support/FileHeaderWriter.cs b/test/Serilog.Sinks.File.Tests/Support/FileHeaderWriter.cs index 83937c0..ae90604 100644 --- a/test/Serilog.Sinks.File.Tests/Support/FileHeaderWriter.cs +++ b/test/Serilog.Sinks.File.Tests/Support/FileHeaderWriter.cs @@ -12,7 +12,7 @@ public FileHeaderWriter(string header) Header = header; } - public override Stream OnOpened(Stream underlyingStream, Encoding encoding) + public override Stream OnFileOpened(Stream underlyingStream, Encoding encoding) { if (underlyingStream.Length == 0) { @@ -22,7 +22,7 @@ public override Stream OnOpened(Stream underlyingStream, Encoding encoding) underlyingStream.Flush(); } - return base.OnOpened(underlyingStream, encoding); + return base.OnFileOpened(underlyingStream, encoding); } } } diff --git a/test/Serilog.Sinks.File.Tests/Support/GZipHooks.cs b/test/Serilog.Sinks.File.Tests/Support/GZipHooks.cs index 64f57e1..40a77bb 100644 --- a/test/Serilog.Sinks.File.Tests/Support/GZipHooks.cs +++ b/test/Serilog.Sinks.File.Tests/Support/GZipHooks.cs @@ -17,7 +17,7 @@ public GZipHooks(int bufferSize = 1024 * 32) _bufferSize = bufferSize; } - public override Stream OnOpened(Stream underlyingStream, Encoding _) + public override Stream OnFileOpened(Stream underlyingStream, Encoding _) { var compressStream = new GZipStream(underlyingStream, CompressionMode.Compress); return new BufferedStream(compressStream, _bufferSize); From e420a360b1744373eb09c68925246e9932b25dad Mon Sep 17 00:00:00 2001 From: Nicholas Blumhardt Date: Tue, 23 Apr 2019 08:18:54 +1000 Subject: [PATCH 64/69] Small clarification of size limiting behavior [skip ci] --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 4267d32..5a78565 100644 --- a/README.md +++ b/README.md @@ -30,11 +30,13 @@ log20180702.txt ### Limits -To avoid bringing down apps with runaway disk usage the file sink **limits file size to 1GB by default**. The limit can be increased or removed using the `fileSizeLimitBytes` parameter. +To avoid bringing down apps with runaway disk usage the file sink **limits file size to 1GB by default**. Once the limit is reached, no further events will be written until the next roll point (see also: [Rolling policies](#rolling-policies) below). + +The limit can be changed or removed using the `fileSizeLimitBytes` parameter. ```csharp .WriteTo.File("log.txt", fileSizeLimitBytes: null) -``` +``` For the same reason, only **the most recent 31 files** are retained by default (i.e. one long month). To change or remove this limit, pass the `retainedFileCountLimit` parameter. From 9f8bfc3016f74729e3f5be7d5ffd4c9978475684 Mon Sep 17 00:00:00 2001 From: Matthew Erbs Date: Fri, 3 May 2019 07:52:19 +1000 Subject: [PATCH 65/69] Update NuGet API Key --- appveyor.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index 67a6f2b..79ee987 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -19,7 +19,7 @@ artifacts: deploy: - provider: NuGet api_key: - secure: bd9z4P73oltOXudAjPehwp9iDKsPtC+HbgshOrSgoyQKr5xVK+bxJQngrDJkHdY8 + secure: N59tiJECUYpip6tEn0xvdmDAEiP9SIzyLEFLpwiigm/8WhJvBNs13QxzT1/3/JW/ skip_symbols: true on: branch: /^(master|dev)$/ From 57d96c2c263aff760997fc68fc3e2e4c3358a923 Mon Sep 17 00:00:00 2001 From: Marcin_Osada Date: Mon, 8 Jul 2019 13:14:19 +0200 Subject: [PATCH 66/69] Add OnFileRemoving life cycle hook #106 --- .../Sinks/File/FileLifecycleHooks.cs | 14 ++++++++++++++ .../Sinks/File/RollingFileSink.cs | 5 +++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/Serilog.Sinks.File/Sinks/File/FileLifecycleHooks.cs b/src/Serilog.Sinks.File/Sinks/File/FileLifecycleHooks.cs index 3dbddeb..bda4c63 100644 --- a/src/Serilog.Sinks.File/Sinks/File/FileLifecycleHooks.cs +++ b/src/Serilog.Sinks.File/Sinks/File/FileLifecycleHooks.cs @@ -35,5 +35,19 @@ public abstract class FileLifecycleHooks /// The encoding to use when reading/writing to the stream. /// The Serilog should use when writing events to the log file. public virtual Stream OnFileOpened(Stream underlyingStream, Encoding encoding) => underlyingStream; + + /// + /// Method called on log files that were marked as obsolete (old) and by default would be deleted. + /// This can be used to move old logs to archive location or send to backup server + /// + /// + /// Executing long synchronous operation may affect responsiveness of application + /// + /// Log file full path + /// + /// Return if Serilog should delete file. + /// Warning: returning false and keeping file in same place will result in calling this method again in next scan for obsolete files + /// + public virtual bool OnFileRemoving(string fullPath) => true; } } diff --git a/src/Serilog.Sinks.File/Sinks/File/RollingFileSink.cs b/src/Serilog.Sinks.File/Sinks/File/RollingFileSink.cs index 2db6f24..fd81a54 100644 --- a/src/Serilog.Sinks.File/Sinks/File/RollingFileSink.cs +++ b/src/Serilog.Sinks.File/Sinks/File/RollingFileSink.cs @@ -199,11 +199,12 @@ void ApplyRetentionPolicy(string currentFilePath) var fullPath = Path.Combine(_roller.LogFileDirectory, obsolete); try { - System.IO.File.Delete(fullPath); + if (_hooks == null || _hooks.OnFileRemoving(fullPath)) + System.IO.File.Delete(fullPath); } catch (Exception ex) { - SelfLog.WriteLine("Error {0} while removing obsolete log file {1}", ex, fullPath); + SelfLog.WriteLine("Error {0} while processing obsolete log file {1}", ex, fullPath); } } } From 4f1ae37474c229b016d1858f075797a39694bdf1 Mon Sep 17 00:00:00 2001 From: Marcin_Osada Date: Wed, 10 Jul 2019 10:54:22 +0200 Subject: [PATCH 67/69] OnFileRemoving will not interfere with Serilog delete --- .../Sinks/File/FileLifecycleHooks.cs | 10 +++------- src/Serilog.Sinks.File/Sinks/File/RollingFileSink.cs | 5 +++-- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/src/Serilog.Sinks.File/Sinks/File/FileLifecycleHooks.cs b/src/Serilog.Sinks.File/Sinks/File/FileLifecycleHooks.cs index bda4c63..5949219 100644 --- a/src/Serilog.Sinks.File/Sinks/File/FileLifecycleHooks.cs +++ b/src/Serilog.Sinks.File/Sinks/File/FileLifecycleHooks.cs @@ -37,17 +37,13 @@ public abstract class FileLifecycleHooks public virtual Stream OnFileOpened(Stream underlyingStream, Encoding encoding) => underlyingStream; /// - /// Method called on log files that were marked as obsolete (old) and by default would be deleted. - /// This can be used to move old logs to archive location or send to backup server + /// Method called on log files that were marked as obsolete (old) and would be deleted. + /// This can be used to copy old logs to archive location or send to backup server /// /// /// Executing long synchronous operation may affect responsiveness of application /// /// Log file full path - /// - /// Return if Serilog should delete file. - /// Warning: returning false and keeping file in same place will result in calling this method again in next scan for obsolete files - /// - public virtual bool OnFileRemoving(string fullPath) => true; + public virtual void OnFileRemoving(string fullPath) {} } } diff --git a/src/Serilog.Sinks.File/Sinks/File/RollingFileSink.cs b/src/Serilog.Sinks.File/Sinks/File/RollingFileSink.cs index fd81a54..3b0ac61 100644 --- a/src/Serilog.Sinks.File/Sinks/File/RollingFileSink.cs +++ b/src/Serilog.Sinks.File/Sinks/File/RollingFileSink.cs @@ -199,8 +199,9 @@ void ApplyRetentionPolicy(string currentFilePath) var fullPath = Path.Combine(_roller.LogFileDirectory, obsolete); try { - if (_hooks == null || _hooks.OnFileRemoving(fullPath)) - System.IO.File.Delete(fullPath); + if (_hooks != null) _hooks.OnFileRemoving(fullPath); + + System.IO.File.Delete(fullPath); } catch (Exception ex) { From 735efa9699035e979c73c3f4970ae0ab41ebabd7 Mon Sep 17 00:00:00 2001 From: Marcin_Osada Date: Fri, 12 Jul 2019 12:01:11 +0200 Subject: [PATCH 68/69] Update naming and summaries --- .../Sinks/File/FileLifecycleHooks.cs | 12 +++++------- src/Serilog.Sinks.File/Sinks/File/RollingFileSink.cs | 3 +-- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/src/Serilog.Sinks.File/Sinks/File/FileLifecycleHooks.cs b/src/Serilog.Sinks.File/Sinks/File/FileLifecycleHooks.cs index 5949219..fbaf133 100644 --- a/src/Serilog.Sinks.File/Sinks/File/FileLifecycleHooks.cs +++ b/src/Serilog.Sinks.File/Sinks/File/FileLifecycleHooks.cs @@ -19,6 +19,7 @@ namespace Serilog.Sinks.File { /// /// Enables hooking into log file lifecycle events. + /// Hooks run synchronously and therefore may affect responsiveness of the application if long operations are performed. /// public abstract class FileLifecycleHooks { @@ -37,13 +38,10 @@ public abstract class FileLifecycleHooks public virtual Stream OnFileOpened(Stream underlyingStream, Encoding encoding) => underlyingStream; /// - /// Method called on log files that were marked as obsolete (old) and would be deleted. - /// This can be used to copy old logs to archive location or send to backup server + /// Called before an obsolete (rolling) log file is deleted. + /// This can be used to copy old logs to an archive location or send to a backup server. /// - /// - /// Executing long synchronous operation may affect responsiveness of application - /// - /// Log file full path - public virtual void OnFileRemoving(string fullPath) {} + /// The full path to the file being deleted. + public virtual void OnFileDeleting(string path) {} } } diff --git a/src/Serilog.Sinks.File/Sinks/File/RollingFileSink.cs b/src/Serilog.Sinks.File/Sinks/File/RollingFileSink.cs index 3b0ac61..43e5fad 100644 --- a/src/Serilog.Sinks.File/Sinks/File/RollingFileSink.cs +++ b/src/Serilog.Sinks.File/Sinks/File/RollingFileSink.cs @@ -199,8 +199,7 @@ void ApplyRetentionPolicy(string currentFilePath) var fullPath = Path.Combine(_roller.LogFileDirectory, obsolete); try { - if (_hooks != null) _hooks.OnFileRemoving(fullPath); - + _hooks?.OnFileDeleting(fullPath); System.IO.File.Delete(fullPath); } catch (Exception ex) From f6d1cffa6bd69649d76d366a92e1647dbde8745f Mon Sep 17 00:00:00 2001 From: Marcin_Osada Date: Mon, 15 Jul 2019 17:05:31 +0200 Subject: [PATCH 69/69] Add test --- .../RollingFileSinkTests.cs | 22 ++++++++++++ .../Support/ArchiveOldLogsHook.cs | 35 +++++++++++++++++++ 2 files changed, 57 insertions(+) create mode 100644 test/Serilog.Sinks.File.Tests/Support/ArchiveOldLogsHook.cs diff --git a/test/Serilog.Sinks.File.Tests/RollingFileSinkTests.cs b/test/Serilog.Sinks.File.Tests/RollingFileSinkTests.cs index 2e9f613..70408b3 100644 --- a/test/Serilog.Sinks.File.Tests/RollingFileSinkTests.cs +++ b/test/Serilog.Sinks.File.Tests/RollingFileSinkTests.cs @@ -71,6 +71,28 @@ public void WhenRetentionCountIsSetOldFilesAreDeleted() }); } + [Fact] + public void WhenRetentionCountAndArchivingHookIsSetOldFilesAreCopiedAndOriginalDeleted() + { + const string archiveDirectory = "OldLogs"; + LogEvent e1 = Some.InformationEvent(), + e2 = Some.InformationEvent(e1.Timestamp.AddDays(1)), + e3 = Some.InformationEvent(e2.Timestamp.AddDays(5)); + + TestRollingEventSequence( + (pf, wt) => wt.File(pf, retainedFileCountLimit: 2, rollingInterval: RollingInterval.Day, hooks: new ArchiveOldLogsHook(archiveDirectory)), + new[] {e1, e2, e3}, + files => + { + Assert.Equal(3, files.Count); + Assert.True(!System.IO.File.Exists(files[0])); + Assert.True(System.IO.File.Exists(files[1])); + Assert.True(System.IO.File.Exists(files[2])); + + Assert.True(System.IO.File.Exists(ArchiveOldLogsHook.AddTopDirectory(files[0], archiveDirectory))); + }); + } + [Fact] public void WhenSizeLimitIsBreachedNewFilesCreated() { diff --git a/test/Serilog.Sinks.File.Tests/Support/ArchiveOldLogsHook.cs b/test/Serilog.Sinks.File.Tests/Support/ArchiveOldLogsHook.cs new file mode 100644 index 0000000..96dcb8c --- /dev/null +++ b/test/Serilog.Sinks.File.Tests/Support/ArchiveOldLogsHook.cs @@ -0,0 +1,35 @@ +using System; +using System.IO; +using System.Text; + +namespace Serilog.Sinks.File.Tests.Support +{ + internal class ArchiveOldLogsHook : FileLifecycleHooks + { + private readonly string _relativeArchiveDir; + + public ArchiveOldLogsHook(string relativeArchiveDir) + { + _relativeArchiveDir = relativeArchiveDir; + } + + public override void OnFileDeleting(string path) + { + base.OnFileDeleting(path); + var newFile = AddTopDirectory(path, _relativeArchiveDir, true); + System.IO.File.Copy(path, newFile, false); + } + + public static string AddTopDirectory(string path, string directoryToAdd, bool createOnNonExist = false) + { + string file = Path.GetFileName(path); + string directory = Path.Combine(Path.GetDirectoryName(path) ?? throw new InvalidOperationException(), directoryToAdd); + + if (createOnNonExist && !Directory.Exists(directory)) + { + Directory.CreateDirectory(directory); + } + return Path.Combine(directory, file); + } + } +}