From 1596f80d66c65ca0cb22358dbaeaa871321a7b99 Mon Sep 17 00:00:00 2001 From: Thiago Garcia Date: Tue, 30 Apr 2019 09:37:12 -0300 Subject: [PATCH 01/29] File retention policy by date - Add configuration to discard files older than X time Fixed constructors for compatibility; Unified where clause to retain files Adjust error messages New test cases --- .../FileLoggerConfigurationExtensions.cs | 36 ++++++++---- .../Sinks/File/RollingFileSink.cs | 25 +++++--- .../RollingFileSinkTests.cs | 57 +++++++++++++++++++ 3 files changed, 99 insertions(+), 19 deletions(-) diff --git a/src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs b/src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs index 490803d..c735437 100644 --- a/src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs +++ b/src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs @@ -138,6 +138,7 @@ public static LoggerConfiguration File( /// 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. [Obsolete("New code should not be compiled against this obsolete overload"), EditorBrowsable(EditorBrowsableState.Never)] public static LoggerConfiguration File( this LoggerSinkConfiguration sinkConfiguration, @@ -165,7 +166,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. @@ -181,7 +182,7 @@ 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. @@ -227,13 +228,18 @@ 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 hooking into log file lifecycle events. + /// The maximum time after the end of an interval that a rolling log file will be retained. + /// Must be greater than or equal to . + /// Ignored if is . + /// The default is to retain files indefinitely. /// 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, @@ -249,7 +255,8 @@ public static LoggerConfiguration File( bool rollOnFileSizeLimit = false, int? retainedFileCountLimit = DefaultRetainedFileCountLimit, Encoding encoding = null, - FileLifecycleHooks hooks = null) + FileLifecycleHooks hooks = null, + TimeSpan? retainedFileTimeLimit = null) { if (sinkConfiguration == null) throw new ArgumentNullException(nameof(sinkConfiguration)); if (path == null) throw new ArgumentNullException(nameof(path)); @@ -258,7 +265,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, hooks); + rollingInterval, rollOnFileSizeLimit, retainedFileCountLimit, encoding, hooks, retainedFileTimeLimit); } /// @@ -267,7 +274,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. @@ -289,6 +296,10 @@ public static LoggerConfiguration File( /// 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 hooking into log file lifecycle events. + /// The maximum time after the end of an interval that a rolling log file will be retained. + /// Must be greater than or equal to . + /// Ignored if is . + /// The default is to retain files indefinitely. /// Configuration object allowing method chaining. public static LoggerConfiguration File( this LoggerSinkConfiguration sinkConfiguration, @@ -304,7 +315,8 @@ public static LoggerConfiguration File( bool rollOnFileSizeLimit = false, int? retainedFileCountLimit = DefaultRetainedFileCountLimit, Encoding encoding = null, - FileLifecycleHooks hooks = null) + FileLifecycleHooks hooks = null, + TimeSpan? retainedFileTimeLimit = null) { if (sinkConfiguration == null) throw new ArgumentNullException(nameof(sinkConfiguration)); if (formatter == null) throw new ArgumentNullException(nameof(formatter)); @@ -312,7 +324,7 @@ public static LoggerConfiguration File( return ConfigureFile(sinkConfiguration.Sink, formatter, path, restrictedToMinimumLevel, fileSizeLimitBytes, levelSwitch, buffered, false, shared, flushToDiskInterval, encoding, rollingInterval, rollOnFileSizeLimit, - retainedFileCountLimit, hooks); + retainedFileCountLimit, hooks, retainedFileTimeLimit); } /// @@ -432,7 +444,7 @@ public static LoggerConfiguration File( if (path == null) throw new ArgumentNullException(nameof(path)); return ConfigureFile(sinkConfiguration.Sink, formatter, path, restrictedToMinimumLevel, null, levelSwitch, false, true, - false, null, encoding, RollingInterval.Infinite, false, null, hooks); + false, null, encoding, RollingInterval.Infinite, false, null, hooks, null); } static LoggerConfiguration ConfigureFile( @@ -450,13 +462,15 @@ static LoggerConfiguration ConfigureFile( RollingInterval rollingInterval, bool rollOnFileSizeLimit, int? retainedFileCountLimit, - FileLifecycleHooks hooks) + FileLifecycleHooks hooks, + TimeSpan? retainedFileTimeLimit) { 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.", nameof(fileSizeLimitBytes)); if (retainedFileCountLimit.HasValue && retainedFileCountLimit < 1) throw new ArgumentException("At least one file must be retained.", nameof(retainedFileCountLimit)); + if (retainedFileTimeLimit.HasValue && retainedFileTimeLimit < TimeSpan.Zero) throw new ArgumentException("Negative value provided; retained file time limit must be non-negative.", nameof(retainedFileTimeLimit)); 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("File lifecycle hooks are not currently supported for shared log files.", nameof(hooks)); @@ -464,7 +478,7 @@ static LoggerConfiguration ConfigureFile( if (rollOnFileSizeLimit || rollingInterval != RollingInterval.Infinite) { - sink = new RollingFileSink(path, formatter, fileSizeLimitBytes, retainedFileCountLimit, encoding, buffered, shared, rollingInterval, rollOnFileSizeLimit, hooks); + sink = new RollingFileSink(path, formatter, fileSizeLimitBytes, retainedFileCountLimit, encoding, buffered, shared, rollingInterval, rollOnFileSizeLimit, hooks, retainedFileTimeLimit); } else { diff --git a/src/Serilog.Sinks.File/Sinks/File/RollingFileSink.cs b/src/Serilog.Sinks.File/Sinks/File/RollingFileSink.cs index 2db6f24..7d34453 100644 --- a/src/Serilog.Sinks.File/Sinks/File/RollingFileSink.cs +++ b/src/Serilog.Sinks.File/Sinks/File/RollingFileSink.cs @@ -20,6 +20,7 @@ using Serilog.Debugging; using Serilog.Events; using Serilog.Formatting; +using System.Collections.Generic; namespace Serilog.Sinks.File { @@ -29,6 +30,7 @@ sealed class RollingFileSink : ILogEventSink, IFlushableFileSink, IDisposable readonly ITextFormatter _textFormatter; readonly long? _fileSizeLimitBytes; readonly int? _retainedFileCountLimit; + readonly TimeSpan? _retainedFileTimeLimit; readonly Encoding _encoding; readonly bool _buffered; readonly bool _shared; @@ -50,16 +52,19 @@ public RollingFileSink(string path, bool shared, RollingInterval rollingInterval, bool rollOnFileSizeLimit, - FileLifecycleHooks hooks) + FileLifecycleHooks hooks, + TimeSpan? retainedFileTimeLimit) { 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"); + if (retainedFileTimeLimit.HasValue && retainedFileTimeLimit < TimeSpan.Zero) throw new ArgumentException("Negative value provided; retained file time limit must be non-negative.", nameof(retainedFileTimeLimit)); _roller = new PathRoller(path, rollingInterval); _textFormatter = textFormatter; _fileSizeLimitBytes = fileSizeLimitBytes; _retainedFileCountLimit = retainedFileCountLimit; + _retainedFileTimeLimit = retainedFileTimeLimit; _encoding = encoding; _buffered = buffered; _shared = shared; @@ -173,7 +178,7 @@ void OpenFile(DateTime now, int? minSequence = null) void ApplyRetentionPolicy(string currentFilePath) { - if (_retainedFileCountLimit == null) return; + if (_retainedFileCountLimit == null && _retainedFileTimeLimit == null) return; var currentFileName = Path.GetFileName(currentFilePath); @@ -181,17 +186,21 @@ void ApplyRetentionPolicy(string currentFilePath) // 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 }); + .Union(new[] { currentFileName }); var newestFirst = _roller .SelectMatches(potentialMatches) .OrderByDescending(m => m.DateTime) .ThenByDescending(m => m.SequenceNumber) - .Select(m => m.Filename); + .Select(m => new { m.Filename, m.DateTime }); var toRemove = newestFirst - .Where(n => StringComparer.OrdinalIgnoreCase.Compare(currentFileName, n) != 0) - .Skip(_retainedFileCountLimit.Value - 1) + .Where(n => StringComparer.OrdinalIgnoreCase.Compare(currentFileName, n.Filename) != 0) + .SkipWhile((x, i) => (i < (_retainedFileCountLimit - 1 ?? 0)) && + (!_retainedFileTimeLimit.HasValue || + x.DateTime.HasValue && + DateTime.Now.Subtract(_retainedFileTimeLimit.Value).CompareTo(x.DateTime.Value) <= 0)) + .Select(x => x.Filename) .ToList(); foreach (var obsolete in toRemove) diff --git a/test/Serilog.Sinks.File.Tests/RollingFileSinkTests.cs b/test/Serilog.Sinks.File.Tests/RollingFileSinkTests.cs index 2e9f613..746d587 100644 --- a/test/Serilog.Sinks.File.Tests/RollingFileSinkTests.cs +++ b/test/Serilog.Sinks.File.Tests/RollingFileSinkTests.cs @@ -71,6 +71,63 @@ public void WhenRetentionCountIsSetOldFilesAreDeleted() }); } + [Fact] + public void WhenRetentionTimeIsSetOldFilesAreDeleted() + { + LogEvent e1 = Some.InformationEvent(DateTime.Today.AddDays(-5)), + e2 = Some.InformationEvent(e1.Timestamp.AddDays(2)), + e3 = Some.InformationEvent(e2.Timestamp.AddDays(5)); + + TestRollingEventSequence( + (pf, wt) => wt.File(pf, retainedFileTimeLimit: TimeSpan.FromDays(1), 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 WhenRetentionCountAndTimeIsSetOldFilesAreDeletedByTime() + { + LogEvent e1 = Some.InformationEvent(DateTime.Today.AddDays(-5)), + e2 = Some.InformationEvent(e1.Timestamp.AddDays(2)), + e3 = Some.InformationEvent(e2.Timestamp.AddDays(5)); + + TestRollingEventSequence( + (pf, wt) => wt.File(pf, retainedFileCountLimit: 2, retainedFileTimeLimit: TimeSpan.FromDays(1), 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 WhenRetentionCountAndTimeIsSetOldFilesAreDeletedByCount() + { + LogEvent e1 = Some.InformationEvent(DateTime.Today.AddDays(-5)), + e2 = Some.InformationEvent(e1.Timestamp.AddDays(2)), + e3 = Some.InformationEvent(e2.Timestamp.AddDays(5)); + + TestRollingEventSequence( + (pf, wt) => wt.File(pf, retainedFileCountLimit: 2, retainedFileTimeLimit: TimeSpan.FromDays(10), 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 WhenSizeLimitIsBreachedNewFilesCreated() { From b213a7a8481c0f9033d0c129078c829c9cbb4187 Mon Sep 17 00:00:00 2001 From: Thiago Garcia Date: Sat, 1 Jun 2019 18:07:59 -0300 Subject: [PATCH 02/29] Refactoring file filter; fixing docs --- .../FileLoggerConfigurationExtensions.cs | 2 -- .../Sinks/File/RollingFileSink.cs | 19 +++++++++++++------ 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs b/src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs index c735437..ad6b80b 100644 --- a/src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs +++ b/src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs @@ -138,7 +138,6 @@ public static LoggerConfiguration File( /// 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. [Obsolete("New code should not be compiled against this obsolete overload"), EditorBrowsable(EditorBrowsableState.Never)] public static LoggerConfiguration File( this LoggerSinkConfiguration sinkConfiguration, @@ -239,7 +238,6 @@ public static LoggerConfiguration File( /// Ignored if is . /// The default is to retain files indefinitely. /// 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, diff --git a/src/Serilog.Sinks.File/Sinks/File/RollingFileSink.cs b/src/Serilog.Sinks.File/Sinks/File/RollingFileSink.cs index 7d34453..0e61954 100644 --- a/src/Serilog.Sinks.File/Sinks/File/RollingFileSink.cs +++ b/src/Serilog.Sinks.File/Sinks/File/RollingFileSink.cs @@ -191,15 +191,11 @@ void ApplyRetentionPolicy(string currentFilePath) var newestFirst = _roller .SelectMatches(potentialMatches) .OrderByDescending(m => m.DateTime) - .ThenByDescending(m => m.SequenceNumber) - .Select(m => new { m.Filename, m.DateTime }); + .ThenByDescending(m => m.SequenceNumber); var toRemove = newestFirst .Where(n => StringComparer.OrdinalIgnoreCase.Compare(currentFileName, n.Filename) != 0) - .SkipWhile((x, i) => (i < (_retainedFileCountLimit - 1 ?? 0)) && - (!_retainedFileTimeLimit.HasValue || - x.DateTime.HasValue && - DateTime.Now.Subtract(_retainedFileTimeLimit.Value).CompareTo(x.DateTime.Value) <= 0)) + .SkipWhile(FilterFiles) .Select(x => x.Filename) .ToList(); @@ -217,6 +213,17 @@ void ApplyRetentionPolicy(string currentFilePath) } } + private bool FilterFiles(RollingLogFile file, int index) + { + var isInCountLimit = index < (_retainedFileCountLimit - 1 ?? 0); + + var isInTimeLimit = !_retainedFileTimeLimit.HasValue; + if (_retainedFileTimeLimit.HasValue && file.DateTime.HasValue) + isInTimeLimit = DateTime.Now.Subtract(_retainedFileTimeLimit.Value).CompareTo(file.DateTime.Value) <= 0; + + return isInCountLimit && isInTimeLimit; + } + public void Dispose() { lock (_syncRoot) From 2d3cee74e8f45ddcf4fccedd1876b3c57b70298f Mon Sep 17 00:00:00 2001 From: Maciej Warszawski Date: Fri, 6 Dec 2019 17:30:10 +0100 Subject: [PATCH 03/29] update github sourcelink support package to latest version --- 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 f8640f6..d30fc06 100644 --- a/src/Serilog.Sinks.File/Serilog.Sinks.File.csproj +++ b/src/Serilog.Sinks.File/Serilog.Sinks.File.csproj @@ -30,7 +30,7 @@ - + From 1093c3baa0b60ee94b1c3caf371e47e70c44b04e Mon Sep 17 00:00:00 2001 From: Nicholas Blumhardt Date: Tue, 4 Feb 2020 15:57:17 +1000 Subject: [PATCH 04/29] Major version bump; accepting breaking changes into dev --- 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 d30fc06..41afc3f 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.1.0 + 5.0.0 Serilog Contributors net45;netstandard1.3;netstandard2.0 true From 1b863ff185b8760df6334f7138003d885fbdb21d Mon Sep 17 00:00:00 2001 From: Nicholas Blumhardt Date: Tue, 4 Feb 2020 16:10:19 +1000 Subject: [PATCH 05/29] Remove explicit private modifier; name tweak; reorganize some conditionals --- .../Sinks/File/RollingFileSink.cs | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/src/Serilog.Sinks.File/Sinks/File/RollingFileSink.cs b/src/Serilog.Sinks.File/Sinks/File/RollingFileSink.cs index 0e61954..330dfbc 100644 --- a/src/Serilog.Sinks.File/Sinks/File/RollingFileSink.cs +++ b/src/Serilog.Sinks.File/Sinks/File/RollingFileSink.cs @@ -171,12 +171,12 @@ void OpenFile(DateTime now, int? minSequence = null) throw; } - ApplyRetentionPolicy(path); + ApplyRetentionPolicy(path, now); return; } } - void ApplyRetentionPolicy(string currentFilePath) + void ApplyRetentionPolicy(string currentFilePath, DateTime now) { if (_retainedFileCountLimit == null && _retainedFileTimeLimit == null) return; @@ -195,7 +195,7 @@ void ApplyRetentionPolicy(string currentFilePath) var toRemove = newestFirst .Where(n => StringComparer.OrdinalIgnoreCase.Compare(currentFileName, n.Filename) != 0) - .SkipWhile(FilterFiles) + .SkipWhile((f, i) => ShouldRetainFile(f, i, now)) .Select(x => x.Filename) .ToList(); @@ -213,15 +213,18 @@ void ApplyRetentionPolicy(string currentFilePath) } } - private bool FilterFiles(RollingLogFile file, int index) + bool ShouldRetainFile(RollingLogFile file, int index, DateTime now) { - var isInCountLimit = index < (_retainedFileCountLimit - 1 ?? 0); + if (_retainedFileCountLimit.HasValue && index >= _retainedFileCountLimit.Value) + return false; - var isInTimeLimit = !_retainedFileTimeLimit.HasValue; - if (_retainedFileTimeLimit.HasValue && file.DateTime.HasValue) - isInTimeLimit = DateTime.Now.Subtract(_retainedFileTimeLimit.Value).CompareTo(file.DateTime.Value) <= 0; - - return isInCountLimit && isInTimeLimit; + if (_retainedFileTimeLimit.HasValue && file.DateTime.HasValue && + file.DateTime.Value < now.Subtract(_retainedFileTimeLimit.Value)) + { + return false; + } + + return true; } public void Dispose() From c2375ebd46f872042b5b68fe763b391f314369e2 Mon Sep 17 00:00:00 2001 From: Nicholas Blumhardt Date: Tue, 4 Feb 2020 16:33:21 +1000 Subject: [PATCH 06/29] Fix test; update to use the 2019 build image --- appveyor.yml | 7 +++---- serilog-sinks-file.sln.DotSettings | 2 ++ src/Serilog.Sinks.File/Serilog.Sinks.File.csproj | 5 ----- src/Serilog.Sinks.File/Sinks/File/RollingFileSink.cs | 3 +-- test/Serilog.Sinks.File.Tests/RollingFileSinkTests.cs | 8 ++++---- 5 files changed, 10 insertions(+), 15 deletions(-) create mode 100644 serilog-sinks-file.sln.DotSettings diff --git a/appveyor.yml b/appveyor.yml index 79ee987..ef5f126 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,16 +1,15 @@ version: '{build}' skip_tags: true image: - - Visual Studio 2017 - - Ubuntu -configuration: Release + - Visual Studio 2019 + - Ubuntu1804 build_script: - ps: ./Build.ps1 for: - matrix: only: - - image: Ubuntu + - image: Ubuntu1804 build_script: - sh build.sh test: off diff --git a/serilog-sinks-file.sln.DotSettings b/serilog-sinks-file.sln.DotSettings new file mode 100644 index 0000000..bfd92fd --- /dev/null +++ b/serilog-sinks-file.sln.DotSettings @@ -0,0 +1,2 @@ + + True \ No newline at end of file diff --git a/src/Serilog.Sinks.File/Serilog.Sinks.File.csproj b/src/Serilog.Sinks.File/Serilog.Sinks.File.csproj index 41afc3f..14bdff7 100644 --- a/src/Serilog.Sinks.File/Serilog.Sinks.File.csproj +++ b/src/Serilog.Sinks.File/Serilog.Sinks.File.csproj @@ -6,11 +6,9 @@ Serilog Contributors net45;netstandard1.3;netstandard2.0 true - Serilog.Sinks.File ../../assets/Serilog.snk true true - Serilog.Sinks.File serilog;file http://serilog.net/images/serilog-sink-nuget.png http://serilog.net @@ -20,10 +18,7 @@ false Serilog true - Serilog.Sinks.File - true - false true $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb diff --git a/src/Serilog.Sinks.File/Sinks/File/RollingFileSink.cs b/src/Serilog.Sinks.File/Sinks/File/RollingFileSink.cs index e6d29d5..d007cd5 100644 --- a/src/Serilog.Sinks.File/Sinks/File/RollingFileSink.cs +++ b/src/Serilog.Sinks.File/Sinks/File/RollingFileSink.cs @@ -20,7 +20,6 @@ using Serilog.Debugging; using Serilog.Events; using Serilog.Formatting; -using System.Collections.Generic; namespace Serilog.Sinks.File { @@ -216,7 +215,7 @@ void ApplyRetentionPolicy(string currentFilePath, DateTime now) bool ShouldRetainFile(RollingLogFile file, int index, DateTime now) { - if (_retainedFileCountLimit.HasValue && index >= _retainedFileCountLimit.Value) + if (_retainedFileCountLimit.HasValue && index >= _retainedFileCountLimit.Value - 1) return false; if (_retainedFileTimeLimit.HasValue && file.DateTime.HasValue && diff --git a/test/Serilog.Sinks.File.Tests/RollingFileSinkTests.cs b/test/Serilog.Sinks.File.Tests/RollingFileSinkTests.cs index 5d0879f..568f7a7 100644 --- a/test/Serilog.Sinks.File.Tests/RollingFileSinkTests.cs +++ b/test/Serilog.Sinks.File.Tests/RollingFileSinkTests.cs @@ -8,6 +8,7 @@ using Serilog.Events; using Serilog.Sinks.File.Tests.Support; using Serilog.Configuration; +using Serilog.Core; namespace Serilog.Sinks.File.Tests { @@ -142,7 +143,7 @@ public void WhenRetentionCountAndArchivingHookIsSetOldFilesAreCopiedAndOriginalD files => { Assert.Equal(3, files.Count); - Assert.True(!System.IO.File.Exists(files[0])); + Assert.False(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))); @@ -241,7 +242,7 @@ public void IfTheLogFolderDoesNotExistItWillBeCreated() var folder = Path.Combine(temp, Guid.NewGuid().ToString()); var pathFormat = Path.Combine(folder, fileName); - ILogger log = null; + Logger log = null; try { @@ -255,8 +256,7 @@ public void IfTheLogFolderDoesNotExistItWillBeCreated() } finally { - var disposable = (IDisposable)log; - if (disposable != null) disposable.Dispose(); + log?.Dispose(); Directory.Delete(temp, true); } } From 73ac5ff60cc5cf24442fee04e0cb1fd3d46a71e0 Mon Sep 17 00:00:00 2001 From: Nicholas Blumhardt Date: Wed, 5 Feb 2020 09:03:32 +1000 Subject: [PATCH 07/29] .NET Core App 3.0 target; misc clean-up --- example/Sample/Sample.csproj | 3 -- serilog-sinks-file.sln.DotSettings | 1 + .../Serilog.Sinks.File.csproj | 34 ++++++++----------- .../Serilog.Sinks.File.Tests/FileSinkTests.cs | 4 +-- .../RollingFileSinkTests.cs | 4 +-- .../Serilog.Sinks.File.Tests.csproj | 9 +++-- test/Serilog.Sinks.File.Tests/Support/Some.cs | 9 +++-- .../TemplatedPathRollerTests.cs | 20 +++++------ 8 files changed, 35 insertions(+), 49 deletions(-) diff --git a/example/Sample/Sample.csproj b/example/Sample/Sample.csproj index ec04f95..e307db1 100644 --- a/example/Sample/Sample.csproj +++ b/example/Sample/Sample.csproj @@ -17,8 +17,5 @@ - - - diff --git a/serilog-sinks-file.sln.DotSettings b/serilog-sinks-file.sln.DotSettings index bfd92fd..95887cc 100644 --- a/serilog-sinks-file.sln.DotSettings +++ b/serilog-sinks-file.sln.DotSettings @@ -1,2 +1,3 @@  + True True \ No newline at end of file diff --git a/src/Serilog.Sinks.File/Serilog.Sinks.File.csproj b/src/Serilog.Sinks.File/Serilog.Sinks.File.csproj index 14bdff7..f498bf6 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. 5.0.0 Serilog Contributors - net45;netstandard1.3;netstandard2.0 + net45;netstandard1.3;netstandard2.0;netcoreapp3.0 true ../../assets/Serilog.snk true @@ -18,7 +18,7 @@ false Serilog true - true + true false true $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb @@ -26,13 +26,7 @@ - - - - - - - + @@ -43,23 +37,23 @@ $(DefineConstants);OS_MUTEX - + $(DefineConstants);OS_MUTEX - - - - - - - + + + + + + + - - - + + + diff --git a/test/Serilog.Sinks.File.Tests/FileSinkTests.cs b/test/Serilog.Sinks.File.Tests/FileSinkTests.cs index 2d0f210..10fe926 100644 --- a/test/Serilog.Sinks.File.Tests/FileSinkTests.cs +++ b/test/Serilog.Sinks.File.Tests/FileSinkTests.cs @@ -130,7 +130,7 @@ public void WhenLimitIsSpecifiedAndEncodingHasNoPreambleDataIsCorrectlyAppendedT long? maxBytes = 5000; var encoding = new UTF8Encoding(false); - Assert.Equal(0, encoding.GetPreamble().Length); + Assert.Empty(encoding.GetPreamble()); WriteTwoEventsAndCheckOutputFileLength(maxBytes, encoding); } @@ -139,7 +139,7 @@ public void WhenLimitIsNotSpecifiedAndEncodingHasNoPreambleDataIsCorrectlyAppend { var encoding = new UTF8Encoding(false); - Assert.Equal(0, encoding.GetPreamble().Length); + Assert.Empty(encoding.GetPreamble()); WriteTwoEventsAndCheckOutputFileLength(null, encoding); } diff --git a/test/Serilog.Sinks.File.Tests/RollingFileSinkTests.cs b/test/Serilog.Sinks.File.Tests/RollingFileSinkTests.cs index 568f7a7..d295dfc 100644 --- a/test/Serilog.Sinks.File.Tests/RollingFileSinkTests.cs +++ b/test/Serilog.Sinks.File.Tests/RollingFileSinkTests.cs @@ -227,8 +227,8 @@ public void WhenStreamWrapperSpecifiedIsUsedForRolledFiles() textStream.Position = 0; var lines = textStream.ReadAllLines(); - Assert.Equal(1, lines.Count); - Assert.True(lines[0].EndsWith(logEvents[i].MessageTemplate.Text)); + Assert.Single(lines); + Assert.EndsWith(logEvents[i].MessageTemplate.Text, lines[0]); } } } 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 3491e32..7703dc7 100644 --- a/test/Serilog.Sinks.File.Tests/Serilog.Sinks.File.Tests.csproj +++ b/test/Serilog.Sinks.File.Tests/Serilog.Sinks.File.Tests.csproj @@ -1,13 +1,12 @@ - net452;netcoreapp1.0;netcoreapp2.0 + net452;netcoreapp1.0;netcoreapp2.0;netcoreapp3.1 true Serilog.Sinks.File.Tests ../../assets/Serilog.snk true true - Serilog.Sinks.RollingFile.Tests true $(PackageTargetFallback);dnxcore50;portable-net45+win8 1.0.4 @@ -18,9 +17,9 @@ - - - + + + diff --git a/test/Serilog.Sinks.File.Tests/Support/Some.cs b/test/Serilog.Sinks.File.Tests/Support/Some.cs index 2d29d4d..f0c7fd9 100644 --- a/test/Serilog.Sinks.File.Tests/Support/Some.cs +++ b/test/Serilog.Sinks.File.Tests/Support/Some.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading; @@ -7,6 +6,8 @@ using Serilog.Parsing; using Xunit.Sdk; +// ReSharper disable UnusedMember.Global + namespace Serilog.Sinks.File.Tests.Support { static class Some @@ -46,10 +47,8 @@ public static DateTimeOffset OffsetInstant() 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)) + if (!log.BindMessageTemplate(messageTemplate, propertyValues, out var template, out var properties)) #pragma warning restore Serilog004 // Constant MessageTemplate verifier { throw new XunitException("Template could not be bound."); @@ -65,7 +64,7 @@ public static LogEvent LogEvent(DateTimeOffset? timestamp = null, LogEventLevel public static LogEvent InformationEvent(DateTimeOffset? timestamp = null) { - return LogEvent(timestamp, LogEventLevel.Information); + return LogEvent(timestamp); } public static LogEvent DebugEvent(DateTimeOffset? timestamp = null) diff --git a/test/Serilog.Sinks.File.Tests/TemplatedPathRollerTests.cs b/test/Serilog.Sinks.File.Tests/TemplatedPathRollerTests.cs index 5e1b015..65a974c 100644 --- a/test/Serilog.Sinks.File.Tests/TemplatedPathRollerTests.cs +++ b/test/Serilog.Sinks.File.Tests/TemplatedPathRollerTests.cs @@ -12,8 +12,7 @@ public void TheLogFileIncludesDateToken() { 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); + roller.GetLogFilePath(now, null, out var path); AssertEqualAbsolute(Path.Combine("Logs", "log-20130714.txt"), path); } @@ -22,8 +21,7 @@ public void ANonZeroIncrementIsIncludedAndPadded() { 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); + roller.GetLogFilePath(now, 12, out var path); AssertEqualAbsolute(Path.Combine("Logs", "log-20130714_012.txt"), path); } @@ -46,8 +44,7 @@ public void TheLogFileIsNotRequiredToIncludeAnExtension() { 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); + roller.GetLogFilePath(now, null, out var path); AssertEqualAbsolute(Path.Combine("Logs", "log-20130714"), path); } @@ -56,19 +53,18 @@ public void TheLogFileIsNotRequiredToIncludeADirectory() { var roller = new PathRoller("log-", RollingInterval.Day); var now = new DateTime(2013, 7, 14, 3, 24, 9, 980); - string path; - roller.GetLogFilePath(now, null, out path); + roller.GetLogFilePath(now, null, out var path); AssertEqualAbsolute("log-20130714", path); } [Fact] - public void MatchingExcludesSimilarButNonmatchingFiles() + public void MatchingExcludesSimilarButNonMatchingFiles() { var roller = new PathRoller("log-.txt", RollingInterval.Day); const string similar1 = "log-0.txt"; - const string similar2 = "log-helloyou.txt"; + const string similar2 = "log-hello.txt"; var matched = roller.SelectMatches(new[] { similar1, similar2 }); - Assert.Equal(0, matched.Count()); + Assert.Empty(matched); } [Fact] @@ -86,7 +82,7 @@ public void MatchingSelectsFiles(string template, string zeroth, string thirtyFi var roller = new PathRoller(template, interval); var matched = roller.SelectMatches(new[] { zeroth, thirtyFirst }).ToArray(); Assert.Equal(2, matched.Length); - Assert.Equal(null, matched[0].SequenceNumber); + Assert.Null(matched[0].SequenceNumber); Assert.Equal(31, matched[1].SequenceNumber); } From 849ad79f453e036937e1d5cacd10901afe9a77c8 Mon Sep 17 00:00:00 2001 From: Nicholas Blumhardt Date: Tue, 14 Apr 2020 08:36:19 +1000 Subject: [PATCH 08/29] Cut back frameworks targeted by tests to keep build manageable --- .../Serilog.Sinks.File.Tests/Serilog.Sinks.File.Tests.csproj | 5 ++--- 1 file changed, 2 insertions(+), 3 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 7703dc7..fcf2880 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;netcoreapp2.0;netcoreapp3.1 + net47;netcoreapp3.1 true Serilog.Sinks.File.Tests ../../assets/Serilog.snk @@ -17,7 +17,7 @@ - + @@ -31,5 +31,4 @@ - From 6df7cae82db3a10ba52441196417a8ff28dccbb3 Mon Sep 17 00:00:00 2001 From: Eamon Hetherton Date: Fri, 17 Apr 2020 13:19:09 +1000 Subject: [PATCH 09/29] Added way to chain FileLifeCycleHooks together --- .../File/FileLifecycleHooksExtensions.cs | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 src/Serilog.Sinks.File/Sinks/File/FileLifecycleHooksExtensions.cs diff --git a/src/Serilog.Sinks.File/Sinks/File/FileLifecycleHooksExtensions.cs b/src/Serilog.Sinks.File/Sinks/File/FileLifecycleHooksExtensions.cs new file mode 100644 index 0000000..acc5513 --- /dev/null +++ b/src/Serilog.Sinks.File/Sinks/File/FileLifecycleHooksExtensions.cs @@ -0,0 +1,71 @@ +// 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; +using System.IO; +using System.Text; + +namespace Serilog.Sinks.File +{ + /// + /// FileLifecycleHooks extension methods + /// + public static class FileLifecycleHooksExtensions + { + /// + /// Creates a chain of that have their methods called sequentially + /// Can be used to compose together; e.g. add header information to each log file and + /// compress it. + /// + /// + /// + /// var hooks = new GZipHooks().ChainTo(new HeaderWriter("File Header")); + /// + /// + /// The first to have its methods called in the chain + /// The second to have its methods called in the chain + /// + public static FileLifecycleHooks ChainTo(this FileLifecycleHooks first, FileLifecycleHooks second) + { + return new FileLifeCycleHookChain(first, second); + } + + class FileLifeCycleHookChain : FileLifecycleHooks + { + private readonly FileLifecycleHooks[] hooks; + + public FileLifeCycleHookChain(params FileLifecycleHooks[] hooks) + { + this.hooks = hooks ?? throw new ArgumentNullException(nameof(hooks)); + } + + public override Stream OnFileOpened(Stream underlyingStream, Encoding encoding) + { + for (int i = 0; i < hooks.Length; i++) + { + underlyingStream = hooks[i].OnFileOpened(underlyingStream, encoding); + } + return underlyingStream; + } + + public override void OnFileDeleting(string path) + { + for (int i = 0; i < hooks.Length; i++) + { + hooks[i].OnFileDeleting(path); + } + } + } + } +} From 1af50969e0dafe596b41d4f8c514ca32acbbce02 Mon Sep 17 00:00:00 2001 From: Eamon Hetherton Date: Fri, 17 Apr 2020 13:21:09 +1000 Subject: [PATCH 10/29] stop using params when current implementation only ever has two values --- .../File/FileLifecycleHooksExtensions.cs | 23 +++++++++---------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/src/Serilog.Sinks.File/Sinks/File/FileLifecycleHooksExtensions.cs b/src/Serilog.Sinks.File/Sinks/File/FileLifecycleHooksExtensions.cs index acc5513..f79c768 100644 --- a/src/Serilog.Sinks.File/Sinks/File/FileLifecycleHooksExtensions.cs +++ b/src/Serilog.Sinks.File/Sinks/File/FileLifecycleHooksExtensions.cs @@ -43,28 +43,27 @@ public static FileLifecycleHooks ChainTo(this FileLifecycleHooks first, FileLife class FileLifeCycleHookChain : FileLifecycleHooks { - private readonly FileLifecycleHooks[] hooks; + private readonly FileLifecycleHooks _first; + private readonly FileLifecycleHooks _second; - public FileLifeCycleHookChain(params FileLifecycleHooks[] hooks) + public FileLifeCycleHookChain(FileLifecycleHooks first, FileLifecycleHooks second) { - this.hooks = hooks ?? throw new ArgumentNullException(nameof(hooks)); + _first = first ?? throw new ArgumentNullException(nameof(first)); + _second = second ?? throw new ArgumentNullException(nameof(second)); } public override Stream OnFileOpened(Stream underlyingStream, Encoding encoding) { - for (int i = 0; i < hooks.Length; i++) - { - underlyingStream = hooks[i].OnFileOpened(underlyingStream, encoding); - } - return underlyingStream; + var firstStreamResult = _first.OnFileOpened(underlyingStream, encoding); + var secondStreamResult = _second.OnFileOpened(firstStreamResult, encoding); + + return secondStreamResult; } public override void OnFileDeleting(string path) { - for (int i = 0; i < hooks.Length; i++) - { - hooks[i].OnFileDeleting(path); - } + _first.OnFileDeleting(path); + _second.OnFileDeleting(path); } } } From cad86bd4ae68ecdb6a476f6a53e8ca1d3bd1cc95 Mon Sep 17 00:00:00 2001 From: Eamon Hetherton Date: Fri, 17 Apr 2020 16:21:49 +1000 Subject: [PATCH 11/29] refactor "ChainTo" to instance method "Then" --- .../Sinks/File/FileLifeCycleHookChain.cs | 46 ++++++++++++ .../Sinks/File/FileLifecycleHooks.cs | 17 +++++ .../File/FileLifecycleHooksExtensions.cs | 70 ------------------- 3 files changed, 63 insertions(+), 70 deletions(-) create mode 100644 src/Serilog.Sinks.File/Sinks/File/FileLifeCycleHookChain.cs delete mode 100644 src/Serilog.Sinks.File/Sinks/File/FileLifecycleHooksExtensions.cs diff --git a/src/Serilog.Sinks.File/Sinks/File/FileLifeCycleHookChain.cs b/src/Serilog.Sinks.File/Sinks/File/FileLifeCycleHookChain.cs new file mode 100644 index 0000000..cea5095 --- /dev/null +++ b/src/Serilog.Sinks.File/Sinks/File/FileLifeCycleHookChain.cs @@ -0,0 +1,46 @@ +// 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; +using System.IO; +using System.Text; + +namespace Serilog.Sinks.File +{ + class FileLifeCycleHookChain : FileLifecycleHooks + { + private readonly FileLifecycleHooks _first; + private readonly FileLifecycleHooks _second; + + public FileLifeCycleHookChain(FileLifecycleHooks first, FileLifecycleHooks second) + { + _first = first ?? throw new ArgumentNullException(nameof(first)); + _second = second ?? throw new ArgumentNullException(nameof(second)); + } + + public override Stream OnFileOpened(Stream underlyingStream, Encoding encoding) + { + var firstStreamResult = _first.OnFileOpened(underlyingStream, encoding); + var secondStreamResult = _second.OnFileOpened(firstStreamResult, encoding); + + return secondStreamResult; + } + + public override void OnFileDeleting(string path) + { + _first.OnFileDeleting(path); + _second.OnFileDeleting(path); + } + } +} diff --git a/src/Serilog.Sinks.File/Sinks/File/FileLifecycleHooks.cs b/src/Serilog.Sinks.File/Sinks/File/FileLifecycleHooks.cs index fbaf133..e804cad 100644 --- a/src/Serilog.Sinks.File/Sinks/File/FileLifecycleHooks.cs +++ b/src/Serilog.Sinks.File/Sinks/File/FileLifecycleHooks.cs @@ -43,5 +43,22 @@ public abstract class FileLifecycleHooks /// /// The full path to the file being deleted. public virtual void OnFileDeleting(string path) {} + + /// + /// Creates a chain of that have their methods called sequentially + /// Can be used to compose together; e.g. add header information to each log file and + /// compress it. + /// + /// + /// + /// var hooks = new GZipHooks().Then(new HeaderWriter("File Header")); + /// + /// + /// The next to have its methods called in the chain + /// + public FileLifecycleHooks Then(FileLifecycleHooks next) + { + return new FileLifeCycleHookChain(this, next); + } } } diff --git a/src/Serilog.Sinks.File/Sinks/File/FileLifecycleHooksExtensions.cs b/src/Serilog.Sinks.File/Sinks/File/FileLifecycleHooksExtensions.cs deleted file mode 100644 index f79c768..0000000 --- a/src/Serilog.Sinks.File/Sinks/File/FileLifecycleHooksExtensions.cs +++ /dev/null @@ -1,70 +0,0 @@ -// 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; -using System.IO; -using System.Text; - -namespace Serilog.Sinks.File -{ - /// - /// FileLifecycleHooks extension methods - /// - public static class FileLifecycleHooksExtensions - { - /// - /// Creates a chain of that have their methods called sequentially - /// Can be used to compose together; e.g. add header information to each log file and - /// compress it. - /// - /// - /// - /// var hooks = new GZipHooks().ChainTo(new HeaderWriter("File Header")); - /// - /// - /// The first to have its methods called in the chain - /// The second to have its methods called in the chain - /// - public static FileLifecycleHooks ChainTo(this FileLifecycleHooks first, FileLifecycleHooks second) - { - return new FileLifeCycleHookChain(first, second); - } - - class FileLifeCycleHookChain : FileLifecycleHooks - { - private readonly FileLifecycleHooks _first; - private readonly FileLifecycleHooks _second; - - public FileLifeCycleHookChain(FileLifecycleHooks first, FileLifecycleHooks second) - { - _first = first ?? throw new ArgumentNullException(nameof(first)); - _second = second ?? throw new ArgumentNullException(nameof(second)); - } - - public override Stream OnFileOpened(Stream underlyingStream, Encoding encoding) - { - var firstStreamResult = _first.OnFileOpened(underlyingStream, encoding); - var secondStreamResult = _second.OnFileOpened(firstStreamResult, encoding); - - return secondStreamResult; - } - - public override void OnFileDeleting(string path) - { - _first.OnFileDeleting(path); - _second.OnFileDeleting(path); - } - } - } -} From 5358051a603170cb21df89d80efefc8066289117 Mon Sep 17 00:00:00 2001 From: cocowalla <800977+cocowalla@users.noreply.github.com> Date: Wed, 29 Apr 2020 14:51:11 +0100 Subject: [PATCH 12/29] docs: file lifecycle hooks --- README.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/README.md b/README.md index 5a78565..4bac086 100644 --- a/README.md +++ b/README.md @@ -193,4 +193,19 @@ By default, the file sink will flush each event written through it to disk. To i 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. +### Extensibility +[`FileLifecycleHooks`](https://github.com/serilog/serilog-sinks-file/blob/master/src/Serilog.Sinks.File/Sinks/File/FileLifecycleHooks.cs) provide an extensibility point that allows hooking into different parts of the life cycle of a log file. + +You can create a hook by extending from [`FileLifecycleHooks`](https://github.com/serilog/serilog-sinks-file/blob/master/src/Serilog.Sinks.File/Sinks/File/FileLifecycleHooks.cs) and overriding the `OnFileOpened` and/or `OnFileDeleting` methods. + +- `OnFileOpened` provides access to the underlying stream that log events are written to, before Serilog begins writing events. You can use this to write your own data to the stream (for example, to write a header row), or to wrap the stream in another stream (for example, to add buffering, compression or encryption) + +- `OnFileDeleting` provides a means to work with obsolete rolling log files, *before* they are deleted by Serilog's retention mechanism - for example, to archive log files to another location + +Available hooks: + +- [serilog-sinks-file-header](https://github.com/cocowalla/serilog-sinks-file-header): writes a header to the start of each log file +- [serilog-sinks-file-gzip](https://github.com/cocowalla/serilog-sinks-file-gzip): compresses logs as they are written, using streaming GZIP compression +- [serilog-sinks-file-archive](https://github.com/cocowalla/serilog-sinks-file-archive): archives obsolete rolling log files before they are deleted by Serilog's retention mechanism + _Copyright © 2016 Serilog Contributors - Provided under the [Apache License, Version 2.0](http://apache.org/licenses/LICENSE-2.0.html)._ From 2be5531de626c35a56920a594c3dd804f8aba5f6 Mon Sep 17 00:00:00 2001 From: cocowalla <800977+cocowalla@users.noreply.github.com> Date: Fri, 15 May 2020 20:45:27 +0100 Subject: [PATCH 13/29] fix: #146, file size limit --- .../FileLoggerConfigurationExtensions.cs | 10 +++++----- src/Serilog.Sinks.File/Sinks/File/FileSink.cs | 2 +- src/Serilog.Sinks.File/Sinks/File/RollingFileSink.cs | 4 ++-- .../Sinks/File/SharedFileSink.AtomicAppend.cs | 4 ++-- .../Sinks/File/SharedFileSink.OSMutex.cs | 4 ++-- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs b/src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs index ad6b80b..74383b2 100644 --- a/src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs +++ b/src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs @@ -32,7 +32,7 @@ namespace Serilog public static class FileLoggerConfigurationExtensions { const int DefaultRetainedFileCountLimit = 31; // A long month of logs - const long DefaultFileSizeLimitBytes = 1L * 1024 * 1024 * 1024; + const long DefaultFileSizeLimitBytes = 1L * 1024 * 1024 * 1024; // 1GB const string DefaultOutputTemplate = "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception}"; /// @@ -181,7 +181,7 @@ 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. @@ -227,7 +227,7 @@ 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. @@ -377,7 +377,7 @@ public static LoggerConfiguration File( { return File(sinkConfiguration, formatter, path, restrictedToMinimumLevel, levelSwitch, null, null); } - + /// /// Write audit log events to the specified file. /// @@ -466,7 +466,7 @@ static LoggerConfiguration ConfigureFile( 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.", nameof(fileSizeLimitBytes)); + if (fileSizeLimitBytes.HasValue && fileSizeLimitBytes < 1) throw new ArgumentException("Invalid value provided; file size limit must be at least 1 byte, or null.", nameof(fileSizeLimitBytes)); if (retainedFileCountLimit.HasValue && retainedFileCountLimit < 1) throw new ArgumentException("At least one file must be retained.", nameof(retainedFileCountLimit)); if (retainedFileTimeLimit.HasValue && retainedFileTimeLimit < TimeSpan.Zero) throw new ArgumentException("Negative value provided; retained file time limit must be non-negative.", nameof(retainedFileTimeLimit)); if (shared && buffered) throw new ArgumentException("Buffered writes are not available when file sharing is enabled.", nameof(buffered)); diff --git a/src/Serilog.Sinks.File/Sinks/File/FileSink.cs b/src/Serilog.Sinks.File/Sinks/File/FileSink.cs index 8a913fa..451ba55 100644 --- a/src/Serilog.Sinks.File/Sinks/File/FileSink.cs +++ b/src/Serilog.Sinks.File/Sinks/File/FileSink.cs @@ -61,7 +61,7 @@ internal FileSink( 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 (fileSizeLimitBytes.HasValue && fileSizeLimitBytes < 1) throw new ArgumentException("Invalid value provided; file size limit must be at least 1 byte, or null."); _textFormatter = textFormatter ?? throw new ArgumentNullException(nameof(textFormatter)); _fileSizeLimitBytes = fileSizeLimitBytes; _buffered = buffered; diff --git a/src/Serilog.Sinks.File/Sinks/File/RollingFileSink.cs b/src/Serilog.Sinks.File/Sinks/File/RollingFileSink.cs index d007cd5..0eb4463 100644 --- a/src/Serilog.Sinks.File/Sinks/File/RollingFileSink.cs +++ b/src/Serilog.Sinks.File/Sinks/File/RollingFileSink.cs @@ -55,7 +55,7 @@ public RollingFileSink(string path, TimeSpan? retainedFileTimeLimit) { 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 (fileSizeLimitBytes.HasValue && fileSizeLimitBytes < 1) throw new ArgumentException("Invalid value provided; file size limit must be at least 1 byte, or null."); if (retainedFileCountLimit.HasValue && retainedFileCountLimit < 1) throw new ArgumentException("Zero or negative value provided; retained file count limit must be at least 1"); if (retainedFileTimeLimit.HasValue && retainedFileTimeLimit < TimeSpan.Zero) throw new ArgumentException("Negative value provided; retained file time limit must be non-negative.", nameof(retainedFileTimeLimit)); @@ -223,7 +223,7 @@ bool ShouldRetainFile(RollingLogFile file, int index, DateTime now) { return false; } - + return true; } diff --git a/src/Serilog.Sinks.File/Sinks/File/SharedFileSink.AtomicAppend.cs b/src/Serilog.Sinks.File/Sinks/File/SharedFileSink.AtomicAppend.cs index 866f807..25e8867 100644 --- a/src/Serilog.Sinks.File/Sinks/File/SharedFileSink.AtomicAppend.cs +++ b/src/Serilog.Sinks.File/Sinks/File/SharedFileSink.AtomicAppend.cs @@ -53,8 +53,8 @@ public sealed class SharedFileSink : IFileSink, IDisposable /// public SharedFileSink(string path, ITextFormatter textFormatter, long? fileSizeLimitBytes, Encoding encoding = null) { - if (fileSizeLimitBytes.HasValue && fileSizeLimitBytes < 0) - throw new ArgumentException("Negative value provided; file size limit must be non-negative"); + if (fileSizeLimitBytes.HasValue && fileSizeLimitBytes < 1) + throw new ArgumentException("Invalid value provided; file size limit must be at least 1 byte, or null"); _path = path ?? throw new ArgumentNullException(nameof(path)); _textFormatter = textFormatter ?? 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 index 41a19ef..38b2326 100644 --- a/src/Serilog.Sinks.File/Sinks/File/SharedFileSink.OSMutex.cs +++ b/src/Serilog.Sinks.File/Sinks/File/SharedFileSink.OSMutex.cs @@ -53,8 +53,8 @@ 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 (fileSizeLimitBytes.HasValue && fileSizeLimitBytes < 0) - throw new ArgumentException("Negative value provided; file size limit must be non-negative"); + if (fileSizeLimitBytes.HasValue && fileSizeLimitBytes < 1) + throw new ArgumentException("Invalid value provided; file size limit must be at least 1 byte, or null."); _textFormatter = textFormatter ?? throw new ArgumentNullException(nameof(textFormatter)); _fileSizeLimitBytes = fileSizeLimitBytes; From 455c32c96d4b08aa70a6a4bdce856327726ef1aa Mon Sep 17 00:00:00 2001 From: Nicholas Blumhardt Date: Sat, 16 May 2020 07:31:17 +1000 Subject: [PATCH 14/29] Update NuGet.org publishing key --- appveyor.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index ef5f126..7e41204 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -18,7 +18,7 @@ artifacts: deploy: - provider: NuGet api_key: - secure: N59tiJECUYpip6tEn0xvdmDAEiP9SIzyLEFLpwiigm/8WhJvBNs13QxzT1/3/JW/ + secure: K3/810hkTO6rd2AEHVkUQAadjGmDREus9k96QHu6hxrA1/wRTuAJemHMKtVVgIvf skip_symbols: true on: branch: /^(master|dev)$/ From e44e591c5e7497404daa32e3014ae534372a31bc Mon Sep 17 00:00:00 2001 From: Rafael Cordeiro Date: Wed, 22 Jul 2020 00:39:42 -0700 Subject: [PATCH 15/29] Improve XML Docs --- .../FileLoggerConfigurationExtensions.cs | 54 +++++++++++++++++++ src/Serilog.Sinks.File/Sinks/File/FileSink.cs | 9 ++++ .../Sinks/File/PeriodicFlushToDiskSink.cs | 2 +- .../Sinks/File/SharedFileSink.AtomicAppend.cs | 8 +++ .../Sinks/File/SharedFileSink.OSMutex.cs | 8 +++ 5 files changed, 80 insertions(+), 1 deletion(-) diff --git a/src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs b/src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs index 74383b2..d3e7d8f 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.IO; using System.Text; using Serilog.Configuration; using Serilog.Core; @@ -238,6 +239,15 @@ public static LoggerConfiguration File( /// Ignored if is . /// The default is to retain files indefinitely. /// Configuration object allowing method chaining. + /// When is null + /// When is null + /// When is null + /// + /// + /// + /// When is too long + /// The caller does not have the required permission to access the + /// Invalid public static LoggerConfiguration File( this LoggerSinkConfiguration sinkConfiguration, string path, @@ -299,6 +309,15 @@ public static LoggerConfiguration File( /// Ignored if is . /// The default is to retain files indefinitely. /// Configuration object allowing method chaining. + /// When is null + /// When is null + /// When is null + /// + /// + /// + /// When is too long + /// The caller does not have the required permission to access the + /// Invalid public static LoggerConfiguration File( this LoggerSinkConfiguration sinkConfiguration, ITextFormatter formatter, @@ -339,6 +358,14 @@ 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. + /// When is null + /// When is null + /// + /// + /// + /// When is too long + /// The caller does not have the required permission to access the + /// Invalid [Obsolete("New code should not be compiled against this obsolete overload"), EditorBrowsable(EditorBrowsableState.Never)] public static LoggerConfiguration File( this LoggerAuditSinkConfiguration sinkConfiguration, @@ -367,6 +394,15 @@ 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. + /// When is null + /// When is null + /// When is null + /// + /// + /// + /// When is too long + /// The caller does not have the required permission to access the + /// Invalid [Obsolete("New code should not be compiled against this obsolete overload"), EditorBrowsable(EditorBrowsableState.Never)] public static LoggerConfiguration File( this LoggerAuditSinkConfiguration sinkConfiguration, @@ -393,6 +429,15 @@ 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. + /// When is null + /// When is null + /// When is null + /// + /// + /// + /// When is too long + /// The caller does not have the required permission to access the + /// Invalid public static LoggerConfiguration File( this LoggerAuditSinkConfiguration sinkConfiguration, string path, @@ -428,6 +473,15 @@ 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. + /// When is null + /// When is null + /// When is null + /// + /// + /// + /// When is too long + /// The caller does not have the required permission to access the + /// Invalid public static LoggerConfiguration File( this LoggerAuditSinkConfiguration sinkConfiguration, ITextFormatter formatter, diff --git a/src/Serilog.Sinks.File/Sinks/File/FileSink.cs b/src/Serilog.Sinks.File/Sinks/File/FileSink.cs index 451ba55..bc89ed9 100644 --- a/src/Serilog.Sinks.File/Sinks/File/FileSink.cs +++ b/src/Serilog.Sinks.File/Sinks/File/FileSink.cs @@ -44,7 +44,15 @@ public sealed class FileSink : IFileSink, IDisposable /// is false. /// Configuration object allowing method chaining. /// This constructor preserves compatibility with early versions of the public API. New code should not depend on this type. + /// When is null + /// When is null or less than 0 + /// When is null /// + /// + /// + /// When is too long + /// The caller does not have the required permission to access the + /// Invalid [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) @@ -113,6 +121,7 @@ bool IFileSink.EmitOrOverflow(LogEvent logEvent) /// Emit the provided log event to the sink. /// /// The log event to write. + /// When is null public void Emit(LogEvent logEvent) { ((IFileSink) this).EmitOrOverflow(logEvent); diff --git a/src/Serilog.Sinks.File/Sinks/File/PeriodicFlushToDiskSink.cs b/src/Serilog.Sinks.File/Sinks/File/PeriodicFlushToDiskSink.cs index 66b0868..1b931c9 100644 --- a/src/Serilog.Sinks.File/Sinks/File/PeriodicFlushToDiskSink.cs +++ b/src/Serilog.Sinks.File/Sinks/File/PeriodicFlushToDiskSink.cs @@ -36,7 +36,7 @@ public class PeriodicFlushToDiskSink : ILogEventSink, IDisposable /// /// The sink to wrap. /// The interval at which to flush the underlying sink. - /// + /// When is null public PeriodicFlushToDiskSink(ILogEventSink sink, TimeSpan flushInterval) { _sink = sink ?? throw new ArgumentNullException(nameof(sink)); diff --git a/src/Serilog.Sinks.File/Sinks/File/SharedFileSink.AtomicAppend.cs b/src/Serilog.Sinks.File/Sinks/File/SharedFileSink.AtomicAppend.cs index 25e8867..723058e 100644 --- a/src/Serilog.Sinks.File/Sinks/File/SharedFileSink.AtomicAppend.cs +++ b/src/Serilog.Sinks.File/Sinks/File/SharedFileSink.AtomicAppend.cs @@ -50,7 +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. + /// When is null + /// When is null /// + /// + /// + /// When is too long + /// The caller does not have the required permission to access the + /// Invalid public SharedFileSink(string path, ITextFormatter textFormatter, long? fileSizeLimitBytes, Encoding encoding = null) { if (fileSizeLimitBytes.HasValue && fileSizeLimitBytes < 1) @@ -141,6 +148,7 @@ bool IFileSink.EmitOrOverflow(LogEvent logEvent) /// Emit the provided log event to the sink. /// /// The log event to write. + /// When is null public void Emit(LogEvent logEvent) { ((IFileSink)this).EmitOrOverflow(logEvent); diff --git a/src/Serilog.Sinks.File/Sinks/File/SharedFileSink.OSMutex.cs b/src/Serilog.Sinks.File/Sinks/File/SharedFileSink.OSMutex.cs index 38b2326..2aad6a2 100644 --- a/src/Serilog.Sinks.File/Sinks/File/SharedFileSink.OSMutex.cs +++ b/src/Serilog.Sinks.File/Sinks/File/SharedFileSink.OSMutex.cs @@ -49,7 +49,14 @@ public sealed class SharedFileSink : IFileSink, IDisposable /// 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. + /// When is null + /// When is null /// + /// + /// + /// When is too long + /// The caller does not have the required permission to access the + /// Invalid public SharedFileSink(string path, ITextFormatter textFormatter, long? fileSizeLimitBytes, Encoding encoding = null) { if (path == null) throw new ArgumentNullException(nameof(path)); @@ -104,6 +111,7 @@ bool IFileSink.EmitOrOverflow(LogEvent logEvent) /// Emit the provided log event to the sink. /// /// The log event to write. + /// When is null public void Emit(LogEvent logEvent) { ((IFileSink)this).EmitOrOverflow(logEvent); From f155fab5b550e3e2989a241cbc9b2f6e44cf8503 Mon Sep 17 00:00:00 2001 From: "C. Augusto Proiete" Date: Mon, 9 Nov 2020 00:24:57 -0400 Subject: [PATCH 16/29] Update project to use SPDX LicenseExpression (resolve warning NU5125: The 'licenseUrl' element will be deprecated) --- 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 f498bf6..8a35c53 100644 --- a/src/Serilog.Sinks.File/Serilog.Sinks.File.csproj +++ b/src/Serilog.Sinks.File/Serilog.Sinks.File.csproj @@ -12,7 +12,7 @@ serilog;file http://serilog.net/images/serilog-sink-nuget.png http://serilog.net - http://www.apache.org/licenses/LICENSE-2.0 + Apache-2.0 https://github.com/serilog/serilog-sinks-file git false From d8c6571fefb48753783c40a145348d9d7c31e326 Mon Sep 17 00:00:00 2001 From: "C. Augusto Proiete" Date: Mon, 9 Nov 2020 00:28:42 -0400 Subject: [PATCH 17/29] Add embedded icon to NuGet package (resolve warning NU5048: The 'PackageIconUrl'/'iconUrl' element is deprecated) --- assets/serilog-sink-nuget.png | Bin 0 -> 20852 bytes .../Serilog.Sinks.File.csproj | 8 +++++++- 2 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 assets/serilog-sink-nuget.png diff --git a/assets/serilog-sink-nuget.png b/assets/serilog-sink-nuget.png new file mode 100644 index 0000000000000000000000000000000000000000..a77d65c42658e49e801aebde26b767d3916e6971 GIT binary patch literal 20852 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 AFaQ7m literal 0 HcmV?d00001 diff --git a/src/Serilog.Sinks.File/Serilog.Sinks.File.csproj b/src/Serilog.Sinks.File/Serilog.Sinks.File.csproj index 8a35c53..7b81e3f 100644 --- a/src/Serilog.Sinks.File/Serilog.Sinks.File.csproj +++ b/src/Serilog.Sinks.File/Serilog.Sinks.File.csproj @@ -10,7 +10,8 @@ true true serilog;file - http://serilog.net/images/serilog-sink-nuget.png + images\icon.png + https://serilog.net/images/serilog-sink-nuget.png http://serilog.net Apache-2.0 https://github.com/serilog/serilog-sinks-file @@ -56,4 +57,9 @@ + + + + + From 4c8c3d4868b2b495464d395e67ac1279b3778b1e Mon Sep 17 00:00:00 2001 From: "C. Augusto Proiete" Date: Sun, 8 Nov 2020 23:49:32 -0400 Subject: [PATCH 18/29] Add ability to capture path of log file opened, via FileLifecycleHooks --- .../Sinks/File/FileLifeCycleHookChain.cs | 6 +++--- .../Sinks/File/FileLifecycleHooks.cs | 15 ++++++++++++++ src/Serilog.Sinks.File/Sinks/File/FileSink.cs | 2 +- .../Serilog.Sinks.File.Tests/FileSinkTests.cs | 17 ++++++++++++++++ .../Support/CaptureFilePathHook.cs | 20 +++++++++++++++++++ 5 files changed, 56 insertions(+), 4 deletions(-) create mode 100644 test/Serilog.Sinks.File.Tests/Support/CaptureFilePathHook.cs diff --git a/src/Serilog.Sinks.File/Sinks/File/FileLifeCycleHookChain.cs b/src/Serilog.Sinks.File/Sinks/File/FileLifeCycleHookChain.cs index cea5095..cf27bfe 100644 --- a/src/Serilog.Sinks.File/Sinks/File/FileLifeCycleHookChain.cs +++ b/src/Serilog.Sinks.File/Sinks/File/FileLifeCycleHookChain.cs @@ -29,10 +29,10 @@ public FileLifeCycleHookChain(FileLifecycleHooks first, FileLifecycleHooks secon _second = second ?? throw new ArgumentNullException(nameof(second)); } - public override Stream OnFileOpened(Stream underlyingStream, Encoding encoding) + public override Stream OnFileOpened(string path, Stream underlyingStream, Encoding encoding) { - var firstStreamResult = _first.OnFileOpened(underlyingStream, encoding); - var secondStreamResult = _second.OnFileOpened(firstStreamResult, encoding); + var firstStreamResult = _first.OnFileOpened(path, underlyingStream, encoding); + var secondStreamResult = _second.OnFileOpened(path, firstStreamResult, encoding); return secondStreamResult; } diff --git a/src/Serilog.Sinks.File/Sinks/File/FileLifecycleHooks.cs b/src/Serilog.Sinks.File/Sinks/File/FileLifecycleHooks.cs index e804cad..26fd1e2 100644 --- a/src/Serilog.Sinks.File/Sinks/File/FileLifecycleHooks.cs +++ b/src/Serilog.Sinks.File/Sinks/File/FileLifecycleHooks.cs @@ -23,6 +23,21 @@ namespace Serilog.Sinks.File /// public abstract class FileLifecycleHooks { + /// + /// 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. + /// + /// + /// 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 full path to the log file. + /// 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 OnFileOpened(string path, Stream underlyingStream, Encoding encoding) => OnFileOpened(underlyingStream, encoding); + /// /// 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 diff --git a/src/Serilog.Sinks.File/Sinks/File/FileSink.cs b/src/Serilog.Sinks.File/Sinks/File/FileSink.cs index bc89ed9..dd83a3f 100644 --- a/src/Serilog.Sinks.File/Sinks/File/FileSink.cs +++ b/src/Serilog.Sinks.File/Sinks/File/FileSink.cs @@ -91,7 +91,7 @@ internal FileSink( if (hooks != null) { - outputStream = hooks.OnFileOpened(outputStream, encoding) ?? + outputStream = hooks.OnFileOpened(path, outputStream, encoding) ?? throw new InvalidOperationException($"The file lifecycle hook `{nameof(FileLifecycleHooks.OnFileOpened)}(...)` returned `null`."); } diff --git a/test/Serilog.Sinks.File.Tests/FileSinkTests.cs b/test/Serilog.Sinks.File.Tests/FileSinkTests.cs index 10fe926..80c088f 100644 --- a/test/Serilog.Sinks.File.Tests/FileSinkTests.cs +++ b/test/Serilog.Sinks.File.Tests/FileSinkTests.cs @@ -206,6 +206,23 @@ public static void OnOpenedLifecycleHookCanWriteFileHeader() } } + [Fact] + public static void OnOpenedLifecycleHookCanCaptureFilePath() + { + using (var tmp = TempFolder.ForCaller()) + { + var capturePath = new CaptureFilePathHook(); + + var path = tmp.AllocateFilename("txt"); + using (new FileSink(path, new JsonFormatter(), null, new UTF8Encoding(false), false, capturePath)) + { + // Open and capture the log file path + } + + Assert.Equal(path, capturePath.Path); + } + } + static void WriteTwoEventsAndCheckOutputFileLength(long? maxBytes, Encoding encoding) { using (var tmp = TempFolder.ForCaller()) diff --git a/test/Serilog.Sinks.File.Tests/Support/CaptureFilePathHook.cs b/test/Serilog.Sinks.File.Tests/Support/CaptureFilePathHook.cs new file mode 100644 index 0000000..a116f95 --- /dev/null +++ b/test/Serilog.Sinks.File.Tests/Support/CaptureFilePathHook.cs @@ -0,0 +1,20 @@ +using System.IO; +using System.Text; + +namespace Serilog.Sinks.File.Tests.Support +{ + /// + /// + /// Demonstrates the use of , by capturing the log file path + /// + class CaptureFilePathHook : FileLifecycleHooks + { + public string Path { get; private set; } + + public override Stream OnFileOpened(string path, Stream _, Encoding __) + { + Path = path; + return base.OnFileOpened(path, _, __); + } + } +} From bd4d7dd485d281a42f60cef2a1d2211a0bb2b8f7 Mon Sep 17 00:00:00 2001 From: Petrik van der Velde Date: Tue, 1 Dec 2020 21:05:27 +1300 Subject: [PATCH 19/29] Including the RollingFileSink initialization in the try..catch so that any errors in the creation are included in the self log --- .../FileLoggerConfigurationExtensions.cs | 27 ++++++++++--------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs b/src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs index d3e7d8f..54b20b4 100644 --- a/src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs +++ b/src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs @@ -528,13 +528,13 @@ static LoggerConfiguration ConfigureFile( ILogEventSink sink; - if (rollOnFileSizeLimit || rollingInterval != RollingInterval.Infinite) + try { - sink = new RollingFileSink(path, formatter, fileSizeLimitBytes, retainedFileCountLimit, encoding, buffered, shared, rollingInterval, rollOnFileSizeLimit, hooks, retainedFileTimeLimit); - } - else - { - try + if (rollOnFileSizeLimit || rollingInterval != RollingInterval.Infinite) + { + sink = new RollingFileSink(path, formatter, fileSizeLimitBytes, retainedFileCountLimit, encoding, buffered, shared, rollingInterval, rollOnFileSizeLimit, hooks, retainedFileTimeLimit); + } + else { if (shared) { @@ -546,16 +546,17 @@ static LoggerConfiguration ConfigureFile( { sink = new FileSink(path, formatter, fileSizeLimitBytes, encoding, buffered, hooks); } + } - catch (Exception ex) - { - SelfLog.WriteLine("Unable to open file sink for {0}: {1}", path, ex); + } + catch (Exception 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) From 91cd684ae883f826ebcca7b44c8707274faac2b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Borja=20Dom=C3=ADnguez?= Date: Fri, 11 Jun 2021 18:20:12 +0200 Subject: [PATCH 20/29] Remove unnecessary dependencies --- src/Serilog.Sinks.File/Serilog.Sinks.File.csproj | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/Serilog.Sinks.File/Serilog.Sinks.File.csproj b/src/Serilog.Sinks.File/Serilog.Sinks.File.csproj index 7b81e3f..b2846b9 100644 --- a/src/Serilog.Sinks.File/Serilog.Sinks.File.csproj +++ b/src/Serilog.Sinks.File/Serilog.Sinks.File.csproj @@ -52,12 +52,6 @@ - - - - - - From d599eb69b88fbfd1a83bb87c6d3c58e2536349f6 Mon Sep 17 00:00:00 2001 From: Nicholas Blumhardt Date: Tue, 15 Jun 2021 16:17:17 +1000 Subject: [PATCH 21/29] Publishing key update --- appveyor.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index 7e41204..e72de6c 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -18,7 +18,7 @@ artifacts: deploy: - provider: NuGet api_key: - secure: K3/810hkTO6rd2AEHVkUQAadjGmDREus9k96QHu6hxrA1/wRTuAJemHMKtVVgIvf + secure: rbdBqxBpLt4MkB+mrDOYNDOd8aVZ1zMkysaVNAXNKnC41FYifzX3l9LM8DCrUWU5 skip_symbols: true on: branch: /^(master|dev)$/ From a1ba60e92d3324d212436dd45fe6d391e4bf5147 Mon Sep 17 00:00:00 2001 From: Nicholas Blumhardt Date: Tue, 22 Jun 2021 11:37:14 +1000 Subject: [PATCH 22/29] Unpin the assembly version; fixes #135 --- src/Serilog.Sinks.File/Properties/AssemblyInfo.cs | 2 -- src/Serilog.Sinks.File/Serilog.Sinks.File.csproj | 1 - 2 files changed, 3 deletions(-) diff --git a/src/Serilog.Sinks.File/Properties/AssemblyInfo.cs b/src/Serilog.Sinks.File/Properties/AssemblyInfo.cs index 0d5d620..93017cb 100644 --- a/src/Serilog.Sinks.File/Properties/AssemblyInfo.cs +++ b/src/Serilog.Sinks.File/Properties/AssemblyInfo.cs @@ -2,8 +2,6 @@ using System.Reflection; using System.Runtime.CompilerServices; -[assembly: AssemblyVersion("2.0.0.0")] - [assembly: CLSCompliant(true)] [assembly: InternalsVisibleTo("Serilog.Sinks.File.Tests, PublicKey=" + diff --git a/src/Serilog.Sinks.File/Serilog.Sinks.File.csproj b/src/Serilog.Sinks.File/Serilog.Sinks.File.csproj index b2846b9..493078d 100644 --- a/src/Serilog.Sinks.File/Serilog.Sinks.File.csproj +++ b/src/Serilog.Sinks.File/Serilog.Sinks.File.csproj @@ -16,7 +16,6 @@ Apache-2.0 https://github.com/serilog/serilog-sinks-file git - false Serilog true true From 6f4375147895abba5c72d9b6410e0c1f7e9d5f3b Mon Sep 17 00:00:00 2001 From: Nicholas Blumhardt Date: Tue, 22 Jun 2021 12:07:53 +1000 Subject: [PATCH 23/29] Remove matching test --- test/Serilog.Sinks.File.Tests/RollingFileSinkTests.cs | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/test/Serilog.Sinks.File.Tests/RollingFileSinkTests.cs b/test/Serilog.Sinks.File.Tests/RollingFileSinkTests.cs index d295dfc..fb9c9bf 100644 --- a/test/Serilog.Sinks.File.Tests/RollingFileSinkTests.cs +++ b/test/Serilog.Sinks.File.Tests/RollingFileSinkTests.cs @@ -3,7 +3,6 @@ using System.IO; using System.IO.Compression; using System.Linq; -using System.Reflection; using Xunit; using Serilog.Events; using Serilog.Sinks.File.Tests.Support; @@ -128,7 +127,7 @@ public void WhenRetentionCountAndTimeIsSetOldFilesAreDeletedByCount() Assert.True(System.IO.File.Exists(files[2])); }); } - + [Fact] public void WhenRetentionCountAndArchivingHookIsSetOldFilesAreCopiedAndOriginalDeleted() { @@ -261,13 +260,6 @@ public void IfTheLogFolderDoesNotExistItWillBeCreated() } } - [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( From 08ff44551ba38bc23c071f7eb1873fed4a791f45 Mon Sep 17 00:00:00 2001 From: Rafael Sliveira Cordeiro Date: Mon, 21 Jun 2021 19:22:20 -0700 Subject: [PATCH 24/29] Add support C# 8 nullable reference types (#166) * Initial Nullable Support * Update Nullable Reference Support * Update appveyor config * Update Test Project * Update Test Project * Code Format * Code Refactor * Remove unnecessary references * Simplifying Constants * Add More TargetFrameworks for Update References Cleanup References * Remove netcoreapp3.0 to use netstandard2.0 insted * Unpin the assembly version; fixes #135 * Remove matching test Co-authored-by: Nicholas Blumhardt --- appveyor.yml | 4 +- example/Sample/Sample.csproj | 11 +- .../FileLoggerConfigurationExtensions.cs | 36 ++--- .../Serilog.Sinks.File.csproj | 22 +-- src/Serilog.Sinks.File/Sinks/File/FileSink.cs | 10 +- .../Sinks/File/RollingFileSink.cs | 14 +- .../Sinks/File/SharedFileSink.AtomicAppend.cs | 2 +- .../Sinks/File/SharedFileSink.OSMutex.cs | 2 +- .../Support/NullableAttributes..cs | 140 ++++++++++++++++++ .../RollingFileSinkTests.cs | 4 +- .../RollingIntervalExtensionsTests.cs | 24 +-- .../Serilog.Sinks.File.Tests.csproj | 21 ++- .../Support/CaptureFilePathHook.cs | 2 +- .../Support/DelegatingSink.cs | 4 +- .../Support/Extensions.cs | 2 +- test/Serilog.Sinks.File.Tests/Support/Some.cs | 2 +- .../Support/TempFolder.cs | 6 +- 17 files changed, 215 insertions(+), 91 deletions(-) create mode 100644 src/Serilog.Sinks.File/Support/NullableAttributes..cs diff --git a/appveyor.yml b/appveyor.yml index e72de6c..ad4973c 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -2,14 +2,14 @@ version: '{build}' skip_tags: true image: - Visual Studio 2019 - - Ubuntu1804 + - Ubuntu build_script: - ps: ./Build.ps1 for: - matrix: only: - - image: Ubuntu1804 + - image: Ubuntu build_script: - sh build.sh test: off diff --git a/example/Sample/Sample.csproj b/example/Sample/Sample.csproj index e307db1..4915e19 100644 --- a/example/Sample/Sample.csproj +++ b/example/Sample/Sample.csproj @@ -1,7 +1,9 @@ - + - netcoreapp2.0;net47 + net48;net5.0 + 8.0 + enable Sample Exe Sample @@ -12,10 +14,5 @@ - - - - - diff --git a/src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs b/src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs index 54b20b4..3518322 100644 --- a/src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs +++ b/src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs @@ -253,17 +253,17 @@ public static LoggerConfiguration File( string path, LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum, string outputTemplate = DefaultOutputTemplate, - IFormatProvider formatProvider = null, + IFormatProvider? formatProvider = null, long? fileSizeLimitBytes = DefaultFileSizeLimitBytes, - LoggingLevelSwitch levelSwitch = null, + LoggingLevelSwitch? levelSwitch = null, bool buffered = false, bool shared = false, TimeSpan? flushToDiskInterval = null, RollingInterval rollingInterval = RollingInterval.Infinite, bool rollOnFileSizeLimit = false, int? retainedFileCountLimit = DefaultRetainedFileCountLimit, - Encoding encoding = null, - FileLifecycleHooks hooks = null, + Encoding? encoding = null, + FileLifecycleHooks? hooks = null, TimeSpan? retainedFileTimeLimit = null) { if (sinkConfiguration == null) throw new ArgumentNullException(nameof(sinkConfiguration)); @@ -324,15 +324,15 @@ public static LoggerConfiguration File( string path, LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum, long? fileSizeLimitBytes = DefaultFileSizeLimitBytes, - LoggingLevelSwitch levelSwitch = null, + LoggingLevelSwitch? levelSwitch = null, bool buffered = false, bool shared = false, TimeSpan? flushToDiskInterval = null, RollingInterval rollingInterval = RollingInterval.Infinite, bool rollOnFileSizeLimit = false, int? retainedFileCountLimit = DefaultRetainedFileCountLimit, - Encoding encoding = null, - FileLifecycleHooks hooks = null, + Encoding? encoding = null, + FileLifecycleHooks? hooks = null, TimeSpan? retainedFileTimeLimit = null) { if (sinkConfiguration == null) throw new ArgumentNullException(nameof(sinkConfiguration)); @@ -443,10 +443,10 @@ public static LoggerConfiguration File( string path, LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum, string outputTemplate = DefaultOutputTemplate, - IFormatProvider formatProvider = null, - LoggingLevelSwitch levelSwitch = null, - Encoding encoding = null, - FileLifecycleHooks hooks = null) + IFormatProvider? formatProvider = 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)); @@ -487,9 +487,9 @@ public static LoggerConfiguration File( ITextFormatter formatter, string path, LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum, - LoggingLevelSwitch levelSwitch = null, - Encoding encoding = null, - FileLifecycleHooks hooks = 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)); @@ -500,21 +500,21 @@ public static LoggerConfiguration File( } static LoggerConfiguration ConfigureFile( - this Func addSink, + this Func addSink, ITextFormatter formatter, string path, LogEventLevel restrictedToMinimumLevel, long? fileSizeLimitBytes, - LoggingLevelSwitch levelSwitch, + LoggingLevelSwitch? levelSwitch, bool buffered, bool propagateExceptions, bool shared, TimeSpan? flushToDiskInterval, - Encoding encoding, + Encoding? encoding, RollingInterval rollingInterval, bool rollOnFileSizeLimit, int? retainedFileCountLimit, - FileLifecycleHooks hooks, + FileLifecycleHooks? hooks, TimeSpan? retainedFileTimeLimit) { if (addSink == null) throw new ArgumentNullException(nameof(addSink)); diff --git a/src/Serilog.Sinks.File/Serilog.Sinks.File.csproj b/src/Serilog.Sinks.File/Serilog.Sinks.File.csproj index 493078d..6db7104 100644 --- a/src/Serilog.Sinks.File/Serilog.Sinks.File.csproj +++ b/src/Serilog.Sinks.File/Serilog.Sinks.File.csproj @@ -4,7 +4,9 @@ Write Serilog events to text files in plain or JSON format. 5.0.0 Serilog Contributors - net45;netstandard1.3;netstandard2.0;netcoreapp3.0 + net45;netstandard1.3;netstandard2.0;netstandard2.1 + 8.0 + enable true ../../assets/Serilog.snk true @@ -33,26 +35,12 @@ $(DefineConstants);ATOMIC_APPEND;HRESULTS - + $(DefineConstants);OS_MUTEX - - $(DefineConstants);OS_MUTEX - - - - - - - - - - - - - + diff --git a/src/Serilog.Sinks.File/Sinks/File/FileSink.cs b/src/Serilog.Sinks.File/Sinks/File/FileSink.cs index dd83a3f..76ada0b 100644 --- a/src/Serilog.Sinks.File/Sinks/File/FileSink.cs +++ b/src/Serilog.Sinks.File/Sinks/File/FileSink.cs @@ -31,7 +31,7 @@ public sealed class FileSink : IFileSink, IDisposable readonly long? _fileSizeLimitBytes; readonly bool _buffered; readonly object _syncRoot = new object(); - readonly WriteCountingStream _countingStreamWrapper; + readonly WriteCountingStream? _countingStreamWrapper; /// Construct a . /// Path to the file. @@ -54,7 +54,7 @@ public sealed class FileSink : IFileSink, IDisposable /// The caller does not have the required permission to access the /// Invalid [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) + public FileSink(string path, ITextFormatter textFormatter, long? fileSizeLimitBytes, Encoding? encoding = null, bool buffered = false) : this(path, textFormatter, fileSizeLimitBytes, encoding, buffered, null) { } @@ -64,9 +64,9 @@ internal FileSink( string path, ITextFormatter textFormatter, long? fileSizeLimitBytes, - Encoding encoding, + Encoding? encoding, bool buffered, - FileLifecycleHooks hooks) + FileLifecycleHooks? hooks) { if (path == null) throw new ArgumentNullException(nameof(path)); if (fileSizeLimitBytes.HasValue && fileSizeLimitBytes < 1) throw new ArgumentException("Invalid value provided; file size limit must be at least 1 byte, or null."); @@ -105,7 +105,7 @@ bool IFileSink.EmitOrOverflow(LogEvent logEvent) { if (_fileSizeLimitBytes != null) { - if (_countingStreamWrapper.CountedLength >= _fileSizeLimitBytes.Value) + if (_countingStreamWrapper!.CountedLength >= _fileSizeLimitBytes.Value) return false; } diff --git a/src/Serilog.Sinks.File/Sinks/File/RollingFileSink.cs b/src/Serilog.Sinks.File/Sinks/File/RollingFileSink.cs index 0eb4463..dccb802 100644 --- a/src/Serilog.Sinks.File/Sinks/File/RollingFileSink.cs +++ b/src/Serilog.Sinks.File/Sinks/File/RollingFileSink.cs @@ -30,28 +30,28 @@ sealed class RollingFileSink : ILogEventSink, IFlushableFileSink, IDisposable readonly long? _fileSizeLimitBytes; readonly int? _retainedFileCountLimit; readonly TimeSpan? _retainedFileTimeLimit; - readonly Encoding _encoding; + readonly Encoding? _encoding; readonly bool _buffered; readonly bool _shared; readonly bool _rollOnFileSizeLimit; - readonly FileLifecycleHooks _hooks; + readonly FileLifecycleHooks? _hooks; readonly object _syncRoot = new object(); bool _isDisposed; DateTime? _nextCheckpoint; - IFileSink _currentFile; + IFileSink? _currentFile; int? _currentFileSequence; public RollingFileSink(string path, ITextFormatter textFormatter, long? fileSizeLimitBytes, int? retainedFileCountLimit, - Encoding encoding, + Encoding? encoding, bool buffered, bool shared, RollingInterval rollingInterval, bool rollOnFileSizeLimit, - FileLifecycleHooks hooks, + FileLifecycleHooks? hooks, TimeSpan? retainedFileTimeLimit) { if (path == null) throw new ArgumentNullException(nameof(path)); @@ -125,7 +125,7 @@ void OpenFile(DateTime now, int? minSequence = null) if (Directory.Exists(_roller.LogFileDirectory)) { existingFiles = Directory.GetFiles(_roller.LogFileDirectory, _roller.DirectorySearchPattern) - .Select(Path.GetFileName); + .Select(f => Path.GetFileName(f)); } } catch (DirectoryNotFoundException) { } @@ -184,7 +184,7 @@ void ApplyRetentionPolicy(string currentFilePath, DateTime now) // 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) + .Select(f => Path.GetFileName(f)) .Union(new[] { currentFileName }); var newestFirst = _roller diff --git a/src/Serilog.Sinks.File/Sinks/File/SharedFileSink.AtomicAppend.cs b/src/Serilog.Sinks.File/Sinks/File/SharedFileSink.AtomicAppend.cs index 723058e..6cf55cb 100644 --- a/src/Serilog.Sinks.File/Sinks/File/SharedFileSink.AtomicAppend.cs +++ b/src/Serilog.Sinks.File/Sinks/File/SharedFileSink.AtomicAppend.cs @@ -58,7 +58,7 @@ public sealed class SharedFileSink : IFileSink, IDisposable /// When is too long /// The caller does not have the required permission to access the /// Invalid - public SharedFileSink(string path, ITextFormatter textFormatter, long? fileSizeLimitBytes, Encoding encoding = null) + public SharedFileSink(string path, ITextFormatter textFormatter, long? fileSizeLimitBytes, Encoding? encoding = null) { if (fileSizeLimitBytes.HasValue && fileSizeLimitBytes < 1) throw new ArgumentException("Invalid value provided; file size limit must be at least 1 byte, or null"); diff --git a/src/Serilog.Sinks.File/Sinks/File/SharedFileSink.OSMutex.cs b/src/Serilog.Sinks.File/Sinks/File/SharedFileSink.OSMutex.cs index 2aad6a2..b8a07db 100644 --- a/src/Serilog.Sinks.File/Sinks/File/SharedFileSink.OSMutex.cs +++ b/src/Serilog.Sinks.File/Sinks/File/SharedFileSink.OSMutex.cs @@ -57,7 +57,7 @@ public sealed class SharedFileSink : IFileSink, IDisposable /// When is too long /// The caller does not have the required permission to access the /// Invalid - 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 (fileSizeLimitBytes.HasValue && fileSizeLimitBytes < 1) diff --git a/src/Serilog.Sinks.File/Support/NullableAttributes..cs b/src/Serilog.Sinks.File/Support/NullableAttributes..cs new file mode 100644 index 0000000..c7a2018 --- /dev/null +++ b/src/Serilog.Sinks.File/Support/NullableAttributes..cs @@ -0,0 +1,140 @@ +#pragma warning disable MA0048 // File name must match type name +#define INTERNAL_NULLABLE_ATTRIBUTES +#if NETSTANDARD1_0 || NETSTANDARD1_1 || NETSTANDARD1_2 || NETSTANDARD1_3 || NETSTANDARD1_4 || NETSTANDARD1_5 || NETSTANDARD1_6 || NETSTANDARD2_0 || NETCOREAPP1_0 || NETCOREAPP1_1 || NETCOREAPP2_0 || NETCOREAPP2_1 || NETCOREAPP2_2 || NET45 || NET451 || NET452 || NET46 || NET461 || NET462 || NET47 || NET471 || NET472 || NET48 + +// https://github.com/dotnet/corefx/blob/48363ac826ccf66fbe31a5dcb1dc2aab9a7dd768/src/Common/src/CoreLib/System/Diagnostics/CodeAnalysis/NullableAttributes.cs + +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace System.Diagnostics.CodeAnalysis +{ + /// Specifies that null is allowed as an input even if the corresponding type disallows it. + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property, Inherited = false)] +#if INTERNAL_NULLABLE_ATTRIBUTES + internal +#else + public +#endif + sealed class AllowNullAttribute : Attribute + { } + + /// Specifies that null is disallowed as an input even if the corresponding type allows it. + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property, Inherited = false)] +#if INTERNAL_NULLABLE_ATTRIBUTES + internal +#else + public +#endif + sealed class DisallowNullAttribute : Attribute + { } + + /// Specifies that an output may be null even if the corresponding type disallows it. + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, Inherited = false)] +#if INTERNAL_NULLABLE_ATTRIBUTES + internal +#else + public +#endif + sealed class MaybeNullAttribute : Attribute + { } + + /// Specifies that an output will not be null even if the corresponding type allows it. + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, Inherited = false)] +#if INTERNAL_NULLABLE_ATTRIBUTES + internal +#else + public +#endif + sealed class NotNullAttribute : Attribute + { } + + /// Specifies that when a method returns , the parameter may be null even if the corresponding type disallows it. + [AttributeUsage(AttributeTargets.Parameter, Inherited = false)] +#if INTERNAL_NULLABLE_ATTRIBUTES + internal +#else + public +#endif + sealed class MaybeNullWhenAttribute : Attribute + { + /// Initializes the attribute with the specified return value condition. + /// + /// The return value condition. If the method returns this value, the associated parameter may be null. + /// + public MaybeNullWhenAttribute(bool returnValue) => ReturnValue = returnValue; + + /// Gets the return value condition. + public bool ReturnValue { get; } + } + + /// Specifies that when a method returns , the parameter will not be null even if the corresponding type allows it. + [AttributeUsage(AttributeTargets.Parameter, Inherited = false)] +#if INTERNAL_NULLABLE_ATTRIBUTES + internal +#else + public +#endif + sealed class NotNullWhenAttribute : Attribute + { + /// Initializes the attribute with the specified return value condition. + /// + /// The return value condition. If the method returns this value, the associated parameter will not be null. + /// + public NotNullWhenAttribute(bool returnValue) => ReturnValue = returnValue; + + /// Gets the return value condition. + public bool ReturnValue { get; } + } + + /// Specifies that the output will be non-null if the named parameter is non-null. + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, AllowMultiple = true, Inherited = false)] +#if INTERNAL_NULLABLE_ATTRIBUTES + internal +#else + public +#endif + sealed class NotNullIfNotNullAttribute : Attribute + { + /// Initializes the attribute with the associated parameter name. + /// + /// The associated parameter name. The output will be non-null if the argument to the parameter specified is non-null. + /// + public NotNullIfNotNullAttribute(string parameterName) => ParameterName = parameterName; + + /// Gets the associated parameter name. + public string ParameterName { get; } + } + + /// Applied to a method that will never return under any circumstance. + [AttributeUsage(AttributeTargets.Method, Inherited = false)] +#if INTERNAL_NULLABLE_ATTRIBUTES + internal +#else + public +#endif + sealed class DoesNotReturnAttribute : Attribute + { } + + /// Specifies that the method will not return if the associated Boolean parameter is passed the specified value. + [AttributeUsage(AttributeTargets.Parameter, Inherited = false)] +#if INTERNAL_NULLABLE_ATTRIBUTES + internal +#else + public +#endif + sealed class DoesNotReturnIfAttribute : Attribute + { + /// Initializes the attribute with the specified parameter value. + /// + /// The condition parameter value. Code after the method will be considered unreachable by diagnostics if the argument to + /// the associated parameter matches this value. + /// + public DoesNotReturnIfAttribute(bool parameterValue) => ParameterValue = parameterValue; + + /// Gets the condition parameter value. + public bool ParameterValue { get; } + } +} +#endif diff --git a/test/Serilog.Sinks.File.Tests/RollingFileSinkTests.cs b/test/Serilog.Sinks.File.Tests/RollingFileSinkTests.cs index fb9c9bf..9232bde 100644 --- a/test/Serilog.Sinks.File.Tests/RollingFileSinkTests.cs +++ b/test/Serilog.Sinks.File.Tests/RollingFileSinkTests.cs @@ -241,7 +241,7 @@ public void IfTheLogFolderDoesNotExistItWillBeCreated() var folder = Path.Combine(temp, Guid.NewGuid().ToString()); var pathFormat = Path.Combine(folder, fileName); - Logger log = null; + Logger? log = null; try { @@ -270,7 +270,7 @@ static void TestRollingEventSequence(params LogEvent[] events) static void TestRollingEventSequence( Action configureFile, IEnumerable events, - Action> verifyWritten = null) + Action>? verifyWritten = null) { var fileName = Some.String() + "-.txt"; var folder = Some.TempFolderPath(); diff --git a/test/Serilog.Sinks.File.Tests/RollingIntervalExtensionsTests.cs b/test/Serilog.Sinks.File.Tests/RollingIntervalExtensionsTests.cs index 2d97d1b..404d5b4 100644 --- a/test/Serilog.Sinks.File.Tests/RollingIntervalExtensionsTests.cs +++ b/test/Serilog.Sinks.File.Tests/RollingIntervalExtensionsTests.cs @@ -5,19 +5,19 @@ namespace Serilog.Sinks.File.Tests { public class RollingIntervalExtensionsTests { - public static object[][] IntervalInstantCurrentNextCheckpoint => new[] + 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) } + 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] 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 fcf2880..90ef89d 100644 --- a/test/Serilog.Sinks.File.Tests/Serilog.Sinks.File.Tests.csproj +++ b/test/Serilog.Sinks.File.Tests/Serilog.Sinks.File.Tests.csproj @@ -1,15 +1,16 @@ - + - net47;netcoreapp3.1 + + net48;net5.0 + 8.0 + enable true Serilog.Sinks.File.Tests ../../assets/Serilog.snk true true true - $(PackageTargetFallback);dnxcore50;portable-net45+win8 - 1.0.4 @@ -17,16 +18,14 @@ - - + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + - - - - - diff --git a/test/Serilog.Sinks.File.Tests/Support/CaptureFilePathHook.cs b/test/Serilog.Sinks.File.Tests/Support/CaptureFilePathHook.cs index a116f95..65857d1 100644 --- a/test/Serilog.Sinks.File.Tests/Support/CaptureFilePathHook.cs +++ b/test/Serilog.Sinks.File.Tests/Support/CaptureFilePathHook.cs @@ -9,7 +9,7 @@ namespace Serilog.Sinks.File.Tests.Support /// class CaptureFilePathHook : FileLifecycleHooks { - public string Path { get; private set; } + public string? Path { get; private set; } public override Stream OnFileOpened(string path, Stream _, Encoding __) { diff --git a/test/Serilog.Sinks.File.Tests/Support/DelegatingSink.cs b/test/Serilog.Sinks.File.Tests/Support/DelegatingSink.cs index 9d81cc2..12b7f3d 100644 --- a/test/Serilog.Sinks.File.Tests/Support/DelegatingSink.cs +++ b/test/Serilog.Sinks.File.Tests/Support/DelegatingSink.cs @@ -21,13 +21,13 @@ public void Emit(LogEvent logEvent) public static LogEvent GetLogEvent(Action writeAction) { - LogEvent result = null; + LogEvent? result = null; var l = new LoggerConfiguration() .WriteTo.Sink(new DelegatingSink(le => result = le)) .CreateLogger(); writeAction(l); - return result; + return result!; } } } diff --git a/test/Serilog.Sinks.File.Tests/Support/Extensions.cs b/test/Serilog.Sinks.File.Tests/Support/Extensions.cs index f7fb775..a048353 100644 --- a/test/Serilog.Sinks.File.Tests/Support/Extensions.cs +++ b/test/Serilog.Sinks.File.Tests/Support/Extensions.cs @@ -17,7 +17,7 @@ public static List ReadAllLines(this Stream @this) using (var reader = new StreamReader(@this)) { - string line; + string? line; while ((line = reader.ReadLine()) != null) { lines.Add(line); diff --git a/test/Serilog.Sinks.File.Tests/Support/Some.cs b/test/Serilog.Sinks.File.Tests/Support/Some.cs index f0c7fd9..4209102 100644 --- a/test/Serilog.Sinks.File.Tests/Support/Some.cs +++ b/test/Serilog.Sinks.File.Tests/Support/Some.cs @@ -24,7 +24,7 @@ public static decimal Decimal() return Int() + 0.123m; } - public static string String(string tag = null) + public static string String(string? tag = null) { return (tag ?? "") + "__" + Int(); } diff --git a/test/Serilog.Sinks.File.Tests/Support/TempFolder.cs b/test/Serilog.Sinks.File.Tests/Support/TempFolder.cs index 7ff90f8..29682e0 100644 --- a/test/Serilog.Sinks.File.Tests/Support/TempFolder.cs +++ b/test/Serilog.Sinks.File.Tests/Support/TempFolder.cs @@ -11,7 +11,7 @@ class TempFolder : IDisposable readonly string _tempFolder; - public TempFolder(string name = null) + public TempFolder(string? name = null) { _tempFolder = System.IO.Path.Combine( Environment.GetEnvironmentVariable("TMP") ?? Environment.GetEnvironmentVariable("TMPDIR") ?? "/tmp", @@ -37,7 +37,7 @@ public void Dispose() } } - public static TempFolder ForCaller([CallerMemberName] string caller = null, [CallerFilePath] string sourceFileName = "") + public static TempFolder ForCaller([CallerMemberName] string? caller = null, [CallerFilePath] string sourceFileName = "") { if (caller == null) throw new ArgumentNullException(nameof(caller)); if (sourceFileName == null) throw new ArgumentNullException(nameof(sourceFileName)); @@ -47,7 +47,7 @@ public static TempFolder ForCaller([CallerMemberName] string caller = null, [Cal return new TempFolder(folderName); } - public string AllocateFilename(string ext = null) + public string AllocateFilename(string? ext = null) { return System.IO.Path.Combine(Path, Guid.NewGuid().ToString("n") + "." + (ext ?? "tmp")); } From edd4ba91fc3a99467076eb7598ac9d06112a6bbd Mon Sep 17 00:00:00 2001 From: Nicholas Blumhardt Date: Tue, 22 Jun 2021 12:31:30 +1000 Subject: [PATCH 25/29] Add net5.0 target; use (source-only) Nullable package instead of bundling down-level attributes (#228) --- serilog-sinks-file.sln | 1 - .../Serilog.Sinks.File.csproj | 9 +- .../Support/NullableAttributes..cs | 140 ------------------ 3 files changed, 5 insertions(+), 145 deletions(-) delete mode 100644 src/Serilog.Sinks.File/Support/NullableAttributes..cs diff --git a/serilog-sinks-file.sln b/serilog-sinks-file.sln index 9c33a2b..8d76bf7 100644 --- a/serilog-sinks-file.sln +++ b/serilog-sinks-file.sln @@ -11,7 +11,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "assets", "assets", "{E9D1B5 appveyor.yml = appveyor.yml Build.ps1 = Build.ps1 build.sh = build.sh - NuGet.Config = NuGet.Config README.md = README.md assets\Serilog.snk = assets\Serilog.snk EndProjectSection diff --git a/src/Serilog.Sinks.File/Serilog.Sinks.File.csproj b/src/Serilog.Sinks.File/Serilog.Sinks.File.csproj index 6db7104..4048eba 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. 5.0.0 Serilog Contributors - net45;netstandard1.3;netstandard2.0;netstandard2.1 + net45;netstandard1.3;netstandard2.0;netstandard2.1;net5.0 8.0 enable true @@ -14,7 +14,7 @@ serilog;file images\icon.png https://serilog.net/images/serilog-sink-nuget.png - http://serilog.net + https://serilog.net Apache-2.0 https://github.com/serilog/serilog-sinks-file git @@ -28,7 +28,8 @@ - + + @@ -42,5 +43,5 @@ - + diff --git a/src/Serilog.Sinks.File/Support/NullableAttributes..cs b/src/Serilog.Sinks.File/Support/NullableAttributes..cs deleted file mode 100644 index c7a2018..0000000 --- a/src/Serilog.Sinks.File/Support/NullableAttributes..cs +++ /dev/null @@ -1,140 +0,0 @@ -#pragma warning disable MA0048 // File name must match type name -#define INTERNAL_NULLABLE_ATTRIBUTES -#if NETSTANDARD1_0 || NETSTANDARD1_1 || NETSTANDARD1_2 || NETSTANDARD1_3 || NETSTANDARD1_4 || NETSTANDARD1_5 || NETSTANDARD1_6 || NETSTANDARD2_0 || NETCOREAPP1_0 || NETCOREAPP1_1 || NETCOREAPP2_0 || NETCOREAPP2_1 || NETCOREAPP2_2 || NET45 || NET451 || NET452 || NET46 || NET461 || NET462 || NET47 || NET471 || NET472 || NET48 - -// https://github.com/dotnet/corefx/blob/48363ac826ccf66fbe31a5dcb1dc2aab9a7dd768/src/Common/src/CoreLib/System/Diagnostics/CodeAnalysis/NullableAttributes.cs - -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -namespace System.Diagnostics.CodeAnalysis -{ - /// Specifies that null is allowed as an input even if the corresponding type disallows it. - [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property, Inherited = false)] -#if INTERNAL_NULLABLE_ATTRIBUTES - internal -#else - public -#endif - sealed class AllowNullAttribute : Attribute - { } - - /// Specifies that null is disallowed as an input even if the corresponding type allows it. - [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property, Inherited = false)] -#if INTERNAL_NULLABLE_ATTRIBUTES - internal -#else - public -#endif - sealed class DisallowNullAttribute : Attribute - { } - - /// Specifies that an output may be null even if the corresponding type disallows it. - [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, Inherited = false)] -#if INTERNAL_NULLABLE_ATTRIBUTES - internal -#else - public -#endif - sealed class MaybeNullAttribute : Attribute - { } - - /// Specifies that an output will not be null even if the corresponding type allows it. - [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, Inherited = false)] -#if INTERNAL_NULLABLE_ATTRIBUTES - internal -#else - public -#endif - sealed class NotNullAttribute : Attribute - { } - - /// Specifies that when a method returns , the parameter may be null even if the corresponding type disallows it. - [AttributeUsage(AttributeTargets.Parameter, Inherited = false)] -#if INTERNAL_NULLABLE_ATTRIBUTES - internal -#else - public -#endif - sealed class MaybeNullWhenAttribute : Attribute - { - /// Initializes the attribute with the specified return value condition. - /// - /// The return value condition. If the method returns this value, the associated parameter may be null. - /// - public MaybeNullWhenAttribute(bool returnValue) => ReturnValue = returnValue; - - /// Gets the return value condition. - public bool ReturnValue { get; } - } - - /// Specifies that when a method returns , the parameter will not be null even if the corresponding type allows it. - [AttributeUsage(AttributeTargets.Parameter, Inherited = false)] -#if INTERNAL_NULLABLE_ATTRIBUTES - internal -#else - public -#endif - sealed class NotNullWhenAttribute : Attribute - { - /// Initializes the attribute with the specified return value condition. - /// - /// The return value condition. If the method returns this value, the associated parameter will not be null. - /// - public NotNullWhenAttribute(bool returnValue) => ReturnValue = returnValue; - - /// Gets the return value condition. - public bool ReturnValue { get; } - } - - /// Specifies that the output will be non-null if the named parameter is non-null. - [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, AllowMultiple = true, Inherited = false)] -#if INTERNAL_NULLABLE_ATTRIBUTES - internal -#else - public -#endif - sealed class NotNullIfNotNullAttribute : Attribute - { - /// Initializes the attribute with the associated parameter name. - /// - /// The associated parameter name. The output will be non-null if the argument to the parameter specified is non-null. - /// - public NotNullIfNotNullAttribute(string parameterName) => ParameterName = parameterName; - - /// Gets the associated parameter name. - public string ParameterName { get; } - } - - /// Applied to a method that will never return under any circumstance. - [AttributeUsage(AttributeTargets.Method, Inherited = false)] -#if INTERNAL_NULLABLE_ATTRIBUTES - internal -#else - public -#endif - sealed class DoesNotReturnAttribute : Attribute - { } - - /// Specifies that the method will not return if the associated Boolean parameter is passed the specified value. - [AttributeUsage(AttributeTargets.Parameter, Inherited = false)] -#if INTERNAL_NULLABLE_ATTRIBUTES - internal -#else - public -#endif - sealed class DoesNotReturnIfAttribute : Attribute - { - /// Initializes the attribute with the specified parameter value. - /// - /// The condition parameter value. Code after the method will be considered unreachable by diagnostics if the argument to - /// the associated parameter matches this value. - /// - public DoesNotReturnIfAttribute(bool parameterValue) => ParameterValue = parameterValue; - - /// Gets the condition parameter value. - public bool ParameterValue { get; } - } -} -#endif From a2fa447a1db64abc7f2f87096bdb737c803d098b Mon Sep 17 00:00:00 2001 From: "C. Augusto Proiete" Date: Tue, 3 Nov 2020 21:24:41 -0400 Subject: [PATCH 26/29] Allow FileLifecycleHooks to change the length of the stream. Resolves #186 --- src/Serilog.Sinks.File/Sinks/File/FileSink.cs | 4 +- .../Sinks/File/WriteCountingStream.cs | 8 +- .../Serilog.Sinks.File.Tests/FileSinkTests.cs | 26 ++++++ .../Support/TruncateFileHook.cs | 18 ++++ .../WriteCountingStreamTests.cs | 83 +++++++++++++++++++ 5 files changed, 137 insertions(+), 2 deletions(-) create mode 100644 test/Serilog.Sinks.File.Tests/Support/TruncateFileHook.cs create mode 100644 test/Serilog.Sinks.File.Tests/WriteCountingStreamTests.cs diff --git a/src/Serilog.Sinks.File/Sinks/File/FileSink.cs b/src/Serilog.Sinks.File/Sinks/File/FileSink.cs index 76ada0b..611b45d 100644 --- a/src/Serilog.Sinks.File/Sinks/File/FileSink.cs +++ b/src/Serilog.Sinks.File/Sinks/File/FileSink.cs @@ -80,7 +80,9 @@ internal FileSink( Directory.CreateDirectory(directory); } - Stream outputStream = _underlyingStream = System.IO.File.Open(path, FileMode.Append, FileAccess.Write, FileShare.Read); + Stream outputStream = _underlyingStream = System.IO.File.Open(path, FileMode.OpenOrCreate, FileAccess.Write, FileShare.Read); + outputStream.Seek(0, SeekOrigin.End); + if (_fileSizeLimitBytes != null) { outputStream = _countingStreamWrapper = new WriteCountingStream(_underlyingStream); diff --git a/src/Serilog.Sinks.File/Sinks/File/WriteCountingStream.cs b/src/Serilog.Sinks.File/Sinks/File/WriteCountingStream.cs index fe0d5d3..e247144 100644 --- a/src/Serilog.Sinks.File/Sinks/File/WriteCountingStream.cs +++ b/src/Serilog.Sinks.File/Sinks/File/WriteCountingStream.cs @@ -63,7 +63,13 @@ public override long Seek(long offset, SeekOrigin origin) public override void SetLength(long value) { - throw new NotSupportedException(); + _stream.SetLength(value); + + if (value < CountedLength) + { + // File is now shorter and our position has changed to _stream.Length + CountedLength = _stream.Length; + } } public override int Read(byte[] buffer, int offset, int count) diff --git a/test/Serilog.Sinks.File.Tests/FileSinkTests.cs b/test/Serilog.Sinks.File.Tests/FileSinkTests.cs index 80c088f..a33261f 100644 --- a/test/Serilog.Sinks.File.Tests/FileSinkTests.cs +++ b/test/Serilog.Sinks.File.Tests/FileSinkTests.cs @@ -223,6 +223,32 @@ public static void OnOpenedLifecycleHookCanCaptureFilePath() } } + [Fact] + public static void OnOpenedLifecycleHookCanEmptyTheFileContents() + { + using (var tmp = TempFolder.ForCaller()) + { + var emptyFileHook = new TruncateFileHook(); + + var path = tmp.AllocateFilename("txt"); + using (var sink = new FileSink(path, new JsonFormatter(), fileSizeLimitBytes: null, encoding: new UTF8Encoding(false), buffered: false)) + { + sink.Emit(Some.LogEvent()); + } + + using (var sink = new FileSink(path, new JsonFormatter(), fileSizeLimitBytes: null, encoding: new UTF8Encoding(false), buffered: false, hooks: emptyFileHook)) + { + // Hook will clear the contents of the file before emitting the log events + sink.Emit(Some.LogEvent()); + } + + var lines = System.IO.File.ReadAllLines(path); + + Assert.Single(lines); + Assert.Equal('{', lines[0][0]); + } + } + static void WriteTwoEventsAndCheckOutputFileLength(long? maxBytes, Encoding encoding) { using (var tmp = TempFolder.ForCaller()) diff --git a/test/Serilog.Sinks.File.Tests/Support/TruncateFileHook.cs b/test/Serilog.Sinks.File.Tests/Support/TruncateFileHook.cs new file mode 100644 index 0000000..63f8497 --- /dev/null +++ b/test/Serilog.Sinks.File.Tests/Support/TruncateFileHook.cs @@ -0,0 +1,18 @@ +using System.IO; +using System.Text; + +namespace Serilog.Sinks.File.Tests.Support +{ + /// + /// + /// Demonstrates the use of , by emptying the file before it's written to + /// + public class TruncateFileHook : FileLifecycleHooks + { + public override Stream OnFileOpened(Stream underlyingStream, Encoding encoding) + { + underlyingStream.SetLength(0); + return base.OnFileOpened(underlyingStream, encoding); + } + } +} diff --git a/test/Serilog.Sinks.File.Tests/WriteCountingStreamTests.cs b/test/Serilog.Sinks.File.Tests/WriteCountingStreamTests.cs new file mode 100644 index 0000000..887ffe2 --- /dev/null +++ b/test/Serilog.Sinks.File.Tests/WriteCountingStreamTests.cs @@ -0,0 +1,83 @@ +using System.IO; +using System.Text; +using Serilog.Sinks.File.Tests.Support; +using Xunit; + +namespace Serilog.Sinks.File.Tests +{ + public class WriteCountingStreamTests + { + [Fact] + public void CountedLengthIsResetToStreamLengthIfNewSizeIsSmaller() + { + // If we counted 10 bytes written and SetLength was called with a smaller length (e.g. 5) + // we adjust the counter to the new byte count of the file to reflect reality + + using (var tmp = TempFolder.ForCaller()) + { + var path = tmp.AllocateFilename("txt"); + + long streamLengthAfterSetLength; + long countedLengthAfterSetLength; + + using (var fileStream = System.IO.File.Open(path, FileMode.Create, FileAccess.Write, FileShare.Read)) + using (var countingStream = new WriteCountingStream(fileStream)) + using (var writer = new StreamWriter(countingStream, new UTF8Encoding(false))) + { + writer.WriteLine("Hello, world!"); + writer.Flush(); + + countingStream.SetLength(5); + streamLengthAfterSetLength = countingStream.Length; + countedLengthAfterSetLength = countingStream.CountedLength; + } + + Assert.Equal(5, streamLengthAfterSetLength); + Assert.Equal(5, countedLengthAfterSetLength); + + var lines = System.IO.File.ReadAllLines(path); + + Assert.Single(lines); + Assert.Equal("Hello", lines[0]); + } + } + + [Fact] + public void CountedLengthRemainsTheSameIfNewSizeIsLarger() + { + // If we counted 10 bytes written and SetLength was called with a larger length (e.g. 100) + // we leave the counter intact because our position on the stream remains the same... The + // file just grew larger in size + + using (var tmp = TempFolder.ForCaller()) + { + var path = tmp.AllocateFilename("txt"); + + long streamLengthBeforeSetLength; + long streamLengthAfterSetLength; + long countedLengthAfterSetLength; + + using (var fileStream = System.IO.File.Open(path, FileMode.Create, FileAccess.Write, FileShare.Read)) + using (var countingStream = new WriteCountingStream(fileStream)) + using (var writer = new StreamWriter(countingStream, new UTF8Encoding(false))) + { + writer.WriteLine("Hello, world!"); + writer.Flush(); + + streamLengthBeforeSetLength = countingStream.CountedLength; + countingStream.SetLength(100); + streamLengthAfterSetLength = countingStream.Length; + countedLengthAfterSetLength = countingStream.CountedLength; + } + + Assert.Equal(100, streamLengthAfterSetLength); + Assert.Equal(streamLengthBeforeSetLength, countedLengthAfterSetLength); + + var lines = System.IO.File.ReadAllLines(path); + + Assert.Equal(2, lines.Length); + Assert.Equal("Hello, world!", lines[0]); + } + } + } +} From 7ca61434f8a3327f61703533c4976714e0202725 Mon Sep 17 00:00:00 2001 From: Nicholas Blumhardt Date: Tue, 22 Jun 2021 13:25:55 +1000 Subject: [PATCH 27/29] Builds from `main` --- Build.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Build.ps1 b/Build.ps1 index f7ed285..3c41f46 100644 --- a/Build.ps1 +++ b/Build.ps1 @@ -11,7 +11,7 @@ if(Test-Path .\artifacts) { $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.Substring(0, [math]::Min(10,$branch.Length)))-$revision"}[$branch -eq "master" -and $revision -ne "local"] +$suffix = @{ $true = ""; $false = "$($branch.Substring(0, [math]::Min(10,$branch.Length)))-$revision"}[$branch -eq "main" -and $revision -ne "local"] $commitHash = $(git rev-parse --short HEAD) $buildSuffix = @{ $true = "$($suffix)-$($commitHash)"; $false = "$($branch)-$($commitHash)" }[$suffix -ne ""] From 3d63fa7305c5367a1e0bbba3bc9154e5faae5c97 Mon Sep 17 00:00:00 2001 From: Nicholas Blumhardt Date: Tue, 22 Jun 2021 13:26:23 +1000 Subject: [PATCH 28/29] Publish from `main` --- appveyor.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index ad4973c..678cf8d 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -21,11 +21,11 @@ deploy: secure: rbdBqxBpLt4MkB+mrDOYNDOd8aVZ1zMkysaVNAXNKnC41FYifzX3l9LM8DCrUWU5 skip_symbols: true on: - branch: /^(master|dev)$/ + branch: /^(main|dev)$/ - provider: GitHub auth_token: secure: p4LpVhBKxGS5WqucHxFQ5c7C8cP74kbNB0Z8k9Oxx/PMaDQ1+ibmoexNqVU5ZlmX artifact: /Serilog.*\.nupkg/ tag: v$(appveyor_build_version) on: - branch: master + branch: main From e64e9a921442f61d697646f7a537cfbda65cd7d3 Mon Sep 17 00:00:00 2001 From: Nicholas Blumhardt Date: Wed, 23 Jun 2021 08:50:28 +1000 Subject: [PATCH 29/29] =?UTF-8?q?Add=20PrivateAssets=3DAll=20to=20Nullable?= =?UTF-8?q?=20dependency=20=F0=9F=98=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- 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 4048eba..73c5739 100644 --- a/src/Serilog.Sinks.File/Serilog.Sinks.File.csproj +++ b/src/Serilog.Sinks.File/Serilog.Sinks.File.csproj @@ -29,7 +29,7 @@ - + pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy

Alternative Proxy