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 ""]
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)._
diff --git a/appveyor.yml b/appveyor.yml
index 79ee987..678cf8d 100644
--- a/appveyor.yml
+++ b/appveyor.yml
@@ -1,9 +1,8 @@
version: '{build}'
skip_tags: true
image:
- - Visual Studio 2017
+ - Visual Studio 2019
- Ubuntu
-configuration: Release
build_script:
- ps: ./Build.ps1
for:
@@ -19,14 +18,14 @@ artifacts:
deploy:
- provider: NuGet
api_key:
- secure: N59tiJECUYpip6tEn0xvdmDAEiP9SIzyLEFLpwiigm/8WhJvBNs13QxzT1/3/JW/
+ 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
diff --git a/assets/serilog-sink-nuget.png b/assets/serilog-sink-nuget.png
new file mode 100644
index 0000000..a77d65c
Binary files /dev/null and b/assets/serilog-sink-nuget.png differ
diff --git a/example/Sample/Sample.csproj b/example/Sample/Sample.csproj
index ec04f95..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,13 +14,5 @@
-
-
-
-
-
-
-
-
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/serilog-sinks-file.sln.DotSettings b/serilog-sinks-file.sln.DotSettings
new file mode 100644
index 0000000..95887cc
--- /dev/null
+++ b/serilog-sinks-file.sln.DotSettings
@@ -0,0 +1,3 @@
+
+ True
+ True
\ No newline at end of file
diff --git a/src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs b/src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs
index 490803d..3518322 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;
@@ -32,7 +33,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}";
///
@@ -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.
@@ -233,23 +234,37 @@ 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.
+ /// 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,
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));
if (path == null) throw new ArgumentNullException(nameof(path));
@@ -258,7 +273,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 +282,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,22 +304,36 @@ 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.
+ /// 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,
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));
if (formatter == null) throw new ArgumentNullException(nameof(formatter));
@@ -312,7 +341,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);
}
///
@@ -329,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,
@@ -357,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,
@@ -367,7 +413,7 @@ public static LoggerConfiguration File(
{
return File(sinkConfiguration, formatter, path, restrictedToMinimumLevel, levelSwitch, null, null);
}
-
+
///
/// Write audit log events to the specified file.
///
@@ -383,15 +429,24 @@ 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,
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));
@@ -418,57 +473,68 @@ 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,
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));
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(
- 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));
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));
if (shared && hooks != null) throw new ArgumentException("File lifecycle hooks are not currently supported for shared log files.", nameof(hooks));
ILogEventSink sink;
- if (rollOnFileSizeLimit || rollingInterval != RollingInterval.Infinite)
- {
- sink = new RollingFileSink(path, formatter, fileSizeLimitBytes, retainedFileCountLimit, encoding, buffered, shared, rollingInterval, rollOnFileSizeLimit, hooks);
- }
- else
+ try
{
- try
+ if (rollOnFileSizeLimit || rollingInterval != RollingInterval.Infinite)
+ {
+ sink = new RollingFileSink(path, formatter, fileSizeLimitBytes, retainedFileCountLimit, encoding, buffered, shared, rollingInterval, rollOnFileSizeLimit, hooks, retainedFileTimeLimit);
+ }
+ else
{
if (shared)
{
@@ -480,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)
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 f8640f6..73c5739 100644
--- a/src/Serilog.Sinks.File/Serilog.Sinks.File.csproj
+++ b/src/Serilog.Sinks.File/Serilog.Sinks.File.csproj
@@ -2,69 +2,46 @@
Write Serilog events to text files in plain or JSON format.
- 4.1.0
+ 5.0.0
Serilog Contributors
- net45;netstandard1.3;netstandard2.0
+ net45;netstandard1.3;netstandard2.0;netstandard2.1;net5.0
+ 8.0
+ enable
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
- http://www.apache.org/licenses/LICENSE-2.0
+ images\icon.png
+ https://serilog.net/images/serilog-sink-nuget.png
+ https://serilog.net
+ Apache-2.0
https://github.com/serilog/serilog-sinks-file
git
- false
Serilog
true
- Serilog.Sinks.File
-
- true
-
+ true
false
true
$(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb
-
-
-
-
-
-
-
-
+
+
+
$(DefineConstants);ATOMIC_APPEND;HRESULTS
-
+
$(DefineConstants);OS_MUTEX
-
- $(DefineConstants);OS_MUTEX
-
-
-
-
-
-
-
-
-
-
+
+
-
-
-
-
-
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..cf27bfe
--- /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(string path, Stream underlyingStream, Encoding encoding)
+ {
+ var firstStreamResult = _first.OnFileOpened(path, underlyingStream, encoding);
+ var secondStreamResult = _second.OnFileOpened(path, 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..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
@@ -43,5 +58,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/FileSink.cs b/src/Serilog.Sinks.File/Sinks/File/FileSink.cs
index 8a913fa..611b45d 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.
@@ -44,9 +44,17 @@ 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)
+ public FileSink(string path, ITextFormatter textFormatter, long? fileSizeLimitBytes, Encoding? encoding = null, bool buffered = false)
: this(path, textFormatter, fileSizeLimitBytes, encoding, buffered, null)
{
}
@@ -56,12 +64,12 @@ 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 < 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;
@@ -72,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);
@@ -83,7 +93,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`.");
}
@@ -97,7 +107,7 @@ bool IFileSink.EmitOrOverflow(LogEvent logEvent)
{
if (_fileSizeLimitBytes != null)
{
- if (_countingStreamWrapper.CountedLength >= _fileSizeLimitBytes.Value)
+ if (_countingStreamWrapper!.CountedLength >= _fileSizeLimitBytes.Value)
return false;
}
@@ -113,6 +123,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/RollingFileSink.cs b/src/Serilog.Sinks.File/Sinks/File/RollingFileSink.cs
index 43e5fad..dccb802 100644
--- a/src/Serilog.Sinks.File/Sinks/File/RollingFileSink.cs
+++ b/src/Serilog.Sinks.File/Sinks/File/RollingFileSink.cs
@@ -29,37 +29,41 @@ sealed class RollingFileSink : ILogEventSink, IFlushableFileSink, IDisposable
readonly ITextFormatter _textFormatter;
readonly long? _fileSizeLimitBytes;
readonly int? _retainedFileCountLimit;
- readonly Encoding _encoding;
+ readonly TimeSpan? _retainedFileTimeLimit;
+ 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));
- 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 < 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));
_roller = new PathRoller(path, rollingInterval);
_textFormatter = textFormatter;
_fileSizeLimitBytes = fileSizeLimitBytes;
_retainedFileCountLimit = retainedFileCountLimit;
+ _retainedFileTimeLimit = retainedFileTimeLimit;
_encoding = encoding;
_buffered = buffered;
_shared = shared;
@@ -121,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) { }
@@ -166,32 +170,32 @@ 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) return;
+ if (_retainedFileCountLimit == null && _retainedFileTimeLimit == null) return;
var currentFileName = Path.GetFileName(currentFilePath);
// We consider the current file to exist, even if nothing's been written yet,
// because files are only opened on response to an event being processed.
var potentialMatches = Directory.GetFiles(_roller.LogFileDirectory, _roller.DirectorySearchPattern)
- .Select(Path.GetFileName)
- .Union(new [] { currentFileName });
+ .Select(f => Path.GetFileName(f))
+ .Union(new[] { currentFileName });
var newestFirst = _roller
.SelectMatches(potentialMatches)
.OrderByDescending(m => m.DateTime)
- .ThenByDescending(m => m.SequenceNumber)
- .Select(m => m.Filename);
+ .ThenByDescending(m => m.SequenceNumber);
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((f, i) => ShouldRetainFile(f, i, now))
+ .Select(x => x.Filename)
.ToList();
foreach (var obsolete in toRemove)
@@ -209,6 +213,20 @@ void ApplyRetentionPolicy(string currentFilePath)
}
}
+ bool ShouldRetainFile(RollingLogFile file, int index, DateTime now)
+ {
+ if (_retainedFileCountLimit.HasValue && index >= _retainedFileCountLimit.Value - 1)
+ return false;
+
+ if (_retainedFileTimeLimit.HasValue && file.DateTime.HasValue &&
+ file.DateTime.Value < now.Subtract(_retainedFileTimeLimit.Value))
+ {
+ return false;
+ }
+
+ return true;
+ }
+
public void Dispose()
{
lock (_syncRoot)
diff --git a/src/Serilog.Sinks.File/Sinks/File/SharedFileSink.AtomicAppend.cs b/src/Serilog.Sinks.File/Sinks/File/SharedFileSink.AtomicAppend.cs
index 866f807..6cf55cb 100644
--- a/src/Serilog.Sinks.File/Sinks/File/SharedFileSink.AtomicAppend.cs
+++ b/src/Serilog.Sinks.File/Sinks/File/SharedFileSink.AtomicAppend.cs
@@ -50,11 +50,18 @@ 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
///
- public SharedFileSink(string path, ITextFormatter textFormatter, long? fileSizeLimitBytes, Encoding encoding = 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 < 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));
@@ -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 41a19ef..b8a07db 100644
--- a/src/Serilog.Sinks.File/Sinks/File/SharedFileSink.OSMutex.cs
+++ b/src/Serilog.Sinks.File/Sinks/File/SharedFileSink.OSMutex.cs
@@ -49,12 +49,19 @@ 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
///
- public SharedFileSink(string path, ITextFormatter textFormatter, long? fileSizeLimitBytes, Encoding encoding = 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));
- 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;
@@ -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);
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 2d0f210..a33261f 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);
}
@@ -206,6 +206,49 @@ 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);
+ }
+ }
+
+ [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/RollingFileSinkTests.cs b/test/Serilog.Sinks.File.Tests/RollingFileSinkTests.cs
index 70408b3..9232bde 100644
--- a/test/Serilog.Sinks.File.Tests/RollingFileSinkTests.cs
+++ b/test/Serilog.Sinks.File.Tests/RollingFileSinkTests.cs
@@ -1,13 +1,13 @@
-using System;
+using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Linq;
-using System.Reflection;
using Xunit;
using Serilog.Events;
using Serilog.Sinks.File.Tests.Support;
using Serilog.Configuration;
+using Serilog.Core;
namespace Serilog.Sinks.File.Tests
{
@@ -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 WhenRetentionCountAndArchivingHookIsSetOldFilesAreCopiedAndOriginalDeleted()
{
@@ -85,10 +142,9 @@ 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)));
});
}
@@ -170,8 +226,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]);
}
}
}
@@ -185,7 +241,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
{
@@ -199,19 +255,11 @@ public void IfTheLogFolderDoesNotExistItWillBeCreated()
}
finally
{
- var disposable = (IDisposable)log;
- if (disposable != null) disposable.Dispose();
+ log?.Dispose();
Directory.Delete(temp, true);
}
}
- [Fact]
- public void AssemblyVersionIsFixedAt200()
- {
- var assembly = typeof(FileLoggerConfigurationExtensions).GetTypeInfo().Assembly;
- Assert.Equal("2.0.0.0", assembly.GetName().Version.ToString(4));
- }
-
static void TestRollingEventSequence(params LogEvent[] events)
{
TestRollingEventSequence(
@@ -222,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 3491e32..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,16 +1,16 @@
-
+
- net452;netcoreapp1.0;netcoreapp2.0
+
+ net48;net5.0
+ 8.0
+ enable
true
Serilog.Sinks.File.Tests
../../assets/Serilog.snk
true
true
- Serilog.Sinks.RollingFile.Tests
true
- $(PackageTargetFallback);dnxcore50;portable-net45+win8
- 1.0.4
@@ -18,19 +18,16 @@
-
-
-
-
-
-
-
-
+
+
+ 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
new file mode 100644
index 0000000..65857d1
--- /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, _, __);
+ }
+ }
+}
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 2d29d4d..4209102 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
@@ -23,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();
}
@@ -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/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"));
}
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/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);
}
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]);
+ }
+ }
+ }
+}