diff --git a/.editorconfig b/.editorconfig index 102e19f..a853ae9 100644 --- a/.editorconfig +++ b/.editorconfig @@ -5,6 +5,7 @@ trim_trailing_whitespace = true insert_final_newline = true indent_style = space indent_size = 4 +charset = utf-8 [*.{csproj,json,config,yml,props}] indent_size = 2 @@ -14,3 +15,8 @@ end_of_line = lf [*.{cmd, bat}] end_of_line = crlf + +# C# formatting settings - Namespace options +csharp_style_namespace_declarations = file_scoped:suggestion + +csharp_style_prefer_switch_expression = true:suggestion diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..7e44ae4 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,27 @@ +--- +name: Bug report +about: Report a bug and help us to improve Serilog.Sinks.File +title: '' +labels: bug +assignees: '' + +--- + +The Serilog maintainers want you to have a great experience using Serilog.Sinks.File, and will happily track down and resolve bugs. We all have limited time, though, so please think through all of the factors that might be involved and include as much useful information as possible 😊. + +ℹ If the problem is caused by a sink or other extension package, please track down the correct repository for that package and create the report there: this tracker is for the **Serilog.Sinks.File** package only. + +**Description** +What's going wrong? + +**Reproduction** +Please provide code samples showing how you're configuring and calling Serilog.Sinks.File to produce the behavior. + +**Expected behavior** +A concise description of what you expected to happen. + +**Relevant package, tooling and runtime versions** +What Serilog.Sinks.File version are you using, on what platform? + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..cb83e14 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,4 @@ +contact_links: + - name: Ask for help + url: https://stackoverflow.com/tags/serilog + about: Ask for help on how to use Serilog.Sinks.File diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..4319067 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an improvement to Serilog.Sinks.File +title: '' +labels: enhancement +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. For example, "I'd like to do _x_ but currently I can't because _y_ [...]". + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any workarounds or alternative solutions you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/Build.ps1 b/Build.ps1 index 3c41f46..06a36af 100644 --- a/Build.ps1 +++ b/Build.ps1 @@ -1,46 +1,42 @@ -echo "build: Build started" +Write-Output "build: Build started" Push-Location $PSScriptRoot if(Test-Path .\artifacts) { - echo "build: Cleaning .\artifacts" - Remove-Item .\artifacts -Force -Recurse + Write-Output "build: Cleaning ./artifacts" + Remove-Item ./artifacts -Force -Recurse } & dotnet restore --no-cache -$branch = @{ $true = $env:APPVEYOR_REPO_BRANCH; $false = $(git symbolic-ref --short -q HEAD) }[$env:APPVEYOR_REPO_BRANCH -ne $NULL]; -$revision = @{ $true = "{0:00000}" -f [convert]::ToInt32("0" + $env:APPVEYOR_BUILD_NUMBER, 10); $false = "local" }[$env:APPVEYOR_BUILD_NUMBER -ne $NULL]; +$branch = @{ $true = $env:APPVEYOR_REPO_BRANCH; $false = $(git symbolic-ref --short -q HEAD) }[$NULL -ne $env:APPVEYOR_REPO_BRANCH]; +$revision = @{ $true = "{0:00000}" -f [convert]::ToInt32("0" + $env:APPVEYOR_BUILD_NUMBER, 10); $false = "local" }[$NULL -ne $env:APPVEYOR_BUILD_NUMBER]; $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 ""] -echo "build: Package version suffix is $suffix" -echo "build: Build version suffix is $buildSuffix" +Write-Output "build: Package version suffix is $suffix" -foreach ($src in ls src/*) { +foreach ($src in Get-ChildItem src/*) { Push-Location $src - echo "build: Packaging project in $src" + Write-Output "build: Packaging project in $src" - & dotnet build -c Release --version-suffix=$buildSuffix -p:EnableSourceLink=true if ($suffix) { - & dotnet pack -c Release -o ..\..\artifacts --version-suffix=$suffix --no-build + & dotnet pack -c Release --include-source -o ../../artifacts --version-suffix=$suffix } else { - & dotnet pack -c Release -o ..\..\artifacts --no-build + & dotnet pack -c Release --include-source -o ../../artifacts } - if($LASTEXITCODE -ne 0) { exit 1 } + if($LASTEXITCODE -ne 0) { throw "Packaging failed" } Pop-Location } -foreach ($test in ls test/*.Tests) { +foreach ($test in Get-ChildItem test/*.Tests) { Push-Location $test - echo "build: Testing project in $test" + Write-Output "build: Testing project in $test" & dotnet test -c Release - if($LASTEXITCODE -ne 0) { exit 3 } + if($LASTEXITCODE -ne 0) { throw "Testing failed" } Pop-Location } diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 0000000..0248539 --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,17 @@ + + + latest + True + true + true + $(MSBuildThisFileDirectory)assets/Serilog.snk + enable + enable + latest + + + + + + + diff --git a/Directory.Build.targets b/Directory.Build.targets new file mode 100644 index 0000000..6132714 --- /dev/null +++ b/Directory.Build.targets @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/README.md b/README.md index 4bac086..1ac6ed7 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Serilog.Sinks.File [![Build status](https://ci.appveyor.com/api/projects/status/hh9gymy0n6tne46j?svg=true)](https://ci.appveyor.com/project/serilog/serilog-sinks-file) [![NuGet Version](http://img.shields.io/nuget/v/Serilog.Sinks.File.svg?style=flat)](https://www.nuget.org/packages/Serilog.Sinks.File/) [![Documentation](https://img.shields.io/badge/docs-wiki-yellow.svg)](https://github.com/serilog/serilog/wiki) [![Join the chat at https://gitter.im/serilog/serilog](https://img.shields.io/gitter/room/serilog/serilog.svg)](https://gitter.im/serilog/serilog) +# Serilog.Sinks.File [![Build status](https://ci.appveyor.com/api/projects/status/hh9gymy0n6tne46j/branch/dev?svg=true)](https://ci.appveyor.com/project/serilog/serilog-sinks-file/branch/dev) [![NuGet Version](http://img.shields.io/nuget/v/Serilog.Sinks.File.svg?style=flat)](https://www.nuget.org/packages/Serilog.Sinks.File/) [![Documentation](https://img.shields.io/badge/docs-wiki-yellow.svg)](https://github.com/serilog/serilog/wiki) Writes [Serilog](https://serilog.net) events to one or more text files. @@ -7,7 +7,7 @@ Writes [Serilog](https://serilog.net) events to one or more text files. Install the [Serilog.Sinks.File](https://www.nuget.org/packages/Serilog.Sinks.File/) package from NuGet: ```powershell -Install-Package Serilog.Sinks.File +dotnet add package Serilog.Sinks.File ``` To configure the sink in C# code, call `WriteTo.File()` during logger configuration: @@ -36,7 +36,7 @@ The limit can be changed or removed using the `fileSizeLimitBytes` parameter. ```csharp .WriteTo.File("log.txt", fileSizeLimitBytes: null) -``` +``` For the same reason, only **the most recent 31 files** are retained by default (i.e. one long month). To change or remove this limit, pass the `retainedFileCountLimit` parameter. diff --git a/appveyor.yml b/appveyor.yml index 678cf8d..42f5a75 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,10 +1,10 @@ version: '{build}' skip_tags: true image: - - Visual Studio 2019 - - Ubuntu + - Visual Studio 2022 + - Ubuntu2204 build_script: -- ps: ./Build.ps1 +- pwsh: ./Build.ps1 for: - matrix: @@ -15,17 +15,17 @@ for: test: off artifacts: - path: artifacts/Serilog.*.nupkg +- path: artifacts/Serilog.*.snupkg deploy: - provider: NuGet api_key: - secure: rbdBqxBpLt4MkB+mrDOYNDOd8aVZ1zMkysaVNAXNKnC41FYifzX3l9LM8DCrUWU5 - skip_symbols: true + secure: sDnchSg4TZIOK7oIUI6BJwFPNENTOZrGNsroGO1hehLJSvlHpFmpTwiX8+bgPD+Q on: branch: /^(main|dev)$/ - provider: GitHub auth_token: secure: p4LpVhBKxGS5WqucHxFQ5c7C8cP74kbNB0Z8k9Oxx/PMaDQ1+ibmoexNqVU5ZlmX - artifact: /Serilog.*\.nupkg/ + artifact: /Serilog.*(\.|\.s)nupkg/ tag: v$(appveyor_build_version) on: branch: main diff --git a/build.sh b/build.sh index 3bb5fec..cf241dc 100755 --- a/build.sh +++ b/build.sh @@ -1,6 +1,6 @@ -#!/bin/bash +#!/bin/bash -set -e +set -e dotnet --info dotnet --list-sdks dotnet restore diff --git a/example/Sample/Program.cs b/example/Sample/Program.cs index 89e6c95..660ca72 100644 --- a/example/Sample/Program.cs +++ b/example/Sample/Program.cs @@ -1,38 +1,27 @@ -using System; -using System.IO; using Serilog; using Serilog.Debugging; -namespace Sample -{ - public class Program - { - public static void Main(string[] args) - { - SelfLog.Enable(Console.Out); +SelfLog.Enable(Console.Out); - var sw = System.Diagnostics.Stopwatch.StartNew(); +var sw = System.Diagnostics.Stopwatch.StartNew(); - Log.Logger = new LoggerConfiguration() - .WriteTo.File("log.txt") - .CreateLogger(); +Log.Logger = new LoggerConfiguration() + .WriteTo.File("log.txt") + .CreateLogger(); - for (var i = 0; i < 1000000; ++i) - { - Log.Information("Hello, file logger!"); - } +for (var i = 0; i < 1000000; ++i) +{ + Log.Information("Hello, file logger!"); +} - Log.CloseAndFlush(); +Log.CloseAndFlush(); - sw.Stop(); +sw.Stop(); - Console.WriteLine($"Elapsed: {sw.ElapsedMilliseconds} ms"); - Console.WriteLine($"Size: {new FileInfo("log.txt").Length}"); +Console.WriteLine($"Elapsed: {sw.ElapsedMilliseconds} ms"); +Console.WriteLine($"Size: {new FileInfo("log.txt").Length}"); - Console.WriteLine("Press any key to delete the temporary log file..."); - Console.ReadKey(true); +Console.WriteLine("Press any key to delete the temporary log file..."); +Console.ReadKey(true); - File.Delete("log.txt"); - } - } -} +File.Delete("log.txt"); diff --git a/example/Sample/Sample.csproj b/example/Sample/Sample.csproj index 4915e19..fa9016e 100644 --- a/example/Sample/Sample.csproj +++ b/example/Sample/Sample.csproj @@ -1,12 +1,8 @@ - + - net48;net5.0 - 8.0 - enable - Sample + net48;net8.0 Exe - Sample true diff --git a/serilog-sinks-file.sln b/serilog-sinks-file.sln index 8d76bf7..b996c93 100644 --- a/serilog-sinks-file.sln +++ b/serilog-sinks-file.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.26730.15 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.33424.131 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{037440DE-440B-4129-9F7A-09B42D00397E}" EndProject @@ -11,6 +11,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "assets", "assets", "{E9D1B5 appveyor.yml = appveyor.yml Build.ps1 = Build.ps1 build.sh = build.sh + Directory.Build.props = Directory.Build.props + Directory.Build.targets = Directory.Build.targets README.md = README.md assets\Serilog.snk = assets\Serilog.snk EndProjectSection diff --git a/src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs b/src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs index 3518322..5440792 100644 --- a/src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs +++ b/src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs @@ -1,4 +1,4 @@ -// Copyright 2013-2017 Serilog Contributors +// Copyright 2013-2017 Serilog Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -12,9 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -using System; using System.ComponentModel; -using System.IO; using System.Text; using Serilog.Configuration; using Serilog.Core; @@ -27,546 +25,545 @@ // ReSharper disable RedundantArgumentDefaultValue, MethodOverloadWithOptionalParameter -namespace Serilog +namespace Serilog; + +/// Extends with methods to add file sinks. +public static class FileLoggerConfigurationExtensions { - /// Extends with methods to add file sinks. - public static class FileLoggerConfigurationExtensions - { - const int DefaultRetainedFileCountLimit = 31; // A long month of logs - const long DefaultFileSizeLimitBytes = 1L * 1024 * 1024 * 1024; // 1GB - const string DefaultOutputTemplate = "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception}"; + const int DefaultRetainedFileCountLimit = 31; // A long month of logs + 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}"; - /// - /// Write log events to the specified file. - /// - /// Logger sink configuration. - /// Path to the file. - /// The minimum level for - /// events passed through the sink. Ignored when is specified. - /// A switch allowing the pass-through minimum level - /// to be changed at runtime. - /// Supplies culture-specific formatting information, or null. - /// A message template describing the format used to write to the sink. - /// the default is "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception}". - /// The approximate maximum size, in bytes, to which a log file will be allowed to grow. - /// For unrestricted growth, pass null. The default is 1 GB. To avoid writing partial events, the last event within the limit - /// will be written in full even if it exceeds the limit. - /// Indicates if flushing to the output file can be buffered or not. The default - /// is false. - /// Allow the log file to be shared by multiple processes. The default is false. - /// If provided, a full disk flush will be performed periodically at the specified interval. - /// Configuration object allowing method chaining. - /// The file will be written using the UTF-8 character set. - [Obsolete("New code should not be compiled against this obsolete overload"), EditorBrowsable(EditorBrowsableState.Never)] - public static LoggerConfiguration File( - this LoggerSinkConfiguration sinkConfiguration, - string path, - LogEventLevel restrictedToMinimumLevel, - string outputTemplate, - IFormatProvider formatProvider, - long? fileSizeLimitBytes, - LoggingLevelSwitch levelSwitch, - bool buffered, - bool shared, - TimeSpan? flushToDiskInterval) - { - return File(sinkConfiguration, path, restrictedToMinimumLevel, outputTemplate, formatProvider, fileSizeLimitBytes, - levelSwitch, buffered, shared, flushToDiskInterval, RollingInterval.Infinite, false, null, null, null); - } + /// + /// Write log events to the specified file. + /// + /// Logger sink configuration. + /// Path to the file. + /// The minimum level for + /// events passed through the sink. Ignored when is specified. + /// A switch allowing the pass-through minimum level + /// to be changed at runtime. + /// Supplies culture-specific formatting information, or null. + /// A message template describing the format used to write to the sink. + /// the default is "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception}". + /// The approximate maximum size, in bytes, to which a log file will be allowed to grow. + /// For unrestricted growth, pass null. The default is 1 GB. To avoid writing partial events, the last event within the limit + /// will be written in full even if it exceeds the limit. + /// Indicates if flushing to the output file can be buffered or not. The default + /// is false. + /// Allow the log file to be shared by multiple processes. The default is false. + /// If provided, a full disk flush will be performed periodically at the specified interval. + /// Configuration object allowing method chaining. + /// The file will be written using the UTF-8 character set. + [Obsolete("New code should not be compiled against this obsolete overload"), EditorBrowsable(EditorBrowsableState.Never)] + public static LoggerConfiguration File( + this LoggerSinkConfiguration sinkConfiguration, + string path, + LogEventLevel restrictedToMinimumLevel, + string outputTemplate, + IFormatProvider formatProvider, + long? fileSizeLimitBytes, + LoggingLevelSwitch levelSwitch, + bool buffered, + bool shared, + TimeSpan? flushToDiskInterval) + { + return File(sinkConfiguration, path, restrictedToMinimumLevel, outputTemplate, formatProvider, fileSizeLimitBytes, + levelSwitch, buffered, shared, flushToDiskInterval, RollingInterval.Infinite, false, null, null, null); + } - /// - /// Write log events to the specified file. - /// - /// Logger sink configuration. - /// A formatter, such as , to convert the log events into - /// text for the file. If control of regular text formatting is required, use the other - /// overload of - /// and specify the outputTemplate parameter instead. - /// - /// Path to the file. - /// The minimum level for - /// events passed through the sink. Ignored when is specified. - /// A switch allowing the pass-through minimum level - /// to be changed at runtime. - /// The approximate maximum size, in bytes, to which a log file will be allowed to grow. - /// For unrestricted growth, pass null. The default is 1 GB. To avoid writing partial events, the last event within the limit - /// will be written in full even if it exceeds the limit. - /// Indicates if flushing to the output file can be buffered or not. The default - /// is false. - /// Allow the log file to be shared by multiple processes. The default is false. - /// If provided, a full disk flush will be performed periodically at the specified interval. - /// Configuration object allowing method chaining. - /// The file will be written using the UTF-8 character set. - [Obsolete("New code should not be compiled against this obsolete overload"), EditorBrowsable(EditorBrowsableState.Never)] - public static LoggerConfiguration File( - this LoggerSinkConfiguration sinkConfiguration, - ITextFormatter formatter, - string path, - LogEventLevel restrictedToMinimumLevel, - long? fileSizeLimitBytes, - LoggingLevelSwitch levelSwitch, - bool buffered, - bool shared, - TimeSpan? flushToDiskInterval) - { - return File(sinkConfiguration, formatter, path, restrictedToMinimumLevel, fileSizeLimitBytes, levelSwitch, - buffered, shared, flushToDiskInterval, RollingInterval.Infinite, false, null, null, null); - } + /// + /// Write log events to the specified file. + /// + /// Logger sink configuration. + /// A formatter, such as , to convert the log events into + /// text for the file. If control of regular text formatting is required, use the other + /// overload of + /// and specify the outputTemplate parameter instead. + /// + /// Path to the file. + /// The minimum level for + /// events passed through the sink. Ignored when is specified. + /// A switch allowing the pass-through minimum level + /// to be changed at runtime. + /// The approximate maximum size, in bytes, to which a log file will be allowed to grow. + /// For unrestricted growth, pass null. The default is 1 GB. To avoid writing partial events, the last event within the limit + /// will be written in full even if it exceeds the limit. + /// Indicates if flushing to the output file can be buffered or not. The default + /// is false. + /// Allow the log file to be shared by multiple processes. The default is false. + /// If provided, a full disk flush will be performed periodically at the specified interval. + /// Configuration object allowing method chaining. + /// The file will be written using the UTF-8 character set. + [Obsolete("New code should not be compiled against this obsolete overload"), EditorBrowsable(EditorBrowsableState.Never)] + public static LoggerConfiguration File( + this LoggerSinkConfiguration sinkConfiguration, + ITextFormatter formatter, + string path, + LogEventLevel restrictedToMinimumLevel, + long? fileSizeLimitBytes, + LoggingLevelSwitch levelSwitch, + bool buffered, + bool shared, + TimeSpan? flushToDiskInterval) + { + return File(sinkConfiguration, formatter, path, restrictedToMinimumLevel, fileSizeLimitBytes, levelSwitch, + buffered, shared, flushToDiskInterval, RollingInterval.Infinite, false, null, null, null); + } - /// - /// Write log events to the specified file. - /// - /// Logger sink configuration. - /// Path to the file. - /// The minimum level for - /// events passed through the sink. Ignored when is specified. - /// A switch allowing the pass-through minimum level - /// to be changed at runtime. - /// Supplies culture-specific formatting information, or null. - /// A message template describing the format used to write to the sink. - /// the default is "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception}". - /// The approximate maximum size, in bytes, to which a log file will be allowed to grow. - /// For unrestricted growth, pass null. The default is 1 GB. To avoid writing partial events, the last event within the limit - /// will be written in full even if it exceeds the limit. - /// Indicates if flushing to the output file can be buffered or not. The default - /// is false. - /// Allow the log file to be shared by multiple processes. The default is false. - /// If provided, a full disk flush will be performed periodically at the specified interval. - /// The interval at which logging will roll over to a new file. - /// If true, a new file will be created when the file size limit is reached. Filenames - /// will have a number appended in the format _NNN, with the first filename given no number. - /// The maximum number of log files that will be retained, - /// including the current log file. For unlimited retention, pass null. The default is 31. - /// Character encoding used to write the text file. The default is UTF-8 without BOM. - /// Configuration object allowing method chaining. - [Obsolete("New code should not be compiled against this obsolete overload"), EditorBrowsable(EditorBrowsableState.Never)] - public static LoggerConfiguration File( - this LoggerSinkConfiguration sinkConfiguration, - string path, - LogEventLevel restrictedToMinimumLevel, - string outputTemplate, - IFormatProvider formatProvider, - long? fileSizeLimitBytes, - LoggingLevelSwitch levelSwitch, - bool buffered, - bool shared, - TimeSpan? flushToDiskInterval, - RollingInterval rollingInterval, - bool rollOnFileSizeLimit, - int? retainedFileCountLimit, - Encoding encoding) - { - return File(sinkConfiguration, path, restrictedToMinimumLevel, outputTemplate, formatProvider, fileSizeLimitBytes, levelSwitch, buffered, - shared, flushToDiskInterval, rollingInterval, rollOnFileSizeLimit, retainedFileCountLimit, encoding, null); - } + /// + /// Write log events to the specified file. + /// + /// Logger sink configuration. + /// Path to the file. + /// The minimum level for + /// events passed through the sink. Ignored when is specified. + /// A switch allowing the pass-through minimum level + /// to be changed at runtime. + /// Supplies culture-specific formatting information, or null. + /// A message template describing the format used to write to the sink. + /// the default is "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception}". + /// The approximate maximum size, in bytes, to which a log file will be allowed to grow. + /// For unrestricted growth, pass null. The default is 1 GB. To avoid writing partial events, the last event within the limit + /// will be written in full even if it exceeds the limit. + /// Indicates if flushing to the output file can be buffered or not. The default + /// is false. + /// Allow the log file to be shared by multiple processes. The default is false. + /// If provided, a full disk flush will be performed periodically at the specified interval. + /// The interval at which logging will roll over to a new file. + /// If true, a new file will be created when the file size limit is reached. Filenames + /// will have a number appended in the format _NNN, with the first filename given no number. + /// The maximum number of log files that will be retained, + /// including the current log file. For unlimited retention, pass null. The default is 31. + /// Character encoding used to write the text file. The default is UTF-8 without BOM. + /// Configuration object allowing method chaining. + [Obsolete("New code should not be compiled against this obsolete overload"), EditorBrowsable(EditorBrowsableState.Never)] + public static LoggerConfiguration File( + this LoggerSinkConfiguration sinkConfiguration, + string path, + LogEventLevel restrictedToMinimumLevel, + string outputTemplate, + IFormatProvider formatProvider, + long? fileSizeLimitBytes, + LoggingLevelSwitch levelSwitch, + bool buffered, + bool shared, + TimeSpan? flushToDiskInterval, + RollingInterval rollingInterval, + bool rollOnFileSizeLimit, + int? retainedFileCountLimit, + Encoding encoding) + { + return File(sinkConfiguration, path, restrictedToMinimumLevel, outputTemplate, formatProvider, fileSizeLimitBytes, levelSwitch, buffered, + shared, flushToDiskInterval, rollingInterval, rollOnFileSizeLimit, retainedFileCountLimit, encoding, null); + } - /// - /// Write log events to the specified file. - /// - /// Logger sink configuration. - /// A formatter, such as , to convert the log events into - /// text for the file. If control of regular text formatting is required, use the other - /// overload of - /// and specify the outputTemplate parameter instead. - /// - /// Path to the file. - /// The minimum level for - /// events passed through the sink. Ignored when is specified. - /// A switch allowing the pass-through minimum level - /// to be changed at runtime. - /// The approximate maximum size, in bytes, to which a log file will be allowed to grow. - /// For unrestricted growth, pass null. The default is 1 GB. To avoid writing partial events, the last event within the limit - /// will be written in full even if it exceeds the limit. - /// Indicates if flushing to the output file can be buffered or not. The default - /// is false. - /// Allow the log file to be shared by multiple processes. The default is false. - /// If provided, a full disk flush will be performed periodically at the specified interval. - /// The interval at which logging will roll over to a new file. - /// If true, a new file will be created when the file size limit is reached. Filenames - /// will have a number appended in the format _NNN, with the first filename given no number. - /// The maximum number of log files that will be retained, - /// including the current log file. For unlimited retention, pass null. The default is 31. - /// Character encoding used to write the text file. The default is UTF-8 without BOM. - /// Configuration object allowing method chaining. - [Obsolete("New code should not be compiled against this obsolete overload"), EditorBrowsable(EditorBrowsableState.Never)] - public static LoggerConfiguration File( - this LoggerSinkConfiguration sinkConfiguration, - ITextFormatter formatter, - string path, - LogEventLevel restrictedToMinimumLevel, - long? fileSizeLimitBytes, - LoggingLevelSwitch levelSwitch, - bool buffered, - bool shared, - TimeSpan? flushToDiskInterval, - RollingInterval rollingInterval, - bool rollOnFileSizeLimit, - int? retainedFileCountLimit, - Encoding encoding) - { - return File(sinkConfiguration, formatter, path, restrictedToMinimumLevel, fileSizeLimitBytes, levelSwitch, buffered, - shared, flushToDiskInterval, rollingInterval, rollOnFileSizeLimit, retainedFileCountLimit, encoding, null); - } + /// + /// Write log events to the specified file. + /// + /// Logger sink configuration. + /// A formatter, such as , to convert the log events into + /// text for the file. If control of regular text formatting is required, use the other + /// overload of + /// and specify the outputTemplate parameter instead. + /// + /// Path to the file. + /// The minimum level for + /// events passed through the sink. Ignored when is specified. + /// A switch allowing the pass-through minimum level + /// to be changed at runtime. + /// The approximate maximum size, in bytes, to which a log file will be allowed to grow. + /// For unrestricted growth, pass null. The default is 1 GB. To avoid writing partial events, the last event within the limit + /// will be written in full even if it exceeds the limit. + /// Indicates if flushing to the output file can be buffered or not. The default + /// is false. + /// Allow the log file to be shared by multiple processes. The default is false. + /// If provided, a full disk flush will be performed periodically at the specified interval. + /// The interval at which logging will roll over to a new file. + /// If true, a new file will be created when the file size limit is reached. Filenames + /// will have a number appended in the format _NNN, with the first filename given no number. + /// The maximum number of log files that will be retained, + /// including the current log file. For unlimited retention, pass null. The default is 31. + /// Character encoding used to write the text file. The default is UTF-8 without BOM. + /// Configuration object allowing method chaining. + [Obsolete("New code should not be compiled against this obsolete overload"), EditorBrowsable(EditorBrowsableState.Never)] + public static LoggerConfiguration File( + this LoggerSinkConfiguration sinkConfiguration, + ITextFormatter formatter, + string path, + LogEventLevel restrictedToMinimumLevel, + long? fileSizeLimitBytes, + LoggingLevelSwitch levelSwitch, + bool buffered, + bool shared, + TimeSpan? flushToDiskInterval, + RollingInterval rollingInterval, + bool rollOnFileSizeLimit, + int? retainedFileCountLimit, + Encoding encoding) + { + return File(sinkConfiguration, formatter, path, restrictedToMinimumLevel, fileSizeLimitBytes, levelSwitch, buffered, + shared, flushToDiskInterval, rollingInterval, rollOnFileSizeLimit, retainedFileCountLimit, encoding, null); + } - /// - /// Write log events to the specified file. - /// - /// Logger sink configuration. - /// Path to the file. - /// The minimum level for - /// events passed through the sink. Ignored when is specified. - /// A switch allowing the pass-through minimum level - /// to be changed at runtime. - /// Supplies culture-specific formatting information, or null. - /// A message template describing the format used to write to the sink. - /// the default is "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception}". - /// The approximate maximum size, in bytes, to which a log file will be allowed to grow. - /// For unrestricted growth, pass null. The default is 1 GB. To avoid writing partial events, the last event within the limit - /// will be written in full even if it exceeds the limit. - /// Indicates if flushing to the output file can be buffered or not. The default - /// is false. - /// Allow the log file to be shared by multiple processes. The default is false. - /// If provided, a full disk flush will be performed periodically at the specified interval. - /// The interval at which logging will roll over to a new file. - /// If true, a new file will be created when the file size limit is reached. Filenames - /// will have a number appended in the format _NNN, with the first filename given no number. - /// The maximum number of log files that will be retained, - /// including the current log file. For unlimited retention, pass null. The default is 31. - /// Character encoding used to write the text file. The default is UTF-8 without BOM. - /// 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, - long? fileSizeLimitBytes = DefaultFileSizeLimitBytes, - 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, - TimeSpan? retainedFileTimeLimit = null) - { - if (sinkConfiguration == null) throw new ArgumentNullException(nameof(sinkConfiguration)); - if (path == null) throw new ArgumentNullException(nameof(path)); - if (outputTemplate == null) throw new ArgumentNullException(nameof(outputTemplate)); + /// + /// Write log events to the specified file. + /// + /// Logger sink configuration. + /// Path to the file. + /// The minimum level for + /// events passed through the sink. Ignored when is specified. + /// A switch allowing the pass-through minimum level + /// to be changed at runtime. + /// Supplies culture-specific formatting information, or null. + /// A message template describing the format used to write to the sink. + /// the default is "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception}". + /// The approximate maximum size, in bytes, to which a log file will be allowed to grow. + /// For unrestricted growth, pass null. The default is 1 GB. To avoid writing partial events, the last event within the limit + /// will be written in full even if it exceeds the limit. + /// Indicates if flushing to the output file can be buffered or not. The default + /// is false. + /// Allow the log file to be shared by multiple processes. The default is false. + /// If provided, a full disk flush will be performed periodically at the specified interval. + /// The interval at which logging will roll over to a new file. + /// If true, a new file will be created when the file size limit is reached. Filenames + /// will have a number appended in the format _NNN, with the first filename given no number. + /// The maximum number of log files that will be retained, + /// including the current log file. For unlimited retention, pass null. The default is 31. + /// Character encoding used to write the text file. The default is UTF-8 without BOM. + /// 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, + long? fileSizeLimitBytes = DefaultFileSizeLimitBytes, + 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, + TimeSpan? retainedFileTimeLimit = null) + { + if (sinkConfiguration == null) throw new ArgumentNullException(nameof(sinkConfiguration)); + if (path == null) throw new ArgumentNullException(nameof(path)); + if (outputTemplate == null) throw new ArgumentNullException(nameof(outputTemplate)); - var formatter = new MessageTemplateTextFormatter(outputTemplate, formatProvider); - return File(sinkConfiguration, formatter, path, restrictedToMinimumLevel, fileSizeLimitBytes, - levelSwitch, buffered, shared, flushToDiskInterval, - rollingInterval, rollOnFileSizeLimit, retainedFileCountLimit, encoding, hooks, retainedFileTimeLimit); - } + var formatter = new MessageTemplateTextFormatter(outputTemplate, formatProvider); + return File(sinkConfiguration, formatter, path, restrictedToMinimumLevel, fileSizeLimitBytes, + levelSwitch, buffered, shared, flushToDiskInterval, + rollingInterval, rollOnFileSizeLimit, retainedFileCountLimit, encoding, hooks, retainedFileTimeLimit); + } - /// - /// Write log events to the specified file. - /// - /// Logger sink configuration. - /// A formatter, such as , to convert the log events into - /// text for the file. If control of regular text formatting is required, use the other - /// overload of - /// and specify the outputTemplate parameter instead. - /// - /// Path to the file. - /// The minimum level for - /// events passed through the sink. Ignored when is specified. - /// A switch allowing the pass-through minimum level - /// to be changed at runtime. - /// The approximate maximum size, in bytes, to which a log file will be allowed to grow. - /// For unrestricted growth, pass null. The default is 1 GB. To avoid writing partial events, the last event within the limit - /// will be written in full even if it exceeds the limit. - /// Indicates if flushing to the output file can be buffered or not. The default - /// is false. - /// Allow the log file to be shared by multiple processes. The default is false. - /// If provided, a full disk flush will be performed periodically at the specified interval. - /// The interval at which logging will roll over to a new file. - /// If true, a new file will be created when the file size limit is reached. Filenames - /// will have a number appended in the format _NNN, with the first filename given no number. - /// The maximum number of log files that will be retained, - /// including the current log file. For unlimited retention, pass null. The default is 31. - /// Character encoding used to write the text file. The default is UTF-8 without BOM. - /// 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, - 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, - TimeSpan? retainedFileTimeLimit = 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)); + /// + /// Write log events to the specified file. + /// + /// Logger sink configuration. + /// A formatter, such as , to convert the log events into + /// text for the file. If control of regular text formatting is required, use the other + /// overload of + /// and specify the outputTemplate parameter instead. + /// + /// Path to the file. + /// The minimum level for + /// events passed through the sink. Ignored when is specified. + /// A switch allowing the pass-through minimum level + /// to be changed at runtime. + /// The approximate maximum size, in bytes, to which a log file will be allowed to grow. + /// For unrestricted growth, pass null. The default is 1 GB. To avoid writing partial events, the last event within the limit + /// will be written in full even if it exceeds the limit. + /// Indicates if flushing to the output file can be buffered or not. The default + /// is false. + /// Allow the log file to be shared by multiple processes. The default is false. + /// If provided, a full disk flush will be performed periodically at the specified interval. + /// The interval at which logging will roll over to a new file. + /// If true, a new file will be created when the file size limit is reached. Filenames + /// will have a number appended in the format _NNN, with the first filename given no number. + /// The maximum number of log files that will be retained, + /// including the current log file. For unlimited retention, pass null. The default is 31. + /// Character encoding used to write the text file. The default is UTF-8 without BOM. + /// 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, + 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, + TimeSpan? retainedFileTimeLimit = null) + { + if (sinkConfiguration == null) throw new ArgumentNullException(nameof(sinkConfiguration)); + if (formatter == null) throw new ArgumentNullException(nameof(formatter)); + if (path == null) throw new ArgumentNullException(nameof(path)); - return ConfigureFile(sinkConfiguration.Sink, formatter, path, restrictedToMinimumLevel, fileSizeLimitBytes, levelSwitch, - buffered, false, shared, flushToDiskInterval, encoding, rollingInterval, rollOnFileSizeLimit, - retainedFileCountLimit, hooks, retainedFileTimeLimit); - } + return ConfigureFile(sinkConfiguration.Sink, formatter, path, restrictedToMinimumLevel, fileSizeLimitBytes, levelSwitch, + buffered, false, shared, flushToDiskInterval, encoding, rollingInterval, rollOnFileSizeLimit, + retainedFileCountLimit, hooks, retainedFileTimeLimit); + } - /// - /// Write log events to the specified file. - /// - /// Logger sink configuration. - /// Path to the file. - /// The minimum level for - /// events passed through the sink. Ignored when is specified. - /// A switch allowing the pass-through minimum level - /// to be changed at runtime. - /// Supplies culture-specific formatting information, or null. - /// A message template describing the format used to write to the sink. - /// the default is "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception}". - /// 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, - string path, - LogEventLevel restrictedToMinimumLevel, - string outputTemplate, - IFormatProvider formatProvider, - LoggingLevelSwitch levelSwitch) - { - return File(sinkConfiguration, path, restrictedToMinimumLevel, outputTemplate, formatProvider, levelSwitch, null, null); - } + /// + /// Write log events to the specified file. + /// + /// Logger sink configuration. + /// Path to the file. + /// The minimum level for + /// events passed through the sink. Ignored when is specified. + /// A switch allowing the pass-through minimum level + /// to be changed at runtime. + /// Supplies culture-specific formatting information, or null. + /// A message template describing the format used to write to the sink. + /// the default is "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception}". + /// 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, + string path, + LogEventLevel restrictedToMinimumLevel, + string outputTemplate, + IFormatProvider formatProvider, + LoggingLevelSwitch levelSwitch) + { + return File(sinkConfiguration, path, restrictedToMinimumLevel, outputTemplate, formatProvider, levelSwitch, null, null); + } - /// - /// Write log events to the specified file. - /// - /// Logger sink configuration. - /// A formatter, such as , to convert the log events into - /// text for the file. If control of regular text formatting is required, use the other - /// overload of - /// and specify the outputTemplate parameter instead. - /// - /// Path to the file. - /// The minimum level for - /// events passed through the sink. Ignored when is specified. - /// A switch allowing the pass-through minimum level - /// to be changed at runtime. - /// 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, - ITextFormatter formatter, - string path, - LogEventLevel restrictedToMinimumLevel, - LoggingLevelSwitch levelSwitch) - { - return File(sinkConfiguration, formatter, path, restrictedToMinimumLevel, levelSwitch, null, null); - } + /// + /// Write log events to the specified file. + /// + /// Logger sink configuration. + /// A formatter, such as , to convert the log events into + /// text for the file. If control of regular text formatting is required, use the other + /// overload of + /// and specify the outputTemplate parameter instead. + /// + /// Path to the file. + /// The minimum level for + /// events passed through the sink. Ignored when is specified. + /// A switch allowing the pass-through minimum level + /// to be changed at runtime. + /// 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, + ITextFormatter formatter, + string path, + LogEventLevel restrictedToMinimumLevel, + LoggingLevelSwitch levelSwitch) + { + return File(sinkConfiguration, formatter, path, restrictedToMinimumLevel, levelSwitch, null, null); + } - /// - /// Write audit log events to the specified file. - /// - /// Logger sink configuration. - /// Path to the file. - /// The minimum level for - /// events passed through the sink. Ignored when is specified. - /// A switch allowing the pass-through minimum level - /// to be changed at runtime. - /// Supplies culture-specific formatting information, or null. - /// A message template describing the format used to write to the sink. - /// the default is "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception}". - /// Character encoding used to write the text file. The default is UTF-8 without BOM. - /// Optionally enables hooking into log file lifecycle events. - /// Configuration object allowing method chaining. - /// 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) - { - if (sinkConfiguration == null) throw new ArgumentNullException(nameof(sinkConfiguration)); - if (path == null) throw new ArgumentNullException(nameof(path)); - if (outputTemplate == null) throw new ArgumentNullException(nameof(outputTemplate)); + /// + /// Write audit log events to the specified file. + /// + /// Logger sink configuration. + /// Path to the file. + /// The minimum level for + /// events passed through the sink. Ignored when is specified. + /// A switch allowing the pass-through minimum level + /// to be changed at runtime. + /// Supplies culture-specific formatting information, or null. + /// A message template describing the format used to write to the sink. + /// the default is "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception}". + /// Character encoding used to write the text file. The default is UTF-8 without BOM. + /// Optionally enables hooking into log file lifecycle events. + /// Configuration object allowing method chaining. + /// 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) + { + if (sinkConfiguration == null) throw new ArgumentNullException(nameof(sinkConfiguration)); + if (path == null) throw new ArgumentNullException(nameof(path)); + if (outputTemplate == null) throw new ArgumentNullException(nameof(outputTemplate)); - var formatter = new MessageTemplateTextFormatter(outputTemplate, formatProvider); - return File(sinkConfiguration, formatter, path, restrictedToMinimumLevel, levelSwitch, encoding, hooks); - } + var formatter = new MessageTemplateTextFormatter(outputTemplate, formatProvider); + return File(sinkConfiguration, formatter, path, restrictedToMinimumLevel, levelSwitch, encoding, hooks); + } - /// - /// Write audit log events to the specified file. - /// - /// Logger sink configuration. - /// A formatter, such as , to convert the log events into - /// text for the file. If control of regular text formatting is required, use the other - /// overload of - /// and specify the outputTemplate parameter instead. - /// - /// Path to the file. - /// The minimum level for - /// events passed through the sink. Ignored when is specified. - /// A switch allowing the pass-through minimum level - /// to be changed at runtime. - /// 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) - { - if (sinkConfiguration == null) throw new ArgumentNullException(nameof(sinkConfiguration)); - if (formatter == null) throw new ArgumentNullException(nameof(formatter)); - if (path == null) throw new ArgumentNullException(nameof(path)); + /// + /// Write audit log events to the specified file. + /// + /// Logger sink configuration. + /// A formatter, such as , to convert the log events into + /// text for the file. If control of regular text formatting is required, use the other + /// overload of + /// and specify the outputTemplate parameter instead. + /// + /// Path to the file. + /// The minimum level for + /// events passed through the sink. Ignored when is specified. + /// A switch allowing the pass-through minimum level + /// to be changed at runtime. + /// 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) + { + 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, null); - } + return ConfigureFile(sinkConfiguration.Sink, formatter, path, restrictedToMinimumLevel, null, levelSwitch, false, true, + false, null, encoding, RollingInterval.Infinite, false, null, hooks, null); + } - static LoggerConfiguration ConfigureFile( - this Func addSink, - ITextFormatter formatter, - string path, - LogEventLevel restrictedToMinimumLevel, - long? fileSizeLimitBytes, - LoggingLevelSwitch? levelSwitch, - bool buffered, - bool propagateExceptions, - bool shared, - TimeSpan? flushToDiskInterval, - Encoding? encoding, - RollingInterval rollingInterval, - bool rollOnFileSizeLimit, - int? retainedFileCountLimit, - 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 < 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)); + static LoggerConfiguration ConfigureFile( + this Func addSink, + ITextFormatter formatter, + string path, + LogEventLevel restrictedToMinimumLevel, + long? fileSizeLimitBytes, + LoggingLevelSwitch? levelSwitch, + bool buffered, + bool propagateExceptions, + bool shared, + TimeSpan? flushToDiskInterval, + Encoding? encoding, + RollingInterval rollingInterval, + bool rollOnFileSizeLimit, + int? retainedFileCountLimit, + 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 is < 1) throw new ArgumentException("Invalid value provided; file size limit must be at least 1 byte, or null.", nameof(fileSizeLimitBytes)); + if (retainedFileCountLimit is < 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; + ILogEventSink sink; - try + try + { + if (rollOnFileSizeLimit || rollingInterval != RollingInterval.Infinite) + { + sink = new RollingFileSink(path, formatter, fileSizeLimitBytes, retainedFileCountLimit, encoding, buffered, shared, rollingInterval, rollOnFileSizeLimit, hooks, retainedFileTimeLimit); + } + else { - if (rollOnFileSizeLimit || rollingInterval != RollingInterval.Infinite) + if (shared) { - sink = new RollingFileSink(path, formatter, fileSizeLimitBytes, retainedFileCountLimit, encoding, buffered, shared, rollingInterval, rollOnFileSizeLimit, hooks, retainedFileTimeLimit); +#pragma warning disable 618 + sink = new SharedFileSink(path, formatter, fileSizeLimitBytes, encoding); +#pragma warning restore 618 } else { - if (shared) - { -#pragma warning disable 618 - sink = new SharedFileSink(path, formatter, fileSizeLimitBytes, encoding); -#pragma warning restore 618 - } - else - { - sink = new FileSink(path, formatter, fileSizeLimitBytes, encoding, buffered, hooks); - } - + 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) - { + if (flushToDiskInterval.HasValue) + { #pragma warning disable 618 - sink = new PeriodicFlushToDiskSink(sink, flushToDiskInterval.Value); + sink = new PeriodicFlushToDiskSink(sink, flushToDiskInterval.Value); #pragma warning restore 618 - } - - return addSink(sink, restrictedToMinimumLevel, levelSwitch); } + + return addSink(sink, restrictedToMinimumLevel, levelSwitch); } } diff --git a/src/Serilog.Sinks.File/Properties/AssemblyInfo.cs b/src/Serilog.Sinks.File/Properties/AssemblyInfo.cs index 93017cb..b431c64 100644 --- a/src/Serilog.Sinks.File/Properties/AssemblyInfo.cs +++ b/src/Serilog.Sinks.File/Properties/AssemblyInfo.cs @@ -1,6 +1,4 @@ -using System; -using System.Reflection; -using System.Runtime.CompilerServices; +using System.Runtime.CompilerServices; [assembly: CLSCompliant(true)] diff --git a/src/Serilog.Sinks.File/RollingInterval.cs b/src/Serilog.Sinks.File/RollingInterval.cs index 9fac848..55582ec 100644 --- a/src/Serilog.Sinks.File/RollingInterval.cs +++ b/src/Serilog.Sinks.File/RollingInterval.cs @@ -12,41 +12,40 @@ // See the License for the specific language governing permissions and // limitations under the License. -namespace Serilog +namespace Serilog; + +/// +/// Specifies the frequency at which the log file should roll. +/// +public enum RollingInterval { /// - /// Specifies the frequency at which the log file should roll. + /// The log file will never roll; no time period information will be appended to the log file name. /// - public enum RollingInterval - { - /// - /// The log file will never roll; no time period information will be appended to the log file name. - /// - Infinite, + Infinite, - /// - /// Roll every year. Filenames will have a four-digit year appended in the pattern yyyy. - /// - Year, + /// + /// Roll every year. Filenames will have a four-digit year appended in the pattern yyyy. + /// + Year, - /// - /// Roll every calendar month. Filenames will have yyyyMM appended. - /// - Month, + /// + /// Roll every calendar month. Filenames will have yyyyMM appended. + /// + Month, - /// - /// Roll every day. Filenames will have yyyyMMdd appended. - /// - Day, + /// + /// Roll every day. Filenames will have yyyyMMdd appended. + /// + Day, - /// - /// Roll every hour. Filenames will have yyyyMMddHH appended. - /// - Hour, + /// + /// Roll every hour. Filenames will have yyyyMMddHH appended. + /// + Hour, - /// - /// Roll every minute. Filenames will have yyyyMMddHHmm appended. - /// - Minute - } + /// + /// Roll every minute. Filenames will have yyyyMMddHHmm appended. + /// + Minute } diff --git a/src/Serilog.Sinks.File/Serilog.Sinks.File.csproj b/src/Serilog.Sinks.File/Serilog.Sinks.File.csproj index 73c5739..a7629b8 100644 --- a/src/Serilog.Sinks.File/Serilog.Sinks.File.csproj +++ b/src/Serilog.Sinks.File/Serilog.Sinks.File.csproj @@ -2,46 +2,53 @@ Write Serilog events to text files in plain or JSON format. - 5.0.0 + 6.0.0 Serilog Contributors - net45;netstandard1.3;netstandard2.0;netstandard2.1;net5.0 - 8.0 - enable + + net471;net462 + + $(TargetFrameworks);net8.0;net6.0;netstandard2.0 true - ../../assets/Serilog.snk - true - true serilog;file - images\icon.png + serilog-sink-nuget.png https://serilog.net/images/serilog-sink-nuget.png - https://serilog.net + https://github.com/serilog/serilog-sinks-file Apache-2.0 https://github.com/serilog/serilog-sinks-file git Serilog - true - true - false true - $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb + True + snupkg + README.md - - - + + - + $(DefineConstants);ATOMIC_APPEND;HRESULTS - + $(DefineConstants);OS_MUTEX + + $(DefineConstants);ENUMERABLE_MAXBY + + + + $(DefineConstants);ENUMERABLE_MAXBY + + - + + diff --git a/src/Serilog.Sinks.File/Sinks/File/Clock.cs b/src/Serilog.Sinks.File/Sinks/File/Clock.cs index b7cf3cc..7624fb4 100644 --- a/src/Serilog.Sinks.File/Sinks/File/Clock.cs +++ b/src/Serilog.Sinks.File/Sinks/File/Clock.cs @@ -1,4 +1,4 @@ -// Copyright 2013-2016 Serilog Contributors +// Copyright 2013-2016 Serilog Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -12,27 +12,24 @@ // See the License for the specific language governing permissions and // limitations under the License. -using System; +namespace Serilog.Sinks.File; -namespace Serilog.Sinks.File +static class Clock { - static class Clock - { - static Func _dateTimeNow = () => DateTime.Now; + static Func _dateTimeNow = () => DateTime.Now; - [ThreadStatic] - static DateTime _testDateTimeNow; + [ThreadStatic] + static DateTime _testDateTimeNow; - public static DateTime DateTimeNow => _dateTimeNow(); + public static DateTime DateTimeNow => _dateTimeNow(); - // Time is set per thread to support parallel - // If any thread uses the clock in test mode, all threads - // must use it in test mode; once set to test mode only - // terminating the application returns it to normal use. - public static void SetTestDateTimeNow(DateTime now) - { - _testDateTimeNow = now; - _dateTimeNow = () => _testDateTimeNow; - } + // Time is set per thread to support parallel + // If any thread uses the clock in test mode, all threads + // must use it in test mode; once set to test mode only + // terminating the application returns it to normal use. + public static void SetTestDateTimeNow(DateTime now) + { + _testDateTimeNow = now; + _dateTimeNow = () => _testDateTimeNow; } } diff --git a/src/Serilog.Sinks.File/Sinks/File/FileLifeCycleHookChain.cs b/src/Serilog.Sinks.File/Sinks/File/FileLifeCycleHookChain.cs index cf27bfe..b7eb06e 100644 --- a/src/Serilog.Sinks.File/Sinks/File/FileLifeCycleHookChain.cs +++ b/src/Serilog.Sinks.File/Sinks/File/FileLifeCycleHookChain.cs @@ -12,35 +12,32 @@ // 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 +namespace Serilog.Sinks.File; + +sealed class FileLifeCycleHookChain : FileLifecycleHooks { - class FileLifeCycleHookChain : FileLifecycleHooks - { - private readonly FileLifecycleHooks _first; - private readonly FileLifecycleHooks _second; + readonly FileLifecycleHooks _first; + readonly FileLifecycleHooks _second; - public FileLifeCycleHookChain(FileLifecycleHooks first, FileLifecycleHooks second) - { - _first = first ?? throw new ArgumentNullException(nameof(first)); - _second = second ?? throw new ArgumentNullException(nameof(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); + 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; - } + return secondStreamResult; + } - public override void OnFileDeleting(string path) - { - _first.OnFileDeleting(path); - _second.OnFileDeleting(path); - } + 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 26fd1e2..d83926a 100644 --- a/src/Serilog.Sinks.File/Sinks/File/FileLifecycleHooks.cs +++ b/src/Serilog.Sinks.File/Sinks/File/FileLifecycleHooks.cs @@ -12,68 +12,67 @@ // See the License for the specific language governing permissions and // limitations under the License. -using System.IO; using System.Text; +// ReSharper disable UnusedMember.Global -namespace Serilog.Sinks.File +namespace Serilog.Sinks.File; + +/// +/// Enables hooking into log file lifecycle events. +/// Hooks run synchronously and therefore may affect responsiveness of the application if long operations are performed. +/// +public abstract class FileLifecycleHooks { /// - /// Enables hooking into log file lifecycle events. - /// Hooks run synchronously and therefore may affect responsiveness of the application if long operations are performed. + /// 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. /// - 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); + /// + /// 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 - /// 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 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(Stream underlyingStream, Encoding encoding) => underlyingStream; + /// + /// 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 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(Stream underlyingStream, Encoding encoding) => underlyingStream; - /// - /// Called before an obsolete (rolling) log file is deleted. - /// This can be used to copy old logs to an archive location or send to a backup server. - /// - /// The full path to the file being deleted. - public virtual void OnFileDeleting(string path) {} + /// + /// Called before an obsolete (rolling) log file is deleted. + /// This can be used to copy old logs to an archive location or send to a backup server. + /// + /// 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); - } + /// + /// 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 611b45d..a246b66 100644 --- a/src/Serilog.Sinks.File/Sinks/File/FileSink.cs +++ b/src/Serilog.Sinks.File/Sinks/File/FileSink.cs @@ -12,140 +12,137 @@ // See the License for the specific language governing permissions and // limitations under the License. -using System; -using System.IO; using System.Text; using Serilog.Events; using Serilog.Formatting; -namespace Serilog.Sinks.File +namespace Serilog.Sinks.File; + +/// +/// Write log events to a disk file. +/// +public sealed class FileSink : IFileSink, IDisposable { - /// - /// Write log events to a disk file. - /// - public sealed class FileSink : IFileSink, IDisposable + readonly TextWriter _output; + readonly FileStream _underlyingStream; + readonly ITextFormatter _textFormatter; + readonly long? _fileSizeLimitBytes; + readonly bool _buffered; + readonly object _syncRoot = new(); + readonly WriteCountingStream? _countingStreamWrapper; + + /// Construct a . + /// Path to the file. + /// Formatter used to convert log events to text. + /// The approximate maximum size, in bytes, to which a log file will be allowed to grow. + /// For unrestricted growth, pass null. The default is 1 GB. To avoid writing partial events, the last event within the limit + /// will be written in full even if it exceeds the limit. + /// Character encoding used to write the text file. The default is UTF-8 without BOM. + /// Indicates if flushing to the output file can be buffered or not. The default + /// 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) { - readonly TextWriter _output; - readonly FileStream _underlyingStream; - readonly ITextFormatter _textFormatter; - readonly long? _fileSizeLimitBytes; - readonly bool _buffered; - readonly object _syncRoot = new object(); - readonly WriteCountingStream? _countingStreamWrapper; - - /// Construct a . - /// Path to the file. - /// Formatter used to convert log events to text. - /// The approximate maximum size, in bytes, to which a log file will be allowed to grow. - /// For unrestricted growth, pass null. The default is 1 GB. To avoid writing partial events, the last event within the limit - /// will be written in full even if it exceeds the limit. - /// Character encoding used to write the text file. The default is UTF-8 without BOM. - /// Indicates if flushing to the output file can be buffered or not. The default - /// is false. - /// Configuration object allowing method chaining. - /// 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) - { - } + } - // This overload should be used internally; the overload above maintains compatibility with the earlier public API. - internal FileSink( - string path, - ITextFormatter textFormatter, - long? fileSizeLimitBytes, - Encoding? encoding, - bool buffered, - FileLifecycleHooks? hooks) + // This overload should be used internally; the overload above maintains compatibility with the earlier public API. + internal FileSink( + string path, + ITextFormatter textFormatter, + long? fileSizeLimitBytes, + Encoding? encoding, + bool buffered, + FileLifecycleHooks? hooks) + { + if (path == null) throw new ArgumentNullException(nameof(path)); + if (fileSizeLimitBytes is < 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; + + var directory = Path.GetDirectoryName(path); + if (!string.IsNullOrWhiteSpace(directory) && !Directory.Exists(directory)) { - 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."); - _textFormatter = textFormatter ?? throw new ArgumentNullException(nameof(textFormatter)); - _fileSizeLimitBytes = fileSizeLimitBytes; - _buffered = buffered; - - var directory = Path.GetDirectoryName(path); - if (!string.IsNullOrWhiteSpace(directory) && !Directory.Exists(directory)) - { - Directory.CreateDirectory(directory); - } + Directory.CreateDirectory(directory); + } - Stream outputStream = _underlyingStream = System.IO.File.Open(path, FileMode.OpenOrCreate, FileAccess.Write, FileShare.Read); - outputStream.Seek(0, SeekOrigin.End); + 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); - } - - // Parameter reassignment. - encoding = encoding ?? new UTF8Encoding(encoderShouldEmitUTF8Identifier: false); + if (_fileSizeLimitBytes != null) + { + outputStream = _countingStreamWrapper = new WriteCountingStream(_underlyingStream); + } - if (hooks != null) - { - outputStream = hooks.OnFileOpened(path, outputStream, encoding) ?? - throw new InvalidOperationException($"The file lifecycle hook `{nameof(FileLifecycleHooks.OnFileOpened)}(...)` returned `null`."); - } + // Parameter reassignment. + encoding ??= new UTF8Encoding(encoderShouldEmitUTF8Identifier: false); - _output = new StreamWriter(outputStream, encoding); + if (hooks != null) + { + outputStream = hooks.OnFileOpened(path, outputStream, encoding) ?? + throw new InvalidOperationException($"The file lifecycle hook `{nameof(FileLifecycleHooks.OnFileOpened)}(...)` returned `null`."); } - bool IFileSink.EmitOrOverflow(LogEvent logEvent) + _output = new StreamWriter(outputStream, encoding); + } + + bool IFileSink.EmitOrOverflow(LogEvent logEvent) + { + if (logEvent == null) throw new ArgumentNullException(nameof(logEvent)); + lock (_syncRoot) { - if (logEvent == null) throw new ArgumentNullException(nameof(logEvent)); - lock (_syncRoot) + if (_fileSizeLimitBytes != null) { - if (_fileSizeLimitBytes != null) - { - if (_countingStreamWrapper!.CountedLength >= _fileSizeLimitBytes.Value) - return false; - } + if (_countingStreamWrapper!.CountedLength >= _fileSizeLimitBytes.Value) + return false; + } - _textFormatter.Format(logEvent, _output); - if (!_buffered) - _output.Flush(); + _textFormatter.Format(logEvent, _output); + if (!_buffered) + _output.Flush(); - return true; - } + return true; } + } - /// - /// 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); - } + /// + /// 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); + } - /// - public void Dispose() + /// + public void Dispose() + { + lock (_syncRoot) { - lock (_syncRoot) - { - _output.Dispose(); - } + _output.Dispose(); } + } - /// - public void FlushToDisk() + /// + public void FlushToDisk() + { + lock (_syncRoot) { - lock (_syncRoot) - { - _output.Flush(); - _underlyingStream.Flush(true); - } + _output.Flush(); + _underlyingStream.Flush(true); } } } diff --git a/src/Serilog.Sinks.File/Sinks/File/IFileSink.cs b/src/Serilog.Sinks.File/Sinks/File/IFileSink.cs index 89268ab..f1e2d7e 100644 --- a/src/Serilog.Sinks.File/Sinks/File/IFileSink.cs +++ b/src/Serilog.Sinks.File/Sinks/File/IFileSink.cs @@ -15,15 +15,14 @@ using Serilog.Core; using Serilog.Events; -namespace Serilog.Sinks.File +namespace Serilog.Sinks.File; + +/// +/// Exists only for the convenience of , which +/// switches implementations based on sharing. Would refactor, but preserving +/// backwards compatibility. +/// +interface IFileSink : ILogEventSink, IFlushableFileSink { - /// - /// Exists only for the convenience of , which - /// switches implementations based on sharing. Would refactor, but preserving - /// backwards compatibility. - /// - interface IFileSink : ILogEventSink, IFlushableFileSink - { - bool EmitOrOverflow(LogEvent logEvent); - } + bool EmitOrOverflow(LogEvent logEvent); } diff --git a/src/Serilog.Sinks.File/Sinks/File/IFlushableFileSink.cs b/src/Serilog.Sinks.File/Sinks/File/IFlushableFileSink.cs index 75d6e52..ad9e407 100644 --- a/src/Serilog.Sinks.File/Sinks/File/IFlushableFileSink.cs +++ b/src/Serilog.Sinks.File/Sinks/File/IFlushableFileSink.cs @@ -12,16 +12,15 @@ // See the License for the specific language governing permissions and // limitations under the License. -namespace Serilog.Sinks.File +namespace Serilog.Sinks.File; + +/// +/// Supported by (file-based) sinks that can be explicitly flushed. +/// +public interface IFlushableFileSink { /// - /// Supported by (file-based) sinks that can be explicitly flushed. + /// Flush buffered contents to disk. /// - public interface IFlushableFileSink - { - /// - /// Flush buffered contents to disk. - /// - void FlushToDisk(); - } + void FlushToDisk(); } diff --git a/src/Serilog.Sinks.File/Sinks/File/IOErrors.cs b/src/Serilog.Sinks.File/Sinks/File/IOErrors.cs index 36fe8bc..faa121c 100644 --- a/src/Serilog.Sinks.File/Sinks/File/IOErrors.cs +++ b/src/Serilog.Sinks.File/Sinks/File/IOErrors.cs @@ -12,20 +12,17 @@ // See the License for the specific language governing permissions and // limitations under the License. -using System.IO; +namespace Serilog.Sinks.File; -namespace Serilog.Sinks.File +static class IOErrors { - static class IOErrors + public static bool IsLockedFile(IOException ex) { - public static bool IsLockedFile(IOException ex) - { #if HRESULTS - var errorCode = System.Runtime.InteropServices.Marshal.GetHRForException(ex) & ((1 << 16) - 1); - return errorCode == 32 || errorCode == 33; + var errorCode = System.Runtime.InteropServices.Marshal.GetHRForException(ex) & ((1 << 16) - 1); + return errorCode == 32 || errorCode == 33; #else - return true; + return true; #endif - } } } diff --git a/src/Serilog.Sinks.File/Sinks/File/NullSink.cs b/src/Serilog.Sinks.File/Sinks/File/NullSink.cs index 5e9bb2c..386bab0 100644 --- a/src/Serilog.Sinks.File/Sinks/File/NullSink.cs +++ b/src/Serilog.Sinks.File/Sinks/File/NullSink.cs @@ -15,16 +15,15 @@ using Serilog.Core; using Serilog.Events; -namespace Serilog.Sinks.File +namespace Serilog.Sinks.File; + +/// +/// An instance of this sink may be substituted when an instance of the +/// is unable to be constructed. +/// +sealed class NullSink : ILogEventSink { - /// - /// An instance of this sink may be substituted when an instance of the - /// is unable to be constructed. - /// - class NullSink : ILogEventSink + public void Emit(LogEvent logEvent) { - public void Emit(LogEvent logEvent) - { - } } -} \ No newline at end of file +} diff --git a/src/Serilog.Sinks.File/Sinks/File/PathRoller.cs b/src/Serilog.Sinks.File/Sinks/File/PathRoller.cs index 79a6915..e6773eb 100644 --- a/src/Serilog.Sinks.File/Sinks/File/PathRoller.cs +++ b/src/Serilog.Sinks.File/Sinks/File/PathRoller.cs @@ -12,106 +12,102 @@ // See the License for the specific language governing permissions and // limitations under the License. -using System; -using System.Collections.Generic; using System.Globalization; -using System.IO; using System.Text.RegularExpressions; -namespace Serilog.Sinks.File +namespace Serilog.Sinks.File; + +sealed class PathRoller { - class PathRoller - { - const string PeriodMatchGroup = "period"; - const string SequenceNumberMatchGroup = "sequence"; + const string PeriodMatchGroup = "period"; + const string SequenceNumberMatchGroup = "sequence"; - readonly string _directory; - readonly string _filenamePrefix; - readonly string _filenameSuffix; - readonly Regex _filenameMatcher; + readonly string _directory; + readonly string _filenamePrefix; + readonly string _filenameSuffix; + readonly Regex _filenameMatcher; - readonly RollingInterval _interval; - readonly string _periodFormat; + readonly RollingInterval _interval; + readonly string _periodFormat; - public PathRoller(string path, RollingInterval interval) - { - if (path == null) throw new ArgumentNullException(nameof(path)); - _interval = interval; - _periodFormat = interval.GetFormat(); - - var pathDirectory = Path.GetDirectoryName(path); - if (string.IsNullOrEmpty(pathDirectory)) - pathDirectory = Directory.GetCurrentDirectory(); - - _directory = Path.GetFullPath(pathDirectory); - _filenamePrefix = Path.GetFileNameWithoutExtension(path); - _filenameSuffix = Path.GetExtension(path); - _filenameMatcher = new Regex( - "^" + - Regex.Escape(_filenamePrefix) + - "(?<" + PeriodMatchGroup + ">\\d{" + _periodFormat.Length + "})" + - "(?<" + SequenceNumberMatchGroup + ">_[0-9]{3,}){0,1}" + - Regex.Escape(_filenameSuffix) + - "$", - RegexOptions.Compiled); - - DirectorySearchPattern = $"{_filenamePrefix}*{_filenameSuffix}"; - } + public PathRoller(string path, RollingInterval interval) + { + if (path == null) throw new ArgumentNullException(nameof(path)); + _interval = interval; + _periodFormat = interval.GetFormat(); + + var pathDirectory = Path.GetDirectoryName(path); + if (string.IsNullOrEmpty(pathDirectory)) + pathDirectory = Directory.GetCurrentDirectory(); + + _directory = Path.GetFullPath(pathDirectory); + _filenamePrefix = Path.GetFileNameWithoutExtension(path); + _filenameSuffix = Path.GetExtension(path); + _filenameMatcher = new Regex( + "^" + + Regex.Escape(_filenamePrefix) + + "(?<" + PeriodMatchGroup + ">\\d{" + _periodFormat.Length + "})" + + "(?<" + SequenceNumberMatchGroup + ">_[0-9]{3,}){0,1}" + + Regex.Escape(_filenameSuffix) + + "$", + RegexOptions.Compiled); + + DirectorySearchPattern = $"{_filenamePrefix}*{_filenameSuffix}"; + } - public string LogFileDirectory => _directory; + public string LogFileDirectory => _directory; - public string DirectorySearchPattern { get; } + public string DirectorySearchPattern { get; } - public void GetLogFilePath(DateTime date, int? sequenceNumber, out string path) - { - var currentCheckpoint = GetCurrentCheckpoint(date); + public void GetLogFilePath(DateTime date, int? sequenceNumber, out string path) + { + var currentCheckpoint = GetCurrentCheckpoint(date); - var tok = currentCheckpoint?.ToString(_periodFormat, CultureInfo.InvariantCulture) ?? ""; + var tok = currentCheckpoint?.ToString(_periodFormat, CultureInfo.InvariantCulture) ?? ""; - if (sequenceNumber != null) - tok += "_" + sequenceNumber.Value.ToString("000", CultureInfo.InvariantCulture); + if (sequenceNumber != null) + tok += "_" + sequenceNumber.Value.ToString("000", CultureInfo.InvariantCulture); - path = Path.Combine(_directory, _filenamePrefix + tok + _filenameSuffix); - } + path = Path.Combine(_directory, _filenamePrefix + tok + _filenameSuffix); + } - public IEnumerable SelectMatches(IEnumerable filenames) + public IEnumerable SelectMatches(IEnumerable filenames) + { + foreach (var filename in filenames) { - foreach (var filename in filenames) - { - var match = _filenameMatcher.Match(filename); - if (!match.Success) - continue; + var match = _filenameMatcher.Match(filename); + if (!match.Success) + continue; - int? inc = null; - var incGroup = match.Groups[SequenceNumberMatchGroup]; - if (incGroup.Captures.Count != 0) - { - var incPart = incGroup.Captures[0].Value.Substring(1); - inc = int.Parse(incPart, CultureInfo.InvariantCulture); - } + int? inc = null; + var incGroup = match.Groups[SequenceNumberMatchGroup]; + if (incGroup.Captures.Count != 0) + { + var incPart = incGroup.Captures[0].Value.Substring(1); + inc = int.Parse(incPart, CultureInfo.InvariantCulture); + } - DateTime? period = null; - var periodGroup = match.Groups[PeriodMatchGroup]; - if (periodGroup.Captures.Count != 0) + DateTime? period = null; + var periodGroup = match.Groups[PeriodMatchGroup]; + if (periodGroup.Captures.Count != 0) + { + var dateTimePart = periodGroup.Captures[0].Value; + if (DateTime.TryParseExact( + dateTimePart, + _periodFormat, + CultureInfo.InvariantCulture, + DateTimeStyles.None, + out var dateTime)) { - var dateTimePart = periodGroup.Captures[0].Value; - if (DateTime.TryParseExact( - dateTimePart, - _periodFormat, - CultureInfo.InvariantCulture, - DateTimeStyles.None, - out var dateTime)) - { - period = dateTime; - } + period = dateTime; } - - yield return new RollingLogFile(filename, period, inc); } + + yield return new RollingLogFile(filename, period, inc); } + } - public DateTime? GetCurrentCheckpoint(DateTime instant) => _interval.GetCurrentCheckpoint(instant); + public DateTime? GetCurrentCheckpoint(DateTime instant) => _interval.GetCurrentCheckpoint(instant); - public DateTime? GetNextCheckpoint(DateTime instant) => _interval.GetNextCheckpoint(instant); - } + public DateTime? GetNextCheckpoint(DateTime instant) => _interval.GetNextCheckpoint(instant); } diff --git a/src/Serilog.Sinks.File/Sinks/File/PeriodicFlushToDiskSink.cs b/src/Serilog.Sinks.File/Sinks/File/PeriodicFlushToDiskSink.cs index 1b931c9..c4272d9 100644 --- a/src/Serilog.Sinks.File/Sinks/File/PeriodicFlushToDiskSink.cs +++ b/src/Serilog.Sinks.File/Sinks/File/PeriodicFlushToDiskSink.cs @@ -12,75 +12,72 @@ // See the License for the specific language governing permissions and // limitations under the License. -using System; -using System.Threading; using Serilog.Core; using Serilog.Debugging; using Serilog.Events; -namespace Serilog.Sinks.File +namespace Serilog.Sinks.File; + +/// +/// A sink wrapper that periodically flushes the wrapped sink to disk. +/// +[Obsolete("This type will be removed from the public API in a future version; use `WriteTo.File(flushToDiskInterval:)` instead.")] +public class PeriodicFlushToDiskSink : ILogEventSink, IDisposable { + readonly ILogEventSink _sink; + readonly Timer _timer; + int _flushRequired; + /// - /// A sink wrapper that periodically flushes the wrapped sink to disk. + /// Construct a that wraps + /// and flushes it at the specified . /// - [Obsolete("This type will be removed from the public API in a future version; use `WriteTo.File(flushToDiskInterval:)` instead.")] - 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) { - readonly ILogEventSink _sink; - readonly Timer _timer; - int _flushRequired; + _sink = sink ?? throw new ArgumentNullException(nameof(sink)); - /// - /// Construct a that wraps - /// and flushes it at the specified . - /// - /// The sink to wrap. - /// The interval at which to flush the underlying sink. - /// When is null - public PeriodicFlushToDiskSink(ILogEventSink sink, TimeSpan flushInterval) + if (sink is IFlushableFileSink flushable) { - _sink = sink ?? throw new ArgumentNullException(nameof(sink)); - - if (sink is IFlushableFileSink flushable) - { - _timer = new Timer(_ => FlushToDisk(flushable), null, flushInterval, flushInterval); - } - else - { - _timer = new Timer(_ => { }, null, Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan); - SelfLog.WriteLine("{0} configured to flush {1}, but {2} not implemented", typeof(PeriodicFlushToDiskSink), sink, nameof(IFlushableFileSink)); - } + _timer = new Timer(_ => FlushToDisk(flushable), null, flushInterval, flushInterval); } - - /// - public void Emit(LogEvent logEvent) + else { - _sink.Emit(logEvent); - Interlocked.Exchange(ref _flushRequired, 1); + _timer = new Timer(_ => { }, null, Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan); + SelfLog.WriteLine("{0} configured to flush {1}, but {2} not implemented", typeof(PeriodicFlushToDiskSink), sink, nameof(IFlushableFileSink)); } + } - /// - public void Dispose() - { - _timer.Dispose(); - (_sink as IDisposable)?.Dispose(); - } + /// + public void Emit(LogEvent logEvent) + { + _sink.Emit(logEvent); + Interlocked.Exchange(ref _flushRequired, 1); + } + + /// + public void Dispose() + { + _timer.Dispose(); + (_sink as IDisposable)?.Dispose(); + } - void FlushToDisk(IFlushableFileSink flushable) + void FlushToDisk(IFlushableFileSink flushable) + { + try { - try - { - if (Interlocked.CompareExchange(ref _flushRequired, 0, 1) == 1) - { - // May throw ObjectDisposedException, since we're not trying to synchronize - // anything here in the wrapper. - flushable.FlushToDisk(); - } - } - catch (Exception ex) + if (Interlocked.CompareExchange(ref _flushRequired, 0, 1) == 1) { - SelfLog.WriteLine("{0} could not flush the underlying sink to disk: {1}", typeof(PeriodicFlushToDiskSink), ex); + // May throw ObjectDisposedException, since we're not trying to synchronize + // anything here in the wrapper. + flushable.FlushToDisk(); } } + catch (Exception ex) + { + SelfLog.WriteLine("{0} could not flush the underlying sink to disk: {1}", typeof(PeriodicFlushToDiskSink), ex); + } } } diff --git a/src/Serilog.Sinks.File/Sinks/File/RollingFileSink.cs b/src/Serilog.Sinks.File/Sinks/File/RollingFileSink.cs index dccb802..6c55d44 100644 --- a/src/Serilog.Sinks.File/Sinks/File/RollingFileSink.cs +++ b/src/Serilog.Sinks.File/Sinks/File/RollingFileSink.cs @@ -1,4 +1,4 @@ -// Copyright 2013-2017 Serilog Contributors +// Copyright 2013-2017 Serilog Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -12,248 +12,250 @@ // See the License for the specific language governing permissions and // limitations under the License. -using System; -using System.IO; -using System.Linq; using System.Text; using Serilog.Core; using Serilog.Debugging; using Serilog.Events; using Serilog.Formatting; -namespace Serilog.Sinks.File +namespace Serilog.Sinks.File; + +sealed class RollingFileSink : ILogEventSink, IFlushableFileSink, IDisposable { - sealed class RollingFileSink : ILogEventSink, IFlushableFileSink, IDisposable + readonly PathRoller _roller; + readonly ITextFormatter _textFormatter; + readonly long? _fileSizeLimitBytes; + readonly int? _retainedFileCountLimit; + readonly TimeSpan? _retainedFileTimeLimit; + readonly Encoding? _encoding; + readonly bool _buffered; + readonly bool _shared; + readonly bool _rollOnFileSizeLimit; + readonly FileLifecycleHooks? _hooks; + + readonly object _syncRoot = new(); + bool _isDisposed; + DateTime? _nextCheckpoint; + IFileSink? _currentFile; + int? _currentFileSequence; + + public RollingFileSink(string path, + ITextFormatter textFormatter, + long? fileSizeLimitBytes, + int? retainedFileCountLimit, + Encoding? encoding, + bool buffered, + bool shared, + RollingInterval rollingInterval, + bool rollOnFileSizeLimit, + FileLifecycleHooks? hooks, + TimeSpan? retainedFileTimeLimit) { - readonly PathRoller _roller; - readonly ITextFormatter _textFormatter; - readonly long? _fileSizeLimitBytes; - readonly int? _retainedFileCountLimit; - readonly TimeSpan? _retainedFileTimeLimit; - readonly Encoding? _encoding; - readonly bool _buffered; - readonly bool _shared; - readonly bool _rollOnFileSizeLimit; - readonly FileLifecycleHooks? _hooks; - - readonly object _syncRoot = new object(); - bool _isDisposed; - DateTime? _nextCheckpoint; - IFileSink? _currentFile; - int? _currentFileSequence; - - public RollingFileSink(string path, - ITextFormatter textFormatter, - long? fileSizeLimitBytes, - int? retainedFileCountLimit, - Encoding? encoding, - bool buffered, - bool shared, - RollingInterval rollingInterval, - bool rollOnFileSizeLimit, - FileLifecycleHooks? hooks, - TimeSpan? retainedFileTimeLimit) - { - 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."); - 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; - _rollOnFileSizeLimit = rollOnFileSizeLimit; - _hooks = hooks; - } + if (path == null) throw new ArgumentNullException(nameof(path)); + if (fileSizeLimitBytes is < 1) throw new ArgumentException("Invalid value provided; file size limit must be at least 1 byte, or null."); + if (retainedFileCountLimit is < 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; + _rollOnFileSizeLimit = rollOnFileSizeLimit; + _hooks = hooks; + } - public void Emit(LogEvent logEvent) - { - if (logEvent == null) throw new ArgumentNullException(nameof(logEvent)); + public void Emit(LogEvent logEvent) + { + if (logEvent == null) throw new ArgumentNullException(nameof(logEvent)); - lock (_syncRoot) - { - if (_isDisposed) throw new ObjectDisposedException("The log file has been disposed."); + lock (_syncRoot) + { + if (_isDisposed) throw new ObjectDisposedException("The log file has been disposed."); - var now = Clock.DateTimeNow; - AlignCurrentFileTo(now); + var now = Clock.DateTimeNow; + AlignCurrentFileTo(now); - while (_currentFile?.EmitOrOverflow(logEvent) == false && _rollOnFileSizeLimit) - { - AlignCurrentFileTo(now, nextSequence: true); - } + while (_currentFile?.EmitOrOverflow(logEvent) == false && _rollOnFileSizeLimit) + { + AlignCurrentFileTo(now, nextSequence: true); } } + } - void AlignCurrentFileTo(DateTime now, bool nextSequence = false) + void AlignCurrentFileTo(DateTime now, bool nextSequence = false) + { + if (!_nextCheckpoint.HasValue) + { + OpenFile(now); + } + else if (nextSequence || now >= _nextCheckpoint.Value) { - if (!_nextCheckpoint.HasValue) + int? minSequence = null; + if (nextSequence) { - OpenFile(now); + if (_currentFileSequence == null) + minSequence = 1; + else + minSequence = _currentFileSequence.Value + 1; } - else if (nextSequence || now >= _nextCheckpoint.Value) - { - int? minSequence = null; - if (nextSequence) - { - if (_currentFileSequence == null) - minSequence = 1; - else - minSequence = _currentFileSequence.Value + 1; - } - CloseFile(); - OpenFile(now, minSequence); - } + CloseFile(); + OpenFile(now, minSequence); } + } - void OpenFile(DateTime now, int? minSequence = null) - { - var currentCheckpoint = _roller.GetCurrentCheckpoint(now); + void OpenFile(DateTime now, int? minSequence = null) + { + var currentCheckpoint = _roller.GetCurrentCheckpoint(now); - // We only try periodically because repeated failures - // to open log files REALLY slow an app down. - _nextCheckpoint = _roller.GetNextCheckpoint(now) ?? now.AddMinutes(30); + // We only try periodically because repeated failures + // to open log files REALLY slow an app down. + _nextCheckpoint = _roller.GetNextCheckpoint(now) ?? now.AddMinutes(30); - var existingFiles = Enumerable.Empty(); - try + var existingFiles = Enumerable.Empty(); + try + { + if (Directory.Exists(_roller.LogFileDirectory)) { - if (Directory.Exists(_roller.LogFileDirectory)) - { - existingFiles = Directory.GetFiles(_roller.LogFileDirectory, _roller.DirectorySearchPattern) - .Select(f => Path.GetFileName(f)); - } + // ReSharper disable once ConvertClosureToMethodGroup + existingFiles = Directory.GetFiles(_roller.LogFileDirectory, _roller.DirectorySearchPattern) + .Select(f => Path.GetFileName(f)); } - catch (DirectoryNotFoundException) { } - - var latestForThisCheckpoint = _roller - .SelectMatches(existingFiles) - .Where(m => m.DateTime == currentCheckpoint) - .OrderByDescending(m => m.SequenceNumber) - .FirstOrDefault(); + } + catch (DirectoryNotFoundException) { } + + var latestForThisCheckpoint = _roller + .SelectMatches(existingFiles) + .Where(m => m.DateTime == currentCheckpoint) +#if ENUMERABLE_MAXBY + .MaxBy(m => m.SequenceNumber); +#else + .OrderByDescending(m => m.SequenceNumber) + .FirstOrDefault(); +#endif + + var sequence = latestForThisCheckpoint?.SequenceNumber; + if (minSequence != null) + { + if (sequence == null || sequence.Value < minSequence.Value) + sequence = minSequence; + } - var sequence = latestForThisCheckpoint?.SequenceNumber; - if (minSequence != null) - { - if (sequence == null || sequence.Value < minSequence.Value) - sequence = minSequence; - } + const int maxAttempts = 3; + for (var attempt = 0; attempt < maxAttempts; attempt++) + { + _roller.GetLogFilePath(now, sequence, out var path); - const int maxAttempts = 3; - for (var attempt = 0; attempt < maxAttempts; attempt++) + try { - _roller.GetLogFilePath(now, sequence, out var path); - - try - { - _currentFile = _shared ? + _currentFile = _shared ? #pragma warning disable 618 - (IFileSink)new SharedFileSink(path, _textFormatter, _fileSizeLimitBytes, _encoding) : + new SharedFileSink(path, _textFormatter, _fileSizeLimitBytes, _encoding) : #pragma warning restore 618 - new FileSink(path, _textFormatter, _fileSizeLimitBytes, _encoding, _buffered, _hooks); + new FileSink(path, _textFormatter, _fileSizeLimitBytes, _encoding, _buffered, _hooks); - _currentFileSequence = sequence; - } - catch (IOException ex) + _currentFileSequence = sequence; + } + catch (IOException ex) + { + if (IOErrors.IsLockedFile(ex)) { - if (IOErrors.IsLockedFile(ex)) - { - SelfLog.WriteLine("File target {0} was locked, attempting to open next in sequence (attempt {1})", path, attempt + 1); - sequence = (sequence ?? 0) + 1; - continue; - } - - throw; + SelfLog.WriteLine("File target {0} was locked, attempting to open next in sequence (attempt {1})", path, attempt + 1); + sequence = (sequence ?? 0) + 1; + continue; } - ApplyRetentionPolicy(path, now); - return; + throw; } + + ApplyRetentionPolicy(path, now); + return; } + } - void ApplyRetentionPolicy(string currentFilePath, DateTime now) - { - if (_retainedFileCountLimit == null && _retainedFileTimeLimit == null) return; + void ApplyRetentionPolicy(string currentFilePath, DateTime now) + { + if (_retainedFileCountLimit == null && _retainedFileTimeLimit == null) return; - var currentFileName = Path.GetFileName(currentFilePath); + 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(f => Path.GetFileName(f)) - .Union(new[] { currentFileName }); + // 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. + // ReSharper disable once ConvertClosureToMethodGroup + var potentialMatches = Directory.GetFiles(_roller.LogFileDirectory, _roller.DirectorySearchPattern) + .Select(f => Path.GetFileName(f)) + .Union(new[] { currentFileName }); - var newestFirst = _roller - .SelectMatches(potentialMatches) - .OrderByDescending(m => m.DateTime) - .ThenByDescending(m => m.SequenceNumber); + var newestFirst = _roller + .SelectMatches(potentialMatches) + .OrderByDescending(m => m.DateTime) + .ThenByDescending(m => m.SequenceNumber); - var toRemove = newestFirst - .Where(n => StringComparer.OrdinalIgnoreCase.Compare(currentFileName, n.Filename) != 0) - .SkipWhile((f, i) => ShouldRetainFile(f, i, now)) - .Select(x => x.Filename) - .ToList(); + var toRemove = newestFirst + .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) + foreach (var obsolete in toRemove) + { + var fullPath = Path.Combine(_roller.LogFileDirectory, obsolete); + try { - var fullPath = Path.Combine(_roller.LogFileDirectory, obsolete); - try - { - _hooks?.OnFileDeleting(fullPath); - System.IO.File.Delete(fullPath); - } - catch (Exception ex) - { - SelfLog.WriteLine("Error {0} while processing obsolete log file {1}", ex, fullPath); - } + _hooks?.OnFileDeleting(fullPath); + System.IO.File.Delete(fullPath); } - } - - 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)) + catch (Exception ex) { - return false; + SelfLog.WriteLine("Error {0} while processing obsolete log file {1}", ex, fullPath); } - - return true; } + } - public void Dispose() + 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)) { - lock (_syncRoot) - { - if (_currentFile == null) return; - CloseFile(); - _isDisposed = true; - } + return false; } - void CloseFile() + return true; + } + + public void Dispose() + { + lock (_syncRoot) { - if (_currentFile != null) - { - (_currentFile as IDisposable)?.Dispose(); - _currentFile = null; - } + if (_currentFile == null) return; + CloseFile(); + _isDisposed = true; + } + } - _nextCheckpoint = null; + void CloseFile() + { + if (_currentFile != null) + { + (_currentFile as IDisposable)?.Dispose(); + _currentFile = null; } - public void FlushToDisk() + _nextCheckpoint = null; + } + + public void FlushToDisk() + { + lock (_syncRoot) { - lock (_syncRoot) - { - _currentFile?.FlushToDisk(); - } + _currentFile?.FlushToDisk(); } } } diff --git a/src/Serilog.Sinks.File/Sinks/File/RollingIntervalExtensions.cs b/src/Serilog.Sinks.File/Sinks/File/RollingIntervalExtensions.cs index 2c9e2fd..a469abe 100644 --- a/src/Serilog.Sinks.File/Sinks/File/RollingIntervalExtensions.cs +++ b/src/Serilog.Sinks.File/Sinks/File/RollingIntervalExtensions.cs @@ -12,75 +12,52 @@ // See the License for the specific language governing permissions and // limitations under the License. -using System; +namespace Serilog.Sinks.File; -namespace Serilog.Sinks.File +static class RollingIntervalExtensions { - static class RollingIntervalExtensions + public static string GetFormat(this RollingInterval interval) { - public static string GetFormat(this RollingInterval interval) + return interval switch { - switch (interval) - { - case RollingInterval.Infinite: - return ""; - case RollingInterval.Year: - return "yyyy"; - case RollingInterval.Month: - return "yyyyMM"; - case RollingInterval.Day: - return "yyyyMMdd"; - case RollingInterval.Hour: - return "yyyyMMddHH"; - case RollingInterval.Minute: - return "yyyyMMddHHmm"; - default: - throw new ArgumentException("Invalid rolling interval"); - } - } + RollingInterval.Infinite => "", + RollingInterval.Year => "yyyy", + RollingInterval.Month => "yyyyMM", + RollingInterval.Day => "yyyyMMdd", + RollingInterval.Hour => "yyyyMMddHH", + RollingInterval.Minute => "yyyyMMddHHmm", + _ => throw new ArgumentException("Invalid rolling interval.") + }; + } - public static DateTime? GetCurrentCheckpoint(this RollingInterval interval, DateTime instant) + public static DateTime? GetCurrentCheckpoint(this RollingInterval interval, DateTime instant) + { + return interval switch { - switch (interval) - { - case RollingInterval.Infinite: - return null; - case RollingInterval.Year: - return new DateTime(instant.Year, 1, 1, 0, 0, 0, instant.Kind); - case RollingInterval.Month: - return new DateTime(instant.Year, instant.Month, 1, 0, 0, 0, instant.Kind); - case RollingInterval.Day: - return new DateTime(instant.Year, instant.Month, instant.Day, 0, 0, 0, instant.Kind); - case RollingInterval.Hour: - return new DateTime(instant.Year, instant.Month, instant.Day, instant.Hour, 0, 0, instant.Kind); - case RollingInterval.Minute: - return new DateTime(instant.Year, instant.Month, instant.Day, instant.Hour, instant.Minute, 0, instant.Kind); - default: - throw new ArgumentException("Invalid rolling interval"); - } - } + RollingInterval.Infinite => null, + RollingInterval.Year => new DateTime(instant.Year, 1, 1, 0, 0, 0, instant.Kind), + RollingInterval.Month => new DateTime(instant.Year, instant.Month, 1, 0, 0, 0, instant.Kind), + RollingInterval.Day => new DateTime(instant.Year, instant.Month, instant.Day, 0, 0, 0, instant.Kind), + RollingInterval.Hour => new DateTime(instant.Year, instant.Month, instant.Day, instant.Hour, 0, 0, instant.Kind), + RollingInterval.Minute => new DateTime(instant.Year, instant.Month, instant.Day, instant.Hour, instant.Minute, 0, instant.Kind), + _ => throw new ArgumentException("Invalid rolling interval.") + }; + } - public static DateTime? GetNextCheckpoint(this RollingInterval interval, DateTime instant) - { - var current = GetCurrentCheckpoint(interval, instant); - if (current == null) - return null; + public static DateTime? GetNextCheckpoint(this RollingInterval interval, DateTime instant) + { + var current = GetCurrentCheckpoint(interval, instant); + if (current == null) + return null; - switch (interval) - { - case RollingInterval.Year: - return current.Value.AddYears(1); - case RollingInterval.Month: - return current.Value.AddMonths(1); - case RollingInterval.Day: - return current.Value.AddDays(1); - case RollingInterval.Hour: - return current.Value.AddHours(1); - case RollingInterval.Minute: - return current.Value.AddMinutes(1); - default: - throw new ArgumentException("Invalid rolling interval"); - } - } + return interval switch + { + RollingInterval.Year => current.Value.AddYears(1), + RollingInterval.Month => current.Value.AddMonths(1), + RollingInterval.Day => current.Value.AddDays(1), + RollingInterval.Hour => current.Value.AddHours(1), + RollingInterval.Minute => current.Value.AddMinutes(1), + _ => throw new ArgumentException("Invalid rolling interval.") + }; } } diff --git a/src/Serilog.Sinks.File/Sinks/File/RollingLogFile.cs b/src/Serilog.Sinks.File/Sinks/File/RollingLogFile.cs index be64c4e..3a5b493 100644 --- a/src/Serilog.Sinks.File/Sinks/File/RollingLogFile.cs +++ b/src/Serilog.Sinks.File/Sinks/File/RollingLogFile.cs @@ -11,24 +11,13 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. - -using System; -namespace Serilog.Sinks.File -{ - class RollingLogFile - { - public RollingLogFile(string filename, DateTime? dateTime, int? sequenceNumber) - { - Filename = filename; - DateTime = dateTime; - SequenceNumber = sequenceNumber; - } - - public string Filename { get; } - public DateTime? DateTime { get; } +namespace Serilog.Sinks.File; - public int? SequenceNumber { get; } - } -} +class RollingLogFile(string filename, DateTime? dateTime, int? sequenceNumber) +{ + public string Filename { get; } = filename; + public DateTime? DateTime { get; } = dateTime; + public int? SequenceNumber { get; } = sequenceNumber; +} diff --git a/src/Serilog.Sinks.File/Sinks/File/SharedFileSink.AtomicAppend.cs b/src/Serilog.Sinks.File/Sinks/File/SharedFileSink.AtomicAppend.cs index 6cf55cb..c753e46 100644 --- a/src/Serilog.Sinks.File/Sinks/File/SharedFileSink.AtomicAppend.cs +++ b/src/Serilog.Sinks.File/Sinks/File/SharedFileSink.AtomicAppend.cs @@ -14,163 +14,160 @@ #if ATOMIC_APPEND -using System; -using System.IO; using System.Security.AccessControl; using System.Text; using Serilog.Events; using Serilog.Formatting; -namespace Serilog.Sinks.File +namespace Serilog.Sinks.File; + +/// +/// Write log events to a disk file. +/// +[Obsolete("This type will be removed from the public API in a future version; use `WriteTo.File(shared: true)` instead.")] +public sealed class SharedFileSink : IFileSink, IDisposable { - /// - /// Write log events to a disk file. - /// - [Obsolete("This type will be removed from the public API in a future version; use `WriteTo.File(shared: true)` instead.")] - public sealed class SharedFileSink : IFileSink, IDisposable + readonly MemoryStream _writeBuffer; + readonly string _path; + readonly TextWriter _output; + readonly ITextFormatter _textFormatter; + readonly long? _fileSizeLimitBytes; + readonly object _syncRoot = new(); + + // The stream is reopened with a larger buffer if atomic writes beyond the current buffer size are needed. + FileStream _fileOutput; + int _fileStreamBufferLength = DefaultFileStreamBufferLength; + + const int DefaultFileStreamBufferLength = 4096; + + /// Construct a . + /// Path to the file. + /// Formatter used to convert log events to text. + /// The approximate maximum size, in bytes, to which a log file will be allowed to grow. + /// For unrestricted growth, pass null. The default is 1 GB. To avoid writing partial events, the last event within the limit + /// will be written in full even if it exceeds the limit. + /// Character encoding used to write the text file. The default is UTF-8 without BOM. + /// Configuration object allowing method chaining. + /// 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) { - readonly MemoryStream _writeBuffer; - readonly string _path; - readonly TextWriter _output; - readonly ITextFormatter _textFormatter; - readonly long? _fileSizeLimitBytes; - readonly object _syncRoot = new object(); - - // The stream is reopened with a larger buffer if atomic writes beyond the current buffer size are needed. - FileStream _fileOutput; - int _fileStreamBufferLength = DefaultFileStreamBufferLength; - - const int DefaultFileStreamBufferLength = 4096; - - /// Construct a . - /// Path to the file. - /// Formatter used to convert log events to text. - /// The approximate maximum size, in bytes, to which a log file will be allowed to grow. - /// For unrestricted growth, pass null. The default is 1 GB. To avoid writing partial events, the last event within the limit - /// will be written in full even if it exceeds the limit. - /// Character encoding used to write the text file. The default is UTF-8 without BOM. - /// Configuration object allowing method chaining. - /// 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) - throw new ArgumentException("Invalid value provided; file size limit must be at least 1 byte, or null"); + 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)); - _fileSizeLimitBytes = fileSizeLimitBytes; + _path = path ?? throw new ArgumentNullException(nameof(path)); + _textFormatter = textFormatter ?? throw new ArgumentNullException(nameof(textFormatter)); + _fileSizeLimitBytes = fileSizeLimitBytes; - var directory = Path.GetDirectoryName(path); - if (!string.IsNullOrWhiteSpace(directory) && !Directory.Exists(directory)) - { - Directory.CreateDirectory(directory); - } - - // FileSystemRights.AppendData sets the Win32 FILE_APPEND_DATA flag. On Linux this is O_APPEND, but that API is not yet - // exposed by .NET Core. - _fileOutput = new FileStream( - path, - FileMode.Append, - FileSystemRights.AppendData, - FileShare.ReadWrite, - _fileStreamBufferLength, - FileOptions.None); - - _writeBuffer = new MemoryStream(); - _output = new StreamWriter(_writeBuffer, - encoding ?? new UTF8Encoding(encoderShouldEmitUTF8Identifier: false)); + var directory = Path.GetDirectoryName(path); + if (!string.IsNullOrWhiteSpace(directory) && !Directory.Exists(directory)) + { + Directory.CreateDirectory(directory); } - bool IFileSink.EmitOrOverflow(LogEvent logEvent) - { - if (logEvent == null) throw new ArgumentNullException(nameof(logEvent)); + // FileSystemRights.AppendData sets the Win32 FILE_APPEND_DATA flag. On Linux this is O_APPEND, but that API is not yet + // exposed by .NET Core. + _fileOutput = new FileStream( + path, + FileMode.Append, + FileSystemRights.AppendData, + FileShare.ReadWrite, + _fileStreamBufferLength, + FileOptions.None); + + _writeBuffer = new MemoryStream(); + _output = new StreamWriter(_writeBuffer, + encoding ?? new UTF8Encoding(encoderShouldEmitUTF8Identifier: false)); + } + + bool IFileSink.EmitOrOverflow(LogEvent logEvent) + { + if (logEvent == null) throw new ArgumentNullException(nameof(logEvent)); - lock (_syncRoot) + lock (_syncRoot) + { + try { - try + _textFormatter.Format(logEvent, _output); + _output.Flush(); + var bytes = _writeBuffer.GetBuffer(); + var length = (int) _writeBuffer.Length; + if (length > _fileStreamBufferLength) { - _textFormatter.Format(logEvent, _output); - _output.Flush(); - var bytes = _writeBuffer.GetBuffer(); - var length = (int) _writeBuffer.Length; - if (length > _fileStreamBufferLength) - { - var oldOutput = _fileOutput; - - _fileOutput = new FileStream( - _path, - FileMode.Append, - FileSystemRights.AppendData, - FileShare.ReadWrite, - length, - FileOptions.None); - _fileStreamBufferLength = length; - - oldOutput.Dispose(); - } + var oldOutput = _fileOutput; + + _fileOutput = new FileStream( + _path, + FileMode.Append, + FileSystemRights.AppendData, + FileShare.ReadWrite, + length, + FileOptions.None); + _fileStreamBufferLength = length; + + oldOutput.Dispose(); + } - if (_fileSizeLimitBytes != null) + if (_fileSizeLimitBytes != null) + { + try { - try - { - if (_fileOutput.Length >= _fileSizeLimitBytes.Value) - return false; - } - catch (FileNotFoundException) { } // Cheaper and more reliable than checking existence + if (_fileOutput.Length >= _fileSizeLimitBytes.Value) + return false; } - - _fileOutput.Write(bytes, 0, length); - _fileOutput.Flush(); - return true; - } - catch - { - // Make sure there's no leftover cruft in there. - _output.Flush(); - throw; - } - finally - { - _writeBuffer.Position = 0; - _writeBuffer.SetLength(0); + catch (FileNotFoundException) { } // Cheaper and more reliable than checking existence } + + _fileOutput.Write(bytes, 0, length); + _fileOutput.Flush(); + return true; + } + catch + { + // Make sure there's no leftover cruft in there. + _output.Flush(); + throw; + } + finally + { + _writeBuffer.Position = 0; + _writeBuffer.SetLength(0); } } + } - /// - /// 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); - } + /// + /// 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); + } - /// - public void Dispose() + /// + public void Dispose() + { + lock (_syncRoot) { - lock (_syncRoot) - { - _fileOutput.Dispose(); - } + _fileOutput.Dispose(); } + } - /// - public void FlushToDisk() + /// + public void FlushToDisk() + { + lock (_syncRoot) { - lock (_syncRoot) - { - _output.Flush(); - _fileOutput.Flush(true); - } + _output.Flush(); + _fileOutput.Flush(true); } } } diff --git a/src/Serilog.Sinks.File/Sinks/File/SharedFileSink.OSMutex.cs b/src/Serilog.Sinks.File/Sinks/File/SharedFileSink.OSMutex.cs index b8a07db..6f5dc5c 100644 --- a/src/Serilog.Sinks.File/Sinks/File/SharedFileSink.OSMutex.cs +++ b/src/Serilog.Sinks.File/Sinks/File/SharedFileSink.OSMutex.cs @@ -1,4 +1,4 @@ -// Copyright 2013-2019 Serilog Contributors +// Copyright 2013-2019 Serilog Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -14,160 +14,156 @@ #if OS_MUTEX -using System; -using System.IO; using System.Text; using Serilog.Events; using Serilog.Formatting; -using System.Threading; using Serilog.Debugging; -namespace Serilog.Sinks.File +namespace Serilog.Sinks.File; + +/// +/// Write log events to a disk file. +/// +[Obsolete("This type will be removed from the public API in a future version; use `WriteTo.File(shared: true)` instead.")] +public sealed class SharedFileSink : IFileSink, IDisposable { - /// - /// Write log events to a disk file. - /// - [Obsolete("This type will be removed from the public API in a future version; use `WriteTo.File(shared: true)` instead.")] - public sealed class SharedFileSink : IFileSink, IDisposable + readonly TextWriter _output; + readonly FileStream _underlyingStream; + readonly ITextFormatter _textFormatter; + readonly long? _fileSizeLimitBytes; + readonly object _syncRoot = new(); + + const string MutexNameSuffix = ".serilog"; + const int MutexWaitTimeout = 10000; + readonly Mutex _mutex; + + /// Construct a . + /// Path to the file. + /// Formatter used to convert log events to text. + /// The approximate maximum size, in bytes, to which a log file will be allowed to grow. + /// For unrestricted growth, pass null. The default is 1 GB. To avoid writing partial events, the last event within the limit + /// will be written in full even if it exceeds the limit. + /// Character encoding used to write the text file. The default is UTF-8 without BOM. + /// Configuration object allowing method chaining. + /// The file will be written using the UTF-8 character set. + /// 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) { - readonly TextWriter _output; - readonly FileStream _underlyingStream; - readonly ITextFormatter _textFormatter; - readonly long? _fileSizeLimitBytes; - readonly object _syncRoot = new object(); - - const string MutexNameSuffix = ".serilog"; - const int MutexWaitTimeout = 10000; - readonly Mutex _mutex; - - /// Construct a . - /// Path to the file. - /// Formatter used to convert log events to text. - /// The approximate maximum size, in bytes, to which a log file will be allowed to grow. - /// For unrestricted growth, pass null. The default is 1 GB. To avoid writing partial events, the last event within the limit - /// will be written in full even if it exceeds the limit. - /// Character encoding used to write the text file. The default is UTF-8 without BOM. - /// Configuration object allowing method chaining. - /// The file will be written using the UTF-8 character set. - /// 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)); + if (fileSizeLimitBytes is < 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; + + var directory = Path.GetDirectoryName(path); + if (!string.IsNullOrWhiteSpace(directory) && !Directory.Exists(directory)) { - 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."); - _textFormatter = textFormatter ?? throw new ArgumentNullException(nameof(textFormatter)); - _fileSizeLimitBytes = fileSizeLimitBytes; - - var directory = Path.GetDirectoryName(path); - if (!string.IsNullOrWhiteSpace(directory) && !Directory.Exists(directory)) - { - Directory.CreateDirectory(directory); - } - - var mutexName = Path.GetFullPath(path).Replace(Path.DirectorySeparatorChar, ':') + MutexNameSuffix; - _mutex = new Mutex(false, mutexName); - _underlyingStream = System.IO.File.Open(path, FileMode.Append, FileAccess.Write, FileShare.ReadWrite); - _output = new StreamWriter(_underlyingStream, encoding ?? new UTF8Encoding(encoderShouldEmitUTF8Identifier: false)); + Directory.CreateDirectory(directory); } - bool IFileSink.EmitOrOverflow(LogEvent logEvent) + var mutexName = Path.GetFullPath(path).Replace(Path.DirectorySeparatorChar, ':') + MutexNameSuffix; + _mutex = new Mutex(false, mutexName); + _underlyingStream = System.IO.File.Open(path, FileMode.Append, FileAccess.Write, FileShare.ReadWrite); + _output = new StreamWriter(_underlyingStream, encoding ?? new UTF8Encoding(encoderShouldEmitUTF8Identifier: false)); + } + + bool IFileSink.EmitOrOverflow(LogEvent logEvent) + { + if (logEvent == null) throw new ArgumentNullException(nameof(logEvent)); + + lock (_syncRoot) { - if (logEvent == null) throw new ArgumentNullException(nameof(logEvent)); + if (!TryAcquireMutex()) + return true; // We didn't overflow, but, roll-on-size should not be attempted - lock (_syncRoot) + try { - if (!TryAcquireMutex()) - return true; // We didn't overflow, but, roll-on-size should not be attempted - - try + _underlyingStream.Seek(0, SeekOrigin.End); + if (_fileSizeLimitBytes != null) { - _underlyingStream.Seek(0, SeekOrigin.End); - if (_fileSizeLimitBytes != null) - { - if (_underlyingStream.Length >= _fileSizeLimitBytes.Value) - return false; - } - - _textFormatter.Format(logEvent, _output); - _output.Flush(); - _underlyingStream.Flush(); - return true; - } - finally - { - ReleaseMutex(); + if (_underlyingStream.Length >= _fileSizeLimitBytes.Value) + return false; } + + _textFormatter.Format(logEvent, _output); + _output.Flush(); + _underlyingStream.Flush(); + return true; + } + finally + { + ReleaseMutex(); } } + } + + /// + /// 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); + } - /// - /// Emit the provided log event to the sink. - /// - /// The log event to write. - /// When is null - public void Emit(LogEvent logEvent) + /// + public void Dispose() + { + lock (_syncRoot) { - ((IFileSink)this).EmitOrOverflow(logEvent); + _output.Dispose(); + _mutex.Dispose(); } + } - /// - public void Dispose() + /// + public void FlushToDisk() + { + lock (_syncRoot) { - lock (_syncRoot) + if (!TryAcquireMutex()) + return; + + try { - _output.Dispose(); - _mutex.Dispose(); + _underlyingStream.Flush(true); } - } - - /// - public void FlushToDisk() - { - lock (_syncRoot) + finally { - if (!TryAcquireMutex()) - return; - - try - { - _underlyingStream.Flush(true); - } - finally - { - ReleaseMutex(); - } + ReleaseMutex(); } } + } - bool TryAcquireMutex() + bool TryAcquireMutex() + { + try { - try - { - if (!_mutex.WaitOne(MutexWaitTimeout)) - { - SelfLog.WriteLine("Shared file mutex could not be acquired within {0} ms", MutexWaitTimeout); - return false; - } - } - catch (AbandonedMutexException) + if (!_mutex.WaitOne(MutexWaitTimeout)) { - SelfLog.WriteLine("Inherited shared file mutex after abandonment by another process"); + SelfLog.WriteLine("Shared file mutex could not be acquired within {0} ms", MutexWaitTimeout); + return false; } - - return true; } - - void ReleaseMutex() + catch (AbandonedMutexException) { - _mutex.ReleaseMutex(); + SelfLog.WriteLine("Inherited shared file mutex after abandonment by another process"); } + + return true; + } + + void ReleaseMutex() + { + _mutex.ReleaseMutex(); } } diff --git a/src/Serilog.Sinks.File/Sinks/File/WriteCountingStream.cs b/src/Serilog.Sinks.File/Sinks/File/WriteCountingStream.cs index e247144..39eec07 100644 --- a/src/Serilog.Sinks.File/Sinks/File/WriteCountingStream.cs +++ b/src/Serilog.Sinks.File/Sinks/File/WriteCountingStream.cs @@ -12,69 +12,64 @@ // See the License for the specific language governing permissions and // limitations under the License. -using System; -using System.IO; +namespace Serilog.Sinks.File; -namespace Serilog.Sinks.File +sealed class WriteCountingStream : Stream { - sealed class WriteCountingStream : Stream - { - readonly Stream _stream; + readonly Stream _stream; - public WriteCountingStream(Stream stream) - { - _stream = stream ?? throw new ArgumentNullException(nameof(stream)); - CountedLength = stream.Length; - } + public WriteCountingStream(Stream stream) + { + _stream = stream ?? throw new ArgumentNullException(nameof(stream)); + CountedLength = stream.Length; + } - public long CountedLength { get; private set; } + public long CountedLength { get; private set; } - protected override void Dispose(bool disposing) - { - if (disposing) - _stream.Dispose(); + protected override void Dispose(bool disposing) + { + if (disposing) + _stream.Dispose(); - base.Dispose(disposing); - } + base.Dispose(disposing); + } - public override void Write(byte[] buffer, int offset, int count) - { - _stream.Write(buffer, offset, count); - CountedLength += count; - } + public override void Write(byte[] buffer, int offset, int count) + { + _stream.Write(buffer, offset, count); + CountedLength += count; + } - public override void Flush() => _stream.Flush(); - public override bool CanRead => false; - public override bool CanSeek => _stream.CanSeek; - public override bool CanWrite => true; - public override long Length => _stream.Length; + public override void Flush() => _stream.Flush(); + public override bool CanRead => false; + public override bool CanSeek => _stream.CanSeek; + public override bool CanWrite => true; + public override long Length => _stream.Length; + public override long Position + { + get => _stream.Position; + set => throw new NotSupportedException(); + } - public override long Position - { - get => _stream.Position; - set => throw new NotSupportedException(); - } + public override long Seek(long offset, SeekOrigin origin) + { + throw new InvalidOperationException($"Seek operations are not available through `{nameof(WriteCountingStream)}`."); + } - public override long Seek(long offset, SeekOrigin origin) - { - throw new InvalidOperationException($"Seek operations are not available through `{nameof(WriteCountingStream)}`."); - } + public override void SetLength(long value) + { + _stream.SetLength(value); - public override void SetLength(long value) + if (value < CountedLength) { - _stream.SetLength(value); - - if (value < CountedLength) - { - // File is now shorter and our position has changed to _stream.Length - CountedLength = _stream.Length; - } + // 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) - { - throw new NotSupportedException(); - } + public override int Read(byte[] buffer, int offset, int count) + { + throw new NotSupportedException(); } } diff --git a/test/Serilog.Sinks.File.Tests/FileLoggerConfigurationExtensionsTests.cs b/test/Serilog.Sinks.File.Tests/FileLoggerConfigurationExtensionsTests.cs index 3dde37a..b3ce3e2 100644 --- a/test/Serilog.Sinks.File.Tests/FileLoggerConfigurationExtensionsTests.cs +++ b/test/Serilog.Sinks.File.Tests/FileLoggerConfigurationExtensionsTests.cs @@ -1,120 +1,106 @@ -using System; -using System.Threading; using Serilog.Sinks.File.Tests.Support; using Serilog.Tests.Support; using Xunit; -using System.IO; using System.Text; -namespace Serilog.Sinks.File.Tests +namespace Serilog.Sinks.File.Tests; + +public class FileLoggerConfigurationExtensionsTests { - public class FileLoggerConfigurationExtensionsTests + static readonly string InvalidPath = new(Path.GetInvalidPathChars()); + + [Fact] + public void WhenWritingCreationExceptionsAreSuppressed() { - static readonly string InvalidPath = new string(Path.GetInvalidPathChars()); + new LoggerConfiguration() + .WriteTo.File(InvalidPath) + .CreateLogger(); + } - [Fact] - public void WhenWritingCreationExceptionsAreSuppressed() - { + [Fact] + public void WhenAuditingCreationExceptionsPropagate() + { + Assert.Throws(() => new LoggerConfiguration() - .WriteTo.File(InvalidPath) - .CreateLogger(); - } + .AuditTo.File(InvalidPath) + .CreateLogger()); + } - [Fact] - public void WhenAuditingCreationExceptionsPropagate() - { - Assert.Throws(() => - new LoggerConfiguration() - .AuditTo.File(InvalidPath) - .CreateLogger()); - } + [Fact] + public void WhenWritingLoggingExceptionsAreSuppressed() + { + using var tmp = TempFolder.ForCaller(); + using var log = new LoggerConfiguration() + .WriteTo.File(new ThrowingLogEventFormatter(), tmp.AllocateFilename()) + .CreateLogger(); + log.Information("Hello"); + } - [Fact] - public void WhenWritingLoggingExceptionsAreSuppressed() - { - using (var tmp = TempFolder.ForCaller()) - using (var log = new LoggerConfiguration() - .WriteTo.File(new ThrowingLogEventFormatter(), tmp.AllocateFilename()) - .CreateLogger()) - { - log.Information("Hello"); - } - } + [Fact] + public void WhenAuditingLoggingExceptionsPropagate() + { + using var tmp = TempFolder.ForCaller(); + using var log = new LoggerConfiguration() + .AuditTo.File(new ThrowingLogEventFormatter(), tmp.AllocateFilename()) + .CreateLogger(); + var ex = Assert.Throws(() => log.Information("Hello")); + Assert.IsType(ex.GetBaseException()); + } - [Fact] - public void WhenAuditingLoggingExceptionsPropagate() - { - using (var tmp = TempFolder.ForCaller()) - using (var log = new LoggerConfiguration() - .AuditTo.File(new ThrowingLogEventFormatter(), tmp.AllocateFilename()) - .CreateLogger()) - { - var ex = Assert.Throws(() => log.Information("Hello")); - Assert.IsType(ex.GetBaseException()); - } - } + [Fact] + public void WhenFlushingToDiskReportedFileSinkCanBeCreatedAndDisposed() + { + using var tmp = TempFolder.ForCaller(); + using var log = new LoggerConfiguration() + .WriteTo.File(tmp.AllocateFilename(), flushToDiskInterval: TimeSpan.FromMilliseconds(500)) + .CreateLogger(); + log.Information("Hello"); + Thread.Sleep(TimeSpan.FromSeconds(1)); + } - [Fact] - public void WhenFlushingToDiskReportedFileSinkCanBeCreatedAndDisposed() - { - using (var tmp = TempFolder.ForCaller()) - using (var log = new LoggerConfiguration() - .WriteTo.File(tmp.AllocateFilename(), flushToDiskInterval: TimeSpan.FromMilliseconds(500)) - .CreateLogger()) - { - log.Information("Hello"); - Thread.Sleep(TimeSpan.FromSeconds(1)); - } - } + [Fact] + public void WhenFlushingToDiskReportedSharedFileSinkCanBeCreatedAndDisposed() + { + using var tmp = TempFolder.ForCaller(); + using var log = new LoggerConfiguration() + .WriteTo.File(tmp.AllocateFilename(), shared: true, flushToDiskInterval: TimeSpan.FromMilliseconds(500)) + .CreateLogger(); + log.Information("Hello"); + Thread.Sleep(TimeSpan.FromSeconds(1)); + } - [Fact] - public void WhenFlushingToDiskReportedSharedFileSinkCanBeCreatedAndDisposed() - { - using (var tmp = TempFolder.ForCaller()) - using (var log = new LoggerConfiguration() - .WriteTo.File(tmp.AllocateFilename(), shared: true, flushToDiskInterval: TimeSpan.FromMilliseconds(500)) - .CreateLogger()) - { - log.Information("Hello"); - Thread.Sleep(TimeSpan.FromSeconds(1)); - } - } + [Fact] + public void BufferingIsNotAvailableWhenSharingEnabled() + { + Assert.Throws(() => + new LoggerConfiguration() + .WriteTo.File("logs", buffered: true, shared: true)); + } - [Fact] - public void BufferingIsNotAvailableWhenSharingEnabled() - { - Assert.Throws(() => - new LoggerConfiguration() - .WriteTo.File("logs", buffered: true, shared: true)); - } + [Fact] + public void HooksAreNotAvailableWhenSharingEnabled() + { + Assert.Throws(() => + new LoggerConfiguration() + .WriteTo.File("logs", shared: true, hooks: new GZipHooks())); + } - [Fact] - public void HooksAreNotAvailableWhenSharingEnabled() - { - Assert.Throws(() => - new LoggerConfiguration() - .WriteTo.File("logs", shared: true, hooks: new GZipHooks())); - } + [Theory] + [InlineData(false)] + [InlineData(true)] + public void SpecifiedEncodingIsPropagated(bool shared) + { + using var tmp = TempFolder.ForCaller(); + var filename = tmp.AllocateFilename("txt"); - [Theory] - [InlineData(false)] - [InlineData(true)] - public void SpecifiedEncodingIsPropagated(bool shared) + using (var log = new LoggerConfiguration() + .WriteTo.File(filename, outputTemplate: "{Message}", encoding: Encoding.Unicode, shared: shared) + .CreateLogger()) { - using (var tmp = TempFolder.ForCaller()) - { - var filename = tmp.AllocateFilename("txt"); - - using (var log = new LoggerConfiguration() - .WriteTo.File(filename, outputTemplate: "{Message}", encoding: Encoding.Unicode, shared: shared) - .CreateLogger()) - { - log.Information("ten chars."); - } - - // Don't forget the two-byte BOM :-) - Assert.Equal(22, System.IO.File.ReadAllBytes(filename).Length); - } + log.Information("ten chars."); } + + // Don't forget the two-byte BOM :-) + Assert.Equal(22, System.IO.File.ReadAllBytes(filename).Length); } } diff --git a/test/Serilog.Sinks.File.Tests/FileSinkTests.cs b/test/Serilog.Sinks.File.Tests/FileSinkTests.cs index a33261f..0c76349 100644 --- a/test/Serilog.Sinks.File.Tests/FileSinkTests.cs +++ b/test/Serilog.Sinks.File.Tests/FileSinkTests.cs @@ -1,6 +1,4 @@ -using System.Collections.Generic; -using System.IO; -using System.IO.Compression; +using System.IO.Compression; using System.Text; using Xunit; using Serilog.Formatting.Json; @@ -9,272 +7,253 @@ #pragma warning disable 618 -namespace Serilog.Sinks.File.Tests +namespace Serilog.Sinks.File.Tests; + +public class FileSinkTests { - public class FileSinkTests + [Fact] + public void FileIsWrittenIfNonexistent() { - [Fact] - public void FileIsWrittenIfNonexistent() + using var tmp = TempFolder.ForCaller(); + var nonexistent = tmp.AllocateFilename("txt"); + var evt = Some.LogEvent("Hello, world!"); + + using (var sink = new FileSink(nonexistent, new JsonFormatter(), null)) { - using (var tmp = TempFolder.ForCaller()) - { - var nonexistent = tmp.AllocateFilename("txt"); - var evt = Some.LogEvent("Hello, world!"); + sink.Emit(evt); + } - using (var sink = new FileSink(nonexistent, new JsonFormatter(), null)) - { - sink.Emit(evt); - } + var lines = System.IO.File.ReadAllLines(nonexistent); + Assert.Contains("Hello, world!", lines[0]); + } - var lines = System.IO.File.ReadAllLines(nonexistent); - Assert.Contains("Hello, world!", lines[0]); - } - } + [Fact] + public void FileIsAppendedToWhenAlreadyCreated() + { + using var tmp = TempFolder.ForCaller(); + var path = tmp.AllocateFilename("txt"); + var evt = Some.LogEvent("Hello, world!"); - [Fact] - public void FileIsAppendedToWhenAlreadyCreated() + using (var sink = new FileSink(path, new JsonFormatter(), null)) { - using (var tmp = TempFolder.ForCaller()) - { - var path = tmp.AllocateFilename("txt"); - var evt = Some.LogEvent("Hello, world!"); - - using (var sink = new FileSink(path, new JsonFormatter(), null)) - { - sink.Emit(evt); - } - - using (var sink = new FileSink(path, new JsonFormatter(), null)) - { - sink.Emit(evt); - } - - var lines = System.IO.File.ReadAllLines(path); - Assert.Contains("Hello, world!", lines[0]); - Assert.Contains("Hello, world!", lines[1]); - } + sink.Emit(evt); } - [Fact] - public void WhenLimitIsSpecifiedFileSizeIsRestricted() + using (var sink = new FileSink(path, new JsonFormatter(), null)) { - const int maxBytes = 5000; - const int eventsToLimit = 10; + sink.Emit(evt); + } + + var lines = System.IO.File.ReadAllLines(path); + Assert.Contains("Hello, world!", lines[0]); + Assert.Contains("Hello, world!", lines[1]); + } + + [Fact] + public void WhenLimitIsSpecifiedFileSizeIsRestricted() + { + const int maxBytes = 5000; + const int eventsToLimit = 10; - using (var tmp = TempFolder.ForCaller()) + using var tmp = TempFolder.ForCaller(); + var path = tmp.AllocateFilename("txt"); + var evt = Some.LogEvent(new string('n', maxBytes / eventsToLimit)); + + using (var sink = new FileSink(path, new JsonFormatter(), maxBytes)) + { + for (var i = 0; i < eventsToLimit * 2; i++) { - var path = tmp.AllocateFilename("txt"); - var evt = Some.LogEvent(new string('n', maxBytes / eventsToLimit)); - - using (var sink = new FileSink(path, new JsonFormatter(), maxBytes)) - { - for (var i = 0; i < eventsToLimit * 2; i++) - { - sink.Emit(evt); - } - } - - var size = new FileInfo(path).Length; - Assert.True(size > maxBytes); - Assert.True(size < maxBytes * 2); + sink.Emit(evt); } } - [Fact] - public void WhenLimitIsNotSpecifiedFileSizeIsNotRestricted() - { - const int maxBytes = 5000; - const int eventsToLimit = 10; + var size = new FileInfo(path).Length; + Assert.True(size > maxBytes); + Assert.True(size < maxBytes * 2); + } - using (var tmp = TempFolder.ForCaller()) + [Fact] + public void WhenLimitIsNotSpecifiedFileSizeIsNotRestricted() + { + const int maxBytes = 5000; + const int eventsToLimit = 10; + + using var tmp = TempFolder.ForCaller(); + var path = tmp.AllocateFilename("txt"); + var evt = Some.LogEvent(new string('n', maxBytes / eventsToLimit)); + + using (var sink = new FileSink(path, new JsonFormatter(), null)) + { + for (var i = 0; i < eventsToLimit * 2; i++) { - var path = tmp.AllocateFilename("txt"); - var evt = Some.LogEvent(new string('n', maxBytes / eventsToLimit)); - - using (var sink = new FileSink(path, new JsonFormatter(), null)) - { - for (var i = 0; i < eventsToLimit * 2; i++) - { - sink.Emit(evt); - } - } - - var size = new FileInfo(path).Length; - Assert.True(size > maxBytes * 2); + sink.Emit(evt); } } + var size = new FileInfo(path).Length; + Assert.True(size > maxBytes * 2); + } - [Fact] - public void WhenLimitIsSpecifiedAndEncodingHasPreambleDataIsCorrectlyAppendedToFileSink() - { - long? maxBytes = 5000; - var encoding = Encoding.UTF8; - Assert.True(encoding.GetPreamble().Length > 0); - WriteTwoEventsAndCheckOutputFileLength(maxBytes, encoding); - } + [Fact] + public void WhenLimitIsSpecifiedAndEncodingHasPreambleDataIsCorrectlyAppendedToFileSink() + { + long? maxBytes = 5000; + var encoding = Encoding.UTF8; - [Fact] - public void WhenLimitIsNotSpecifiedAndEncodingHasPreambleDataIsCorrectlyAppendedToFileSink() - { - var encoding = Encoding.UTF8; + Assert.True(encoding.GetPreamble().Length > 0); + WriteTwoEventsAndCheckOutputFileLength(maxBytes, encoding); + } - Assert.True(encoding.GetPreamble().Length > 0); - WriteTwoEventsAndCheckOutputFileLength(null, encoding); - } + [Fact] + public void WhenLimitIsNotSpecifiedAndEncodingHasPreambleDataIsCorrectlyAppendedToFileSink() + { + var encoding = Encoding.UTF8; - [Fact] - public void WhenLimitIsSpecifiedAndEncodingHasNoPreambleDataIsCorrectlyAppendedToFileSink() - { - long? maxBytes = 5000; - var encoding = new UTF8Encoding(false); + Assert.True(encoding.GetPreamble().Length > 0); + WriteTwoEventsAndCheckOutputFileLength(null, encoding); + } - Assert.Empty(encoding.GetPreamble()); - WriteTwoEventsAndCheckOutputFileLength(maxBytes, encoding); - } + [Fact] + public void WhenLimitIsSpecifiedAndEncodingHasNoPreambleDataIsCorrectlyAppendedToFileSink() + { + long? maxBytes = 5000; + var encoding = new UTF8Encoding(false); - [Fact] - public void WhenLimitIsNotSpecifiedAndEncodingHasNoPreambleDataIsCorrectlyAppendedToFileSink() - { - var encoding = new UTF8Encoding(false); + Assert.Empty(encoding.GetPreamble()); + WriteTwoEventsAndCheckOutputFileLength(maxBytes, encoding); + } - Assert.Empty(encoding.GetPreamble()); - WriteTwoEventsAndCheckOutputFileLength(null, encoding); - } + [Fact] + public void WhenLimitIsNotSpecifiedAndEncodingHasNoPreambleDataIsCorrectlyAppendedToFileSink() + { + var encoding = new UTF8Encoding(false); - [Fact] - public void OnOpenedLifecycleHookCanWrapUnderlyingStream() - { - var gzipWrapper = new GZipHooks(); + Assert.Empty(encoding.GetPreamble()); + WriteTwoEventsAndCheckOutputFileLength(null, encoding); + } - using (var tmp = TempFolder.ForCaller()) - { - var path = tmp.AllocateFilename("txt"); - var evt = Some.LogEvent("Hello, world!"); - - using (var sink = new FileSink(path, new JsonFormatter(), null, null, false, gzipWrapper)) - { - sink.Emit(evt); - sink.Emit(evt); - } - - // Ensure the data was written through the wrapping GZipStream, by decompressing and comparing against - // what we wrote - List lines; - using (var textStream = new MemoryStream()) - { - using (var fs = System.IO.File.OpenRead(path)) - using (var decompressStream = new GZipStream(fs, CompressionMode.Decompress)) - { - decompressStream.CopyTo(textStream); - } - - textStream.Position = 0; - lines = textStream.ReadAllLines(); - } - - Assert.Equal(2, lines.Count); - Assert.Contains("Hello, world!", lines[0]); - } + [Fact] + public void OnOpenedLifecycleHookCanWrapUnderlyingStream() + { + var gzipWrapper = new GZipHooks(); + + using var tmp = TempFolder.ForCaller(); + var path = tmp.AllocateFilename("txt"); + var evt = Some.LogEvent("Hello, world!"); + + using (var sink = new FileSink(path, new JsonFormatter(), null, null, false, gzipWrapper)) + { + sink.Emit(evt); + sink.Emit(evt); } - [Fact] - public static void OnOpenedLifecycleHookCanWriteFileHeader() + // Ensure the data was written through the wrapping GZipStream, by decompressing and comparing against + // what we wrote + List lines; + using (var textStream = new MemoryStream()) { - using (var tmp = TempFolder.ForCaller()) + using (var fs = System.IO.File.OpenRead(path)) + using (var decompressStream = new GZipStream(fs, CompressionMode.Decompress)) { - var headerWriter = new FileHeaderWriter("This is the file header"); + decompressStream.CopyTo(textStream); + } - var path = tmp.AllocateFilename("txt"); - using (new FileSink(path, new JsonFormatter(), null, new UTF8Encoding(false), false, headerWriter)) - { - // Open and write header - } + textStream.Position = 0; + lines = textStream.ReadAllLines(); + } - using (var sink = new FileSink(path, new JsonFormatter(), null, new UTF8Encoding(false), false, headerWriter)) - { - // Length check should prevent duplicate header here - sink.Emit(Some.LogEvent()); - } + Assert.Equal(2, lines.Count); + Assert.Contains("Hello, world!", lines[0]); + } - var lines = System.IO.File.ReadAllLines(path); + [Fact] + public static void OnOpenedLifecycleHookCanWriteFileHeader() + { + using var tmp = TempFolder.ForCaller(); + var headerWriter = new FileHeaderWriter("This is the file header"); - Assert.Equal(2, lines.Length); - Assert.Equal(headerWriter.Header, lines[0]); - Assert.Equal('{', lines[1][0]); - } + var path = tmp.AllocateFilename("txt"); + using (new FileSink(path, new JsonFormatter(), null, new UTF8Encoding(false), false, headerWriter)) + { + // Open and write header } - [Fact] - public static void OnOpenedLifecycleHookCanCaptureFilePath() + using (var sink = new FileSink(path, new JsonFormatter(), null, new UTF8Encoding(false), false, headerWriter)) { - using (var tmp = TempFolder.ForCaller()) - { - var capturePath = new CaptureFilePathHook(); + // Length check should prevent duplicate header here + sink.Emit(Some.LogEvent()); + } - var path = tmp.AllocateFilename("txt"); - using (new FileSink(path, new JsonFormatter(), null, new UTF8Encoding(false), false, capturePath)) - { - // Open and capture the log file path - } + var lines = System.IO.File.ReadAllLines(path); - Assert.Equal(path, capturePath.Path); - } + Assert.Equal(2, lines.Length); + Assert.Equal(headerWriter.Header, lines[0]); + Assert.Equal('{', lines[1][0]); + } + + [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 } - [Fact] - public static void OnOpenedLifecycleHookCanEmptyTheFileContents() + 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)) { - using (var tmp = TempFolder.ForCaller()) - { - var emptyFileHook = new TruncateFileHook(); + sink.Emit(Some.LogEvent()); + } - 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()); + } - 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); - var lines = System.IO.File.ReadAllLines(path); + Assert.Single(lines); + Assert.Equal('{', lines[0][0]); + } - Assert.Single(lines); - Assert.Equal('{', lines[0][0]); - } + static void WriteTwoEventsAndCheckOutputFileLength(long? maxBytes, Encoding encoding) + { + using var tmp = TempFolder.ForCaller(); + var path = tmp.AllocateFilename("txt"); + var evt = Some.LogEvent("Irrelevant as it will be replaced by the formatter"); + const string actualEventOutput = "x"; + var formatter = new FixedOutputFormatter(actualEventOutput); + var eventOuputLength = encoding.GetByteCount(actualEventOutput); + + using (var sink = new FileSink(path, formatter, maxBytes, encoding: encoding)) + { + sink.Emit(evt); } + var size = new FileInfo(path).Length; + Assert.Equal(encoding.GetPreamble().Length + eventOuputLength, size); - static void WriteTwoEventsAndCheckOutputFileLength(long? maxBytes, Encoding encoding) + //write a second event to the same file + using (var sink = new FileSink(path, formatter, maxBytes, encoding: encoding)) { - using (var tmp = TempFolder.ForCaller()) - { - var path = tmp.AllocateFilename("txt"); - var evt = Some.LogEvent("Irrelevant as it will be replaced by the formatter"); - const string actualEventOutput = "x"; - var formatter = new FixedOutputFormatter(actualEventOutput); - var eventOuputLength = encoding.GetByteCount(actualEventOutput); - - using (var sink = new FileSink(path, formatter, maxBytes, encoding: encoding)) - { - sink.Emit(evt); - } - var size = new FileInfo(path).Length; - Assert.Equal(encoding.GetPreamble().Length + eventOuputLength, size); - - //write a second event to the same file - using (var sink = new FileSink(path, formatter, maxBytes, encoding: encoding)) - { - sink.Emit(evt); - } - - size = new FileInfo(path).Length; - Assert.Equal(encoding.GetPreamble().Length + eventOuputLength * 2, size); - } + sink.Emit(evt); } + + size = new FileInfo(path).Length; + Assert.Equal(encoding.GetPreamble().Length + eventOuputLength * 2, size); } } diff --git a/test/Serilog.Sinks.File.Tests/Properties/launchSettings.json b/test/Serilog.Sinks.File.Tests/Properties/launchSettings.json deleted file mode 100644 index 3ab0635..0000000 --- a/test/Serilog.Sinks.File.Tests/Properties/launchSettings.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "profiles": { - "test": { - "commandName": "test" - }, - "test-dnxcore50": { - "commandName": "test", - "sdkVersion": "dnx-coreclr-win-x86.1.0.0-rc1-final" - } - } -} \ No newline at end of file diff --git a/test/Serilog.Sinks.File.Tests/RollingFileSinkTests.cs b/test/Serilog.Sinks.File.Tests/RollingFileSinkTests.cs index 9232bde..5739983 100644 --- a/test/Serilog.Sinks.File.Tests/RollingFileSinkTests.cs +++ b/test/Serilog.Sinks.File.Tests/RollingFileSinkTests.cs @@ -1,306 +1,295 @@ -using System; -using System.Collections.Generic; -using System.IO; using System.IO.Compression; -using System.Linq; using Xunit; using Serilog.Events; using Serilog.Sinks.File.Tests.Support; using Serilog.Configuration; using Serilog.Core; -namespace Serilog.Sinks.File.Tests +namespace Serilog.Sinks.File.Tests; + +public class RollingFileSinkTests { - public class RollingFileSinkTests + [Fact] + public void LogEventsAreEmittedToTheFileNamedAccordingToTheEventTimestamp() { - [Fact] - public void LogEventsAreEmittedToTheFileNamedAccordingToTheEventTimestamp() - { - TestRollingEventSequence(Some.InformationEvent()); - } + TestRollingEventSequence(Some.InformationEvent()); + } - [Fact] - public void EventsAreWrittenWhenSharingIsEnabled() - { - TestRollingEventSequence( - (pf, wt) => wt.File(pf, shared: true, rollingInterval: RollingInterval.Day), - new[] { Some.InformationEvent() }); - } + [Fact] + public void EventsAreWrittenWhenSharingIsEnabled() + { + TestRollingEventSequence( + (pf, wt) => wt.File(pf, shared: true, rollingInterval: RollingInterval.Day), + new[] { Some.InformationEvent() }); + } - [Fact] - public void EventsAreWrittenWhenBufferingIsEnabled() - { - TestRollingEventSequence( - (pf, wt) => wt.File(pf, buffered: true, rollingInterval: RollingInterval.Day), - new[] { Some.InformationEvent() }); - } + [Fact] + public void EventsAreWrittenWhenBufferingIsEnabled() + { + TestRollingEventSequence( + (pf, wt) => wt.File(pf, buffered: true, rollingInterval: RollingInterval.Day), + new[] { Some.InformationEvent() }); + } - [Fact] - public void EventsAreWrittenWhenDiskFlushingIsEnabled() - { - // Doesn't test flushing, but ensures we haven't broken basic logging - TestRollingEventSequence( - (pf, wt) => wt.File(pf, flushToDiskInterval: TimeSpan.FromMilliseconds(50), rollingInterval: RollingInterval.Day), - new[] { Some.InformationEvent() }); - } + [Fact] + public void EventsAreWrittenWhenDiskFlushingIsEnabled() + { + // Doesn't test flushing, but ensures we haven't broken basic logging + TestRollingEventSequence( + (pf, wt) => wt.File(pf, flushToDiskInterval: TimeSpan.FromMilliseconds(50), rollingInterval: RollingInterval.Day), + new[] { Some.InformationEvent() }); + } - [Fact] - public void WhenTheDateChangesTheCorrectFileIsWritten() - { - var e1 = Some.InformationEvent(); - var e2 = Some.InformationEvent(e1.Timestamp.AddDays(1)); - TestRollingEventSequence(e1, e2); - } + [Fact] + public void WhenTheDateChangesTheCorrectFileIsWritten() + { + var e1 = Some.InformationEvent(); + var e2 = Some.InformationEvent(e1.Timestamp.AddDays(1)); + TestRollingEventSequence(e1, e2); + } - [Fact] - public void WhenRetentionCountIsSetOldFilesAreDeleted() - { - LogEvent e1 = Some.InformationEvent(), - e2 = Some.InformationEvent(e1.Timestamp.AddDays(1)), - e3 = Some.InformationEvent(e2.Timestamp.AddDays(5)); + [Fact] + public void WhenRetentionCountIsSetOldFilesAreDeleted() + { + LogEvent e1 = Some.InformationEvent(), + e2 = Some.InformationEvent(e1.Timestamp.AddDays(1)), + e3 = Some.InformationEvent(e2.Timestamp.AddDays(5)); + + TestRollingEventSequence( + (pf, wt) => wt.File(pf, retainedFileCountLimit: 2, rollingInterval: RollingInterval.Day), + new[] {e1, e2, e3}, + files => + { + Assert.Equal(3, files.Count); + Assert.True(!System.IO.File.Exists(files[0])); + Assert.True(System.IO.File.Exists(files[1])); + Assert.True(System.IO.File.Exists(files[2])); + }); + } - TestRollingEventSequence( - (pf, wt) => wt.File(pf, retainedFileCountLimit: 2, rollingInterval: RollingInterval.Day), - new[] {e1, e2, e3}, - files => - { - Assert.Equal(3, files.Count); - Assert.True(!System.IO.File.Exists(files[0])); - Assert.True(System.IO.File.Exists(files[1])); - Assert.True(System.IO.File.Exists(files[2])); - }); - } + [Fact] + public void 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 WhenRetentionTimeIsSetOldFilesAreDeleted() - { - LogEvent e1 = Some.InformationEvent(DateTime.Today.AddDays(-5)), - e2 = Some.InformationEvent(e1.Timestamp.AddDays(2)), - e3 = Some.InformationEvent(e2.Timestamp.AddDays(5)); + [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])); + }); + } - 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 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 WhenRetentionCountAndTimeIsSetOldFilesAreDeletedByTime() - { - LogEvent e1 = Some.InformationEvent(DateTime.Today.AddDays(-5)), - e2 = Some.InformationEvent(e1.Timestamp.AddDays(2)), + [Fact] + public void WhenRetentionCountAndArchivingHookIsSetOldFilesAreCopiedAndOriginalDeleted() + { + const string archiveDirectory = "OldLogs"; + LogEvent e1 = Some.InformationEvent(), + e2 = Some.InformationEvent(e1.Timestamp.AddDays(1)), e3 = Some.InformationEvent(e2.Timestamp.AddDays(5)); - TestRollingEventSequence( - (pf, wt) => wt.File(pf, retainedFileCountLimit: 2, 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])); - }); - } + TestRollingEventSequence( + (pf, wt) => wt.File(pf, retainedFileCountLimit: 2, rollingInterval: RollingInterval.Day, hooks: new ArchiveOldLogsHook(archiveDirectory)), + new[] {e1, e2, e3}, + files => + { + Assert.Equal(3, files.Count); + Assert.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))); + }); + } - [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)); + [Fact] + public void WhenSizeLimitIsBreachedNewFilesCreated() + { + var fileName = Some.String() + ".txt"; + using var temp = new TempFolder(); + using var log = new LoggerConfiguration() + .WriteTo.File(Path.Combine(temp.Path, fileName), rollOnFileSizeLimit: true, fileSizeLimitBytes: 1) + .CreateLogger(); + LogEvent e1 = Some.InformationEvent(), + e2 = Some.InformationEvent(e1.Timestamp), + e3 = Some.InformationEvent(e1.Timestamp); + + log.Write(e1); log.Write(e2); log.Write(e3); + + var files = Directory.GetFiles(temp.Path) + .OrderBy(p => p, StringComparer.OrdinalIgnoreCase) + .ToArray(); + + Assert.Equal(3, files.Length); + Assert.True(files[0].EndsWith(fileName), files[0]); + Assert.True(files[1].EndsWith("_001.txt"), files[1]); + Assert.True(files[2].EndsWith("_002.txt"), files[2]); + } - 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 WhenStreamWrapperSpecifiedIsUsedForRolledFiles() + { + var gzipWrapper = new GZipHooks(); + var fileName = Some.String() + ".txt"; - [Fact] - public void WhenRetentionCountAndArchivingHookIsSetOldFilesAreCopiedAndOriginalDeleted() + using var temp = new TempFolder(); + string[] files; + var logEvents = new[] { - const string archiveDirectory = "OldLogs"; - LogEvent e1 = Some.InformationEvent(), - e2 = Some.InformationEvent(e1.Timestamp.AddDays(1)), - e3 = Some.InformationEvent(e2.Timestamp.AddDays(5)); - - TestRollingEventSequence( - (pf, wt) => wt.File(pf, retainedFileCountLimit: 2, rollingInterval: RollingInterval.Day, hooks: new ArchiveOldLogsHook(archiveDirectory)), - new[] {e1, e2, e3}, - files => - { - Assert.Equal(3, files.Count); - Assert.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))); - }); - } - - [Fact] - public void WhenSizeLimitIsBreachedNewFilesCreated() + Some.InformationEvent(), + Some.InformationEvent(), + Some.InformationEvent() + }; + + using (var log = new LoggerConfiguration() + .WriteTo.File(Path.Combine(temp.Path, fileName), rollOnFileSizeLimit: true, fileSizeLimitBytes: 1, hooks: gzipWrapper) + .CreateLogger()) { - var fileName = Some.String() + ".txt"; - using (var temp = new TempFolder()) - using (var log = new LoggerConfiguration() - .WriteTo.File(Path.Combine(temp.Path, fileName), rollOnFileSizeLimit: true, fileSizeLimitBytes: 1) - .CreateLogger()) - { - LogEvent e1 = Some.InformationEvent(), - e2 = Some.InformationEvent(e1.Timestamp), - e3 = Some.InformationEvent(e1.Timestamp); - log.Write(e1); log.Write(e2); log.Write(e3); + foreach (var logEvent in logEvents) + { + log.Write(logEvent); + } - var files = Directory.GetFiles(temp.Path) - .OrderBy(p => p, StringComparer.OrdinalIgnoreCase) - .ToArray(); + files = Directory.GetFiles(temp.Path) + .OrderBy(p => p, StringComparer.OrdinalIgnoreCase) + .ToArray(); - Assert.Equal(3, files.Length); - Assert.True(files[0].EndsWith(fileName), files[0]); - Assert.True(files[1].EndsWith("_001.txt"), files[1]); - Assert.True(files[2].EndsWith("_002.txt"), files[2]); - } + Assert.Equal(3, files.Length); + Assert.True(files[0].EndsWith(fileName), files[0]); + Assert.True(files[1].EndsWith("_001.txt"), files[1]); + Assert.True(files[2].EndsWith("_002.txt"), files[2]); } - [Fact] - public void WhenStreamWrapperSpecifiedIsUsedForRolledFiles() + // Ensure the data was written through the wrapping GZipStream, by decompressing and comparing against + // what we wrote + for (var i = 0; i < files.Length; i++) { - var gzipWrapper = new GZipHooks(); - var fileName = Some.String() + ".txt"; - - using (var temp = new TempFolder()) + using var textStream = new MemoryStream(); + using (var fs = System.IO.File.OpenRead(files[i])) + using (var decompressStream = new GZipStream(fs, CompressionMode.Decompress)) { - string[] files; - var logEvents = new[] - { - Some.InformationEvent(), - Some.InformationEvent(), - Some.InformationEvent() - }; - - using (var log = new LoggerConfiguration() - .WriteTo.File(Path.Combine(temp.Path, fileName), rollOnFileSizeLimit: true, fileSizeLimitBytes: 1, hooks: gzipWrapper) - .CreateLogger()) - { - - foreach (var logEvent in logEvents) - { - log.Write(logEvent); - } - - files = Directory.GetFiles(temp.Path) - .OrderBy(p => p, StringComparer.OrdinalIgnoreCase) - .ToArray(); - - Assert.Equal(3, files.Length); - Assert.True(files[0].EndsWith(fileName), files[0]); - Assert.True(files[1].EndsWith("_001.txt"), files[1]); - Assert.True(files[2].EndsWith("_002.txt"), files[2]); - } - - // Ensure the data was written through the wrapping GZipStream, by decompressing and comparing against - // what we wrote - for (var i = 0; i < files.Length; i++) - { - using (var textStream = new MemoryStream()) - { - using (var fs = System.IO.File.OpenRead(files[i])) - using (var decompressStream = new GZipStream(fs, CompressionMode.Decompress)) - { - decompressStream.CopyTo(textStream); - } - - textStream.Position = 0; - var lines = textStream.ReadAllLines(); - - Assert.Single(lines); - Assert.EndsWith(logEvents[i].MessageTemplate.Text, lines[0]); - } - } + decompressStream.CopyTo(textStream); } + + textStream.Position = 0; + var lines = textStream.ReadAllLines(); + + Assert.Single(lines); + Assert.EndsWith(logEvents[i].MessageTemplate.Text, lines[0]); } + } - [Fact] - public void IfTheLogFolderDoesNotExistItWillBeCreated() - { - var fileName = Some.String() + "-{Date}.txt"; - var temp = Some.TempFolderPath(); - var folder = Path.Combine(temp, Guid.NewGuid().ToString()); - var pathFormat = Path.Combine(folder, fileName); + [Fact] + public void IfTheLogFolderDoesNotExistItWillBeCreated() + { + var fileName = Some.String() + "-{Date}.txt"; + var temp = Some.TempFolderPath(); + var folder = Path.Combine(temp, Guid.NewGuid().ToString()); + var pathFormat = Path.Combine(folder, fileName); - Logger? log = null; + Logger? log = null; - try - { - log = new LoggerConfiguration() - .WriteTo.File(pathFormat, retainedFileCountLimit: 3, rollingInterval: RollingInterval.Day) - .CreateLogger(); + try + { + log = new LoggerConfiguration() + .WriteTo.File(pathFormat, retainedFileCountLimit: 3, rollingInterval: RollingInterval.Day) + .CreateLogger(); - log.Write(Some.InformationEvent()); + log.Write(Some.InformationEvent()); - Assert.True(Directory.Exists(folder)); - } - finally - { - log?.Dispose(); - Directory.Delete(temp, true); - } + Assert.True(Directory.Exists(folder)); } - - static void TestRollingEventSequence(params LogEvent[] events) + finally { - TestRollingEventSequence( - (pf, wt) => wt.File(pf, retainedFileCountLimit: null, rollingInterval: RollingInterval.Day), - events); + log?.Dispose(); + Directory.Delete(temp, true); } + } - static void TestRollingEventSequence( - Action configureFile, - IEnumerable events, - Action>? verifyWritten = null) - { - var fileName = Some.String() + "-.txt"; - var folder = Some.TempFolderPath(); - var pathFormat = Path.Combine(folder, fileName); + static void TestRollingEventSequence(params LogEvent[] events) + { + TestRollingEventSequence( + (pf, wt) => wt.File(pf, retainedFileCountLimit: null, rollingInterval: RollingInterval.Day), + events); + } + + static void TestRollingEventSequence( + Action configureFile, + IEnumerable events, + Action>? verifyWritten = null) + { + var fileName = Some.String() + "-.txt"; + var folder = Some.TempFolderPath(); + var pathFormat = Path.Combine(folder, fileName); - var config = new LoggerConfiguration(); - configureFile(pathFormat, config.WriteTo); - var log = config.CreateLogger(); + var config = new LoggerConfiguration(); + configureFile(pathFormat, config.WriteTo); + var log = config.CreateLogger(); - var verified = new List(); + var verified = new List(); - try + try + { + foreach (var @event in events) { - foreach (var @event in events) - { - Clock.SetTestDateTimeNow(@event.Timestamp.DateTime); - log.Write(@event); + Clock.SetTestDateTimeNow(@event.Timestamp.DateTime); + log.Write(@event); - var expected = pathFormat.Replace(".txt", @event.Timestamp.ToString("yyyyMMdd") + ".txt"); - Assert.True(System.IO.File.Exists(expected)); + var expected = pathFormat.Replace(".txt", @event.Timestamp.ToString("yyyyMMdd") + ".txt"); + Assert.True(System.IO.File.Exists(expected)); - verified.Add(expected); - } - } - finally - { - log.Dispose(); - verifyWritten?.Invoke(verified); - Directory.Delete(folder, true); + verified.Add(expected); } } + finally + { + log.Dispose(); + verifyWritten?.Invoke(verified); + Directory.Delete(folder, true); + } } } diff --git a/test/Serilog.Sinks.File.Tests/RollingIntervalExtensionsTests.cs b/test/Serilog.Sinks.File.Tests/RollingIntervalExtensionsTests.cs index 404d5b4..7572067 100644 --- a/test/Serilog.Sinks.File.Tests/RollingIntervalExtensionsTests.cs +++ b/test/Serilog.Sinks.File.Tests/RollingIntervalExtensionsTests.cs @@ -1,34 +1,32 @@ -using System; -using Xunit; +using Xunit; -namespace Serilog.Sinks.File.Tests +namespace Serilog.Sinks.File.Tests; + +public class RollingIntervalExtensionsTests { - public class RollingIntervalExtensionsTests - { - public static object?[][] IntervalInstantCurrentNextCheckpoint => new[] - { - new object?[]{ RollingInterval.Infinite, new DateTime(2018, 01, 01), null, null }, - new object?[]{ RollingInterval.Year, new DateTime(2018, 01, 01), new DateTime(2018, 01, 01), new DateTime(2019, 01, 01) }, - new object?[]{ RollingInterval.Year, new DateTime(2018, 06, 01), new DateTime(2018, 01, 01), new DateTime(2019, 01, 01) }, - new object?[]{ RollingInterval.Month, new DateTime(2018, 01, 01), new DateTime(2018, 01, 01), new DateTime(2018, 02, 01) }, - new object?[]{ RollingInterval.Month, new DateTime(2018, 01, 14), new DateTime(2018, 01, 01), new DateTime(2018, 02, 01) }, - new object?[]{ RollingInterval.Day, new DateTime(2018, 01, 01), new DateTime(2018, 01, 01), new DateTime(2018, 01, 02) }, - new object?[]{ RollingInterval.Day, new DateTime(2018, 01, 01, 12, 0, 0), new DateTime(2018, 01, 01), new DateTime(2018, 01, 02) }, - new object?[]{ RollingInterval.Hour, new DateTime(2018, 01, 01, 0, 0, 0), new DateTime(2018, 01, 01), new DateTime(2018, 01, 01, 1, 0, 0) }, - new object?[]{ RollingInterval.Hour, new DateTime(2018, 01, 01, 0, 30, 0), new DateTime(2018, 01, 01), new DateTime(2018, 01, 01, 1, 0, 0) }, - new object?[]{ RollingInterval.Minute, new DateTime(2018, 01, 01, 0, 0, 0), new DateTime(2018, 01, 01), new DateTime(2018, 01, 01, 0, 1, 0) }, - new object?[]{ RollingInterval.Minute, new DateTime(2018, 01, 01, 0, 0, 30), new DateTime(2018, 01, 01), new DateTime(2018, 01, 01, 0, 1, 0) } - }; + public static object?[][] IntervalInstantCurrentNextCheckpoint => + [ + [RollingInterval.Infinite, new DateTime(2018, 01, 01), null, null], + [RollingInterval.Year, new DateTime(2018, 01, 01), new DateTime(2018, 01, 01), new DateTime(2019, 01, 01)], + [RollingInterval.Year, new DateTime(2018, 06, 01), new DateTime(2018, 01, 01), new DateTime(2019, 01, 01)], + [RollingInterval.Month, new DateTime(2018, 01, 01), new DateTime(2018, 01, 01), new DateTime(2018, 02, 01)], + [RollingInterval.Month, new DateTime(2018, 01, 14), new DateTime(2018, 01, 01), new DateTime(2018, 02, 01)], + [RollingInterval.Day, new DateTime(2018, 01, 01), new DateTime(2018, 01, 01), new DateTime(2018, 01, 02)], + [RollingInterval.Day, new DateTime(2018, 01, 01, 12, 0, 0), new DateTime(2018, 01, 01), new DateTime(2018, 01, 02)], + [RollingInterval.Hour, new DateTime(2018, 01, 01, 0, 0, 0), new DateTime(2018, 01, 01), new DateTime(2018, 01, 01, 1, 0, 0)], + [RollingInterval.Hour, new DateTime(2018, 01, 01, 0, 30, 0), new DateTime(2018, 01, 01), new DateTime(2018, 01, 01, 1, 0, 0)], + [RollingInterval.Minute, new DateTime(2018, 01, 01, 0, 0, 0), new DateTime(2018, 01, 01), new DateTime(2018, 01, 01, 0, 1, 0)], + [RollingInterval.Minute, new DateTime(2018, 01, 01, 0, 0, 30), new DateTime(2018, 01, 01), new DateTime(2018, 01, 01, 0, 1, 0)] + ]; - [Theory] - [MemberData(nameof(IntervalInstantCurrentNextCheckpoint))] - public void NextIntervalTests(RollingInterval interval, DateTime instant, DateTime? currentCheckpoint, DateTime? nextCheckpoint) - { - var current = interval.GetCurrentCheckpoint(instant); - Assert.Equal(currentCheckpoint, current); + [Theory] + [MemberData(nameof(IntervalInstantCurrentNextCheckpoint))] + public void NextIntervalTests(RollingInterval interval, DateTime instant, DateTime? currentCheckpoint, DateTime? nextCheckpoint) + { + var current = interval.GetCurrentCheckpoint(instant); + Assert.Equal(currentCheckpoint, current); - var next = interval.GetNextCheckpoint(instant); - Assert.Equal(nextCheckpoint, next); - } + var next = interval.GetNextCheckpoint(instant); + Assert.Equal(nextCheckpoint, next); } } 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 90ef89d..9ad3db7 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,7 @@ - + - - net48;net5.0 - 8.0 - enable - true - Serilog.Sinks.File.Tests - ../../assets/Serilog.snk - true - true + net48;net8.0 true @@ -18,16 +10,12 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - + diff --git a/test/Serilog.Sinks.File.Tests/SharedFileSinkTests.cs b/test/Serilog.Sinks.File.Tests/SharedFileSinkTests.cs index 565be9b..dff0a0d 100644 --- a/test/Serilog.Sinks.File.Tests/SharedFileSinkTests.cs +++ b/test/Serilog.Sinks.File.Tests/SharedFileSinkTests.cs @@ -1,103 +1,93 @@ -using System.IO; -using Xunit; +using Xunit; using Serilog.Formatting.Json; using Serilog.Sinks.File.Tests.Support; #pragma warning disable 618 -namespace Serilog.Sinks.File.Tests +namespace Serilog.Sinks.File.Tests; + +public class SharedFileSinkTests { - public class SharedFileSinkTests + [Fact] + public void FileIsWrittenIfNonexistent() { - [Fact] - public void FileIsWrittenIfNonexistent() + using var tmp = TempFolder.ForCaller(); + var nonexistent = tmp.AllocateFilename("txt"); + var evt = Some.LogEvent("Hello, world!"); + + using (var sink = new SharedFileSink(nonexistent, new JsonFormatter(), null)) { - using (var tmp = TempFolder.ForCaller()) - { - var nonexistent = tmp.AllocateFilename("txt"); - var evt = Some.LogEvent("Hello, world!"); + sink.Emit(evt); + } - using (var sink = new SharedFileSink(nonexistent, new JsonFormatter(), null)) - { - sink.Emit(evt); - } + var lines = System.IO.File.ReadAllLines(nonexistent); + Assert.Contains("Hello, world!", lines[0]); + } - var lines = System.IO.File.ReadAllLines(nonexistent); - Assert.Contains("Hello, world!", lines[0]); - } - } + [Fact] + public void FileIsAppendedToWhenAlreadyCreated() + { + using var tmp = TempFolder.ForCaller(); + var path = tmp.AllocateFilename("txt"); + var evt = Some.LogEvent("Hello, world!"); - [Fact] - public void FileIsAppendedToWhenAlreadyCreated() + using (var sink = new SharedFileSink(path, new JsonFormatter(), null)) { - using (var tmp = TempFolder.ForCaller()) - { - var path = tmp.AllocateFilename("txt"); - var evt = Some.LogEvent("Hello, world!"); - - using (var sink = new SharedFileSink(path, new JsonFormatter(), null)) - { - sink.Emit(evt); - } - - using (var sink = new SharedFileSink(path, new JsonFormatter(), null)) - { - sink.Emit(evt); - } - - var lines = System.IO.File.ReadAllLines(path); - Assert.Contains("Hello, world!", lines[0]); - Assert.Contains("Hello, world!", lines[1]); - } + sink.Emit(evt); } - [Fact] - public void WhenLimitIsSpecifiedFileSizeIsRestricted() + using (var sink = new SharedFileSink(path, new JsonFormatter(), null)) { - const int maxBytes = 5000; - const int eventsToLimit = 10; + sink.Emit(evt); + } + + var lines = System.IO.File.ReadAllLines(path); + Assert.Contains("Hello, world!", lines[0]); + Assert.Contains("Hello, world!", lines[1]); + } + + [Fact] + public void WhenLimitIsSpecifiedFileSizeIsRestricted() + { + const int maxBytes = 5000; + const int eventsToLimit = 10; - using (var tmp = TempFolder.ForCaller()) + using var tmp = TempFolder.ForCaller(); + var path = tmp.AllocateFilename("txt"); + var evt = Some.LogEvent(new string('n', maxBytes / eventsToLimit)); + + using (var sink = new SharedFileSink(path, new JsonFormatter(), maxBytes)) + { + for (var i = 0; i < eventsToLimit * 2; i++) { - var path = tmp.AllocateFilename("txt"); - var evt = Some.LogEvent(new string('n', maxBytes / eventsToLimit)); - - using (var sink = new SharedFileSink(path, new JsonFormatter(), maxBytes)) - { - for (var i = 0; i < eventsToLimit * 2; i++) - { - sink.Emit(evt); - } - } - - var size = new FileInfo(path).Length; - Assert.True(size > maxBytes); - Assert.True(size < maxBytes * 2); + sink.Emit(evt); } } - [Fact] - public void WhenLimitIsNotSpecifiedFileSizeIsNotRestricted() - { - const int maxBytes = 5000; - const int eventsToLimit = 10; + var size = new FileInfo(path).Length; + Assert.True(size > maxBytes); + Assert.True(size < maxBytes * 2); + } + + [Fact] + public void WhenLimitIsNotSpecifiedFileSizeIsNotRestricted() + { + const int maxBytes = 5000; + const int eventsToLimit = 10; - using (var tmp = TempFolder.ForCaller()) + using var tmp = TempFolder.ForCaller(); + var path = tmp.AllocateFilename("txt"); + var evt = Some.LogEvent(new string('n', maxBytes / eventsToLimit)); + + using (var sink = new SharedFileSink(path, new JsonFormatter(), null)) + { + for (var i = 0; i < eventsToLimit * 2; i++) { - var path = tmp.AllocateFilename("txt"); - var evt = Some.LogEvent(new string('n', maxBytes / eventsToLimit)); - - using (var sink = new SharedFileSink(path, new JsonFormatter(), null)) - { - for (var i = 0; i < eventsToLimit * 2; i++) - { - sink.Emit(evt); - } - } - - var size = new FileInfo(path).Length; - Assert.True(size > maxBytes * 2); + sink.Emit(evt); } } + + var size = new FileInfo(path).Length; + Assert.True(size > maxBytes * 2); } } diff --git a/test/Serilog.Sinks.File.Tests/Support/ArchiveOldLogsHook.cs b/test/Serilog.Sinks.File.Tests/Support/ArchiveOldLogsHook.cs index 96dcb8c..c8b93a8 100644 --- a/test/Serilog.Sinks.File.Tests/Support/ArchiveOldLogsHook.cs +++ b/test/Serilog.Sinks.File.Tests/Support/ArchiveOldLogsHook.cs @@ -1,35 +1,30 @@ -using System; -using System.IO; -using System.Text; +namespace Serilog.Sinks.File.Tests.Support; -namespace Serilog.Sinks.File.Tests.Support +class ArchiveOldLogsHook : FileLifecycleHooks { - internal class ArchiveOldLogsHook : FileLifecycleHooks + readonly string _relativeArchiveDir; + + public ArchiveOldLogsHook(string relativeArchiveDir) { - private readonly string _relativeArchiveDir; + _relativeArchiveDir = relativeArchiveDir; + } - public ArchiveOldLogsHook(string relativeArchiveDir) - { - _relativeArchiveDir = relativeArchiveDir; - } + public override void OnFileDeleting(string path) + { + base.OnFileDeleting(path); + var newFile = AddTopDirectory(path, _relativeArchiveDir, true); + System.IO.File.Copy(path, newFile, false); + } - public override void OnFileDeleting(string path) - { - base.OnFileDeleting(path); - var newFile = AddTopDirectory(path, _relativeArchiveDir, true); - System.IO.File.Copy(path, newFile, false); - } + public static string AddTopDirectory(string path, string directoryToAdd, bool createOnNonExist = false) + { + string file = Path.GetFileName(path); + string directory = Path.Combine(Path.GetDirectoryName(path) ?? throw new InvalidOperationException(), directoryToAdd); - public static string AddTopDirectory(string path, string directoryToAdd, bool createOnNonExist = false) + if (createOnNonExist && !Directory.Exists(directory)) { - string file = Path.GetFileName(path); - string directory = Path.Combine(Path.GetDirectoryName(path) ?? throw new InvalidOperationException(), directoryToAdd); - - if (createOnNonExist && !Directory.Exists(directory)) - { - Directory.CreateDirectory(directory); - } - return Path.Combine(directory, file); + Directory.CreateDirectory(directory); } + return Path.Combine(directory, file); } } diff --git a/test/Serilog.Sinks.File.Tests/Support/CaptureFilePathHook.cs b/test/Serilog.Sinks.File.Tests/Support/CaptureFilePathHook.cs index 65857d1..14fa452 100644 --- a/test/Serilog.Sinks.File.Tests/Support/CaptureFilePathHook.cs +++ b/test/Serilog.Sinks.File.Tests/Support/CaptureFilePathHook.cs @@ -1,20 +1,18 @@ -using System.IO; -using System.Text; +using System.Text; -namespace Serilog.Sinks.File.Tests.Support +namespace Serilog.Sinks.File.Tests.Support; + +/// +/// +/// Demonstrates the use of , by capturing the log file path +/// +class CaptureFilePathHook : FileLifecycleHooks { - /// - /// - /// Demonstrates the use of , by capturing the log file path - /// - class CaptureFilePathHook : FileLifecycleHooks - { - public string? Path { get; private set; } + public string? Path { get; private set; } - public override Stream OnFileOpened(string path, Stream _, Encoding __) - { - Path = path; - return base.OnFileOpened(path, _, __); - } + public override Stream OnFileOpened(string path, Stream _, Encoding __) + { + Path = path; + return base.OnFileOpened(path, _, __); } } diff --git a/test/Serilog.Sinks.File.Tests/Support/CollectingSink.cs b/test/Serilog.Sinks.File.Tests/Support/CollectingSink.cs index 244ae5c..323aba8 100644 --- a/test/Serilog.Sinks.File.Tests/Support/CollectingSink.cs +++ b/test/Serilog.Sinks.File.Tests/Support/CollectingSink.cs @@ -1,21 +1,18 @@ -using System.Collections.Generic; -using System.Linq; -using Serilog.Core; +using Serilog.Core; using Serilog.Events; -namespace Serilog.Sinks.File.Tests.Support +namespace Serilog.Sinks.File.Tests.Support; + +class CollectingSink : ILogEventSink { - class CollectingSink : ILogEventSink - { - readonly List _events = new List(); + readonly List _events = new(); - public List Events { get { return _events; } } + public List Events { get { return _events; } } - public LogEvent SingleEvent { get { return _events.Single(); } } - - public void Emit(LogEvent logEvent) - { - _events.Add(logEvent); - } + public LogEvent SingleEvent { get { return _events.Single(); } } + + public void Emit(LogEvent logEvent) + { + _events.Add(logEvent); } } diff --git a/test/Serilog.Sinks.File.Tests/Support/DelegateDisposable.cs b/test/Serilog.Sinks.File.Tests/Support/DelegateDisposable.cs index 3ac9974..81a480c 100644 --- a/test/Serilog.Sinks.File.Tests/Support/DelegateDisposable.cs +++ b/test/Serilog.Sinks.File.Tests/Support/DelegateDisposable.cs @@ -1,24 +1,21 @@ -using System; +namespace Serilog.Sinks.File.Tests.Support; -namespace Serilog.Sinks.File.Tests.Support +public class DelegateDisposable : IDisposable { - public class DelegateDisposable : IDisposable - { - private readonly Action _disposeAction; - private bool _disposed; + readonly Action _disposeAction; + bool _disposed; - public DelegateDisposable(Action disposeAction) - { - _disposeAction = disposeAction; - } + public DelegateDisposable(Action disposeAction) + { + _disposeAction = disposeAction; + } - public void Dispose() - { - if (_disposed) - return; + public void Dispose() + { + if (_disposed) + return; - _disposeAction(); - _disposed = true; - } + _disposeAction(); + _disposed = true; } } diff --git a/test/Serilog.Sinks.File.Tests/Support/DelegatingEnricher.cs b/test/Serilog.Sinks.File.Tests/Support/DelegatingEnricher.cs index 0a480fb..67b9758 100644 --- a/test/Serilog.Sinks.File.Tests/Support/DelegatingEnricher.cs +++ b/test/Serilog.Sinks.File.Tests/Support/DelegatingEnricher.cs @@ -1,22 +1,20 @@ -using System; -using Serilog.Core; +using Serilog.Core; using Serilog.Events; -namespace Serilog.Sinks.File.Tests.Support +namespace Serilog.Sinks.File.Tests.Support; + +class DelegatingEnricher : ILogEventEnricher { - class DelegatingEnricher : ILogEventEnricher - { - readonly Action _enrich; + readonly Action _enrich; - public DelegatingEnricher(Action enrich) - { - if (enrich == null) throw new ArgumentNullException(nameof(enrich)); - _enrich = enrich; - } + public DelegatingEnricher(Action enrich) + { + if (enrich == null) throw new ArgumentNullException(nameof(enrich)); + _enrich = enrich; + } - public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory) - { - _enrich(logEvent, propertyFactory); - } + public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory) + { + _enrich(logEvent, propertyFactory); } } diff --git a/test/Serilog.Sinks.File.Tests/Support/DelegatingSink.cs b/test/Serilog.Sinks.File.Tests/Support/DelegatingSink.cs index 12b7f3d..aee1615 100644 --- a/test/Serilog.Sinks.File.Tests/Support/DelegatingSink.cs +++ b/test/Serilog.Sinks.File.Tests/Support/DelegatingSink.cs @@ -1,33 +1,31 @@ -using System; -using Serilog.Core; +using Serilog.Core; using Serilog.Events; -namespace Serilog.Sinks.File.Tests.Support +namespace Serilog.Sinks.File.Tests.Support; + +public class DelegatingSink : ILogEventSink { - public class DelegatingSink : ILogEventSink - { - readonly Action _write; + readonly Action _write; - public DelegatingSink(Action write) - { - if (write == null) throw new ArgumentNullException(nameof(write)); - _write = write; - } + public DelegatingSink(Action write) + { + if (write == null) throw new ArgumentNullException(nameof(write)); + _write = write; + } - public void Emit(LogEvent logEvent) - { - _write(logEvent); - } + public void Emit(LogEvent logEvent) + { + _write(logEvent); + } - public static LogEvent GetLogEvent(Action writeAction) - { - LogEvent? result = null; - var l = new LoggerConfiguration() - .WriteTo.Sink(new DelegatingSink(le => result = le)) - .CreateLogger(); + public static LogEvent GetLogEvent(Action writeAction) + { + LogEvent? result = null; + var l = new LoggerConfiguration() + .WriteTo.Sink(new DelegatingSink(le => result = le)) + .CreateLogger(); - writeAction(l); - return result!; - } + writeAction(l); + return result!; } } diff --git a/test/Serilog.Sinks.File.Tests/Support/DisposableLogger.cs b/test/Serilog.Sinks.File.Tests/Support/DisposableLogger.cs deleted file mode 100644 index befcbd4..0000000 --- a/test/Serilog.Sinks.File.Tests/Support/DisposableLogger.cs +++ /dev/null @@ -1,422 +0,0 @@ -using System; -using System.Collections.Generic; -using Serilog.Core; -using Serilog.Events; - -namespace Serilog.Sinks.File.Tests.Support -{ - public class DisposableLogger : ILogger, IDisposable - { - public bool Disposed { get; set; } - - public void Dispose() - { - Disposed = true; - } - - public ILogger ForContext(ILogEventEnricher enricher) - { - throw new NotImplementedException(); - } - - public ILogger ForContext(IEnumerable enrichers) - { - throw new NotImplementedException(); - } - - public ILogger ForContext(string propertyName, object value, bool destructureObjects = false) - { - throw new NotImplementedException(); - } - - public ILogger ForContext() - { - throw new NotImplementedException(); - } - - public ILogger ForContext(Type source) - { - throw new NotImplementedException(); - } - - public void Write(LogEvent logEvent) - { - throw new NotImplementedException(); - } - - public void Write(LogEventLevel level, string messageTemplate) - { - throw new NotImplementedException(); - } - - public void Write(LogEventLevel level, string messageTemplate, T propertyValue) - { - throw new NotImplementedException(); - } - - public void Write(LogEventLevel level, string messageTemplate, T0 propertyValue0, T1 propertyValue1) - { - throw new NotImplementedException(); - } - - public void Write(LogEventLevel level, string messageTemplate, T0 propertyValue0, T1 propertyValue1, - T2 propertyValue2) - { - throw new NotImplementedException(); - } - - public void Write(LogEventLevel level, string messageTemplate, params object[] propertyValues) - { - throw new NotImplementedException(); - } - - public void Write(LogEventLevel level, Exception exception, string messageTemplate) - { - throw new NotImplementedException(); - } - - public void Write(LogEventLevel level, Exception exception, string messageTemplate, T propertyValue) - { - throw new NotImplementedException(); - } - - public void Write(LogEventLevel level, Exception exception, string messageTemplate, T0 propertyValue0, - T1 propertyValue1) - { - throw new NotImplementedException(); - } - - public void Write(LogEventLevel level, Exception exception, string messageTemplate, T0 propertyValue0, - T1 propertyValue1, T2 propertyValue2) - { - throw new NotImplementedException(); - } - - public void Write(LogEventLevel level, Exception exception, string messageTemplate, params object[] propertyValues) - { - throw new NotImplementedException(); - } - - public bool IsEnabled(LogEventLevel level) - { - throw new NotImplementedException(); - } - - public void Verbose(string messageTemplate) - { - throw new NotImplementedException(); - } - - public void Verbose(string messageTemplate, T propertyValue) - { - throw new NotImplementedException(); - } - - public void Verbose(string messageTemplate, T0 propertyValue0, T1 propertyValue1) - { - throw new NotImplementedException(); - } - - public void Verbose(string messageTemplate, T0 propertyValue0, T1 propertyValue1, T2 propertyValue2) - { - throw new NotImplementedException(); - } - - public void Verbose(string messageTemplate, params object[] propertyValues) - { - throw new NotImplementedException(); - } - - public void Verbose(Exception exception, string messageTemplate) - { - throw new NotImplementedException(); - } - - public void Verbose(Exception exception, string messageTemplate, T propertyValue) - { - throw new NotImplementedException(); - } - - public void Verbose(Exception exception, string messageTemplate, T0 propertyValue0, T1 propertyValue1) - { - throw new NotImplementedException(); - } - - public void Verbose(Exception exception, string messageTemplate, T0 propertyValue0, T1 propertyValue1, - T2 propertyValue2) - { - throw new NotImplementedException(); - } - - public void Verbose(Exception exception, string messageTemplate, params object[] propertyValues) - { - throw new NotImplementedException(); - } - - public void Debug(string messageTemplate) - { - throw new NotImplementedException(); - } - - public void Debug(string messageTemplate, T propertyValue) - { - throw new NotImplementedException(); - } - - public void Debug(string messageTemplate, T0 propertyValue0, T1 propertyValue1) - { - throw new NotImplementedException(); - } - - public void Debug(string messageTemplate, T0 propertyValue0, T1 propertyValue1, T2 propertyValue2) - { - throw new NotImplementedException(); - } - - public void Debug(string messageTemplate, params object[] propertyValues) - { - throw new NotImplementedException(); - } - - public void Debug(Exception exception, string messageTemplate) - { - throw new NotImplementedException(); - } - - public void Debug(Exception exception, string messageTemplate, T propertyValue) - { - throw new NotImplementedException(); - } - - public void Debug(Exception exception, string messageTemplate, T0 propertyValue0, T1 propertyValue1) - { - throw new NotImplementedException(); - } - - public void Debug(Exception exception, string messageTemplate, T0 propertyValue0, T1 propertyValue1, - T2 propertyValue2) - { - throw new NotImplementedException(); - } - - public void Debug(Exception exception, string messageTemplate, params object[] propertyValues) - { - throw new NotImplementedException(); - } - - public void Information(string messageTemplate) - { - throw new NotImplementedException(); - } - - public void Information(string messageTemplate, T propertyValue) - { - throw new NotImplementedException(); - } - - public void Information(string messageTemplate, T0 propertyValue0, T1 propertyValue1) - { - throw new NotImplementedException(); - } - - public void Information(string messageTemplate, T0 propertyValue0, T1 propertyValue1, T2 propertyValue2) - { - throw new NotImplementedException(); - } - - public void Information(string messageTemplate, params object[] propertyValues) - { - throw new NotImplementedException(); - } - - public void Information(Exception exception, string messageTemplate) - { - throw new NotImplementedException(); - } - - public void Information(Exception exception, string messageTemplate, T propertyValue) - { - throw new NotImplementedException(); - } - - public void Information(Exception exception, string messageTemplate, T0 propertyValue0, T1 propertyValue1) - { - throw new NotImplementedException(); - } - - public void Information(Exception exception, string messageTemplate, T0 propertyValue0, T1 propertyValue1, - T2 propertyValue2) - { - throw new NotImplementedException(); - } - - public void Information(Exception exception, string messageTemplate, params object[] propertyValues) - { - throw new NotImplementedException(); - } - - public void Warning(string messageTemplate) - { - throw new NotImplementedException(); - } - - public void Warning(string messageTemplate, T propertyValue) - { - throw new NotImplementedException(); - } - - public void Warning(string messageTemplate, T0 propertyValue0, T1 propertyValue1) - { - throw new NotImplementedException(); - } - - public void Warning(string messageTemplate, T0 propertyValue0, T1 propertyValue1, T2 propertyValue2) - { - throw new NotImplementedException(); - } - - public void Warning(string messageTemplate, params object[] propertyValues) - { - throw new NotImplementedException(); - } - - public void Warning(Exception exception, string messageTemplate) - { - throw new NotImplementedException(); - } - - public void Warning(Exception exception, string messageTemplate, T propertyValue) - { - throw new NotImplementedException(); - } - - public void Warning(Exception exception, string messageTemplate, T0 propertyValue0, T1 propertyValue1) - { - throw new NotImplementedException(); - } - - public void Warning(Exception exception, string messageTemplate, T0 propertyValue0, T1 propertyValue1, - T2 propertyValue2) - { - throw new NotImplementedException(); - } - - public void Warning(Exception exception, string messageTemplate, params object[] propertyValues) - { - throw new NotImplementedException(); - } - - public void Error(string messageTemplate) - { - throw new NotImplementedException(); - } - - public void Error(string messageTemplate, T propertyValue) - { - throw new NotImplementedException(); - } - - public void Error(string messageTemplate, T0 propertyValue0, T1 propertyValue1) - { - throw new NotImplementedException(); - } - - public void Error(string messageTemplate, T0 propertyValue0, T1 propertyValue1, T2 propertyValue2) - { - throw new NotImplementedException(); - } - - public void Error(string messageTemplate, params object[] propertyValues) - { - throw new NotImplementedException(); - } - - public void Error(Exception exception, string messageTemplate) - { - throw new NotImplementedException(); - } - - public void Error(Exception exception, string messageTemplate, T propertyValue) - { - throw new NotImplementedException(); - } - - public void Error(Exception exception, string messageTemplate, T0 propertyValue0, T1 propertyValue1) - { - throw new NotImplementedException(); - } - - public void Error(Exception exception, string messageTemplate, T0 propertyValue0, T1 propertyValue1, - T2 propertyValue2) - { - throw new NotImplementedException(); - } - - public void Error(Exception exception, string messageTemplate, params object[] propertyValues) - { - throw new NotImplementedException(); - } - - public void Fatal(string messageTemplate) - { - throw new NotImplementedException(); - } - - public void Fatal(string messageTemplate, T propertyValue) - { - throw new NotImplementedException(); - } - - public void Fatal(string messageTemplate, T0 propertyValue0, T1 propertyValue1) - { - throw new NotImplementedException(); - } - - public void Fatal(string messageTemplate, T0 propertyValue0, T1 propertyValue1, T2 propertyValue2) - { - throw new NotImplementedException(); - } - - public void Fatal(string messageTemplate, params object[] propertyValues) - { - throw new NotImplementedException(); - } - - public void Fatal(Exception exception, string messageTemplate) - { - throw new NotImplementedException(); - } - - public void Fatal(Exception exception, string messageTemplate, T propertyValue) - { - throw new NotImplementedException(); - } - - public void Fatal(Exception exception, string messageTemplate, T0 propertyValue0, T1 propertyValue1) - { - throw new NotImplementedException(); - } - - public void Fatal(Exception exception, string messageTemplate, T0 propertyValue0, T1 propertyValue1, - T2 propertyValue2) - { - throw new NotImplementedException(); - } - - public void Fatal(Exception exception, string messageTemplate, params object[] propertyValues) - { - throw new NotImplementedException(); - } - - public bool BindMessageTemplate(string messageTemplate, object[] propertyValues, out MessageTemplate parsedTemplate, - out IEnumerable boundProperties) - { - throw new NotImplementedException(); - } - - public bool BindProperty(string propertyName, object value, bool destructureObjects, out LogEventProperty property) - { - throw new NotImplementedException(); - } - } -} diff --git a/test/Serilog.Sinks.File.Tests/Support/DisposeTrackingSink.cs b/test/Serilog.Sinks.File.Tests/Support/DisposeTrackingSink.cs index 29cac56..c4e2559 100644 --- a/test/Serilog.Sinks.File.Tests/Support/DisposeTrackingSink.cs +++ b/test/Serilog.Sinks.File.Tests/Support/DisposeTrackingSink.cs @@ -1,20 +1,18 @@ -using System; -using Serilog.Core; +using Serilog.Core; using Serilog.Events; -namespace Serilog.Sinks.File.Tests.Support +namespace Serilog.Sinks.File.Tests.Support; + +class DisposeTrackingSink : ILogEventSink, IDisposable { - class DisposeTrackingSink : ILogEventSink, IDisposable - { - public bool IsDisposed { get; set; } + public bool IsDisposed { get; set; } - public void Emit(LogEvent logEvent) - { - } + public void Emit(LogEvent logEvent) + { + } - public void Dispose() - { - IsDisposed = true; - } + public void Dispose() + { + IsDisposed = true; } } diff --git a/test/Serilog.Sinks.File.Tests/Support/Extensions.cs b/test/Serilog.Sinks.File.Tests/Support/Extensions.cs index a048353..db8781b 100644 --- a/test/Serilog.Sinks.File.Tests/Support/Extensions.cs +++ b/test/Serilog.Sinks.File.Tests/Support/Extensions.cs @@ -1,30 +1,25 @@ -using System.Collections.Generic; -using System.IO; -using Serilog.Events; +using Serilog.Events; -namespace Serilog.Sinks.File.Tests.Support +namespace Serilog.Sinks.File.Tests.Support; + +public static class Extensions { - public static class Extensions + public static object? LiteralValue(this LogEventPropertyValue @this) { - public static object LiteralValue(this LogEventPropertyValue @this) - { - return ((ScalarValue)@this).Value; - } - - public static List ReadAllLines(this Stream @this) - { - var lines = new List(); + return ((ScalarValue)@this).Value; + } - using (var reader = new StreamReader(@this)) - { - string? line; - while ((line = reader.ReadLine()) != null) - { - lines.Add(line); - } - } + public static List ReadAllLines(this Stream @this) + { + var lines = new List(); - return lines; + using var reader = new StreamReader(@this); + string? line; + while ((line = reader.ReadLine()) != null) + { + lines.Add(line); } + + return lines; } } diff --git a/test/Serilog.Sinks.File.Tests/Support/FileHeaderWriter.cs b/test/Serilog.Sinks.File.Tests/Support/FileHeaderWriter.cs index ae90604..9e92294 100644 --- a/test/Serilog.Sinks.File.Tests/Support/FileHeaderWriter.cs +++ b/test/Serilog.Sinks.File.Tests/Support/FileHeaderWriter.cs @@ -1,28 +1,26 @@ -using System.IO; -using System.Text; +using System.Text; -namespace Serilog.Sinks.File.Tests.Support +namespace Serilog.Sinks.File.Tests.Support; + +class FileHeaderWriter : FileLifecycleHooks { - class FileHeaderWriter : FileLifecycleHooks + public string Header { get; } + + public FileHeaderWriter(string header) { - public string Header { get; } + Header = header; + } - public FileHeaderWriter(string header) + public override Stream OnFileOpened(Stream underlyingStream, Encoding encoding) + { + if (underlyingStream.Length == 0) { - Header = header; + var writer = new StreamWriter(underlyingStream, encoding); + writer.WriteLine(Header); + writer.Flush(); + underlyingStream.Flush(); } - public override Stream OnFileOpened(Stream underlyingStream, Encoding encoding) - { - if (underlyingStream.Length == 0) - { - var writer = new StreamWriter(underlyingStream, encoding); - writer.WriteLine(Header); - writer.Flush(); - underlyingStream.Flush(); - } - - return base.OnFileOpened(underlyingStream, encoding); - } + return base.OnFileOpened(underlyingStream, encoding); } } diff --git a/test/Serilog.Sinks.File.Tests/Support/FixedOutputFormatter.cs b/test/Serilog.Sinks.File.Tests/Support/FixedOutputFormatter.cs index 24b70da..650a2e2 100644 --- a/test/Serilog.Sinks.File.Tests/Support/FixedOutputFormatter.cs +++ b/test/Serilog.Sinks.File.Tests/Support/FixedOutputFormatter.cs @@ -1,21 +1,19 @@ using Serilog.Formatting; using Serilog.Events; -using System.IO; -namespace Serilog.Tests.Support +namespace Serilog.Tests.Support; + +public class FixedOutputFormatter : ITextFormatter { - public class FixedOutputFormatter : ITextFormatter - { - string _substitutionText; + string _substitutionText; - public FixedOutputFormatter(string substitutionText) - { - _substitutionText = substitutionText; - } + public FixedOutputFormatter(string substitutionText) + { + _substitutionText = substitutionText; + } - public void Format(LogEvent logEvent, TextWriter output) - { - output.Write(_substitutionText); - } + public void Format(LogEvent logEvent, TextWriter output) + { + output.Write(_substitutionText); } } diff --git a/test/Serilog.Sinks.File.Tests/Support/GZipHooks.cs b/test/Serilog.Sinks.File.Tests/Support/GZipHooks.cs index 40a77bb..7191db1 100644 --- a/test/Serilog.Sinks.File.Tests/Support/GZipHooks.cs +++ b/test/Serilog.Sinks.File.Tests/Support/GZipHooks.cs @@ -1,26 +1,24 @@ -using System.IO; using System.IO.Compression; using System.Text; -namespace Serilog.Sinks.File.Tests.Support +namespace Serilog.Sinks.File.Tests.Support; + +/// +/// +/// Demonstrates the use of , by compressing log output using GZip +/// +public class GZipHooks : FileLifecycleHooks { - /// - /// - /// Demonstrates the use of , by compressing log output using GZip - /// - public class GZipHooks : FileLifecycleHooks - { - readonly int _bufferSize; + readonly int _bufferSize; - public GZipHooks(int bufferSize = 1024 * 32) - { - _bufferSize = bufferSize; - } + public GZipHooks(int bufferSize = 1024 * 32) + { + _bufferSize = bufferSize; + } - public override Stream OnFileOpened(Stream underlyingStream, Encoding _) - { - var compressStream = new GZipStream(underlyingStream, CompressionMode.Compress); - return new BufferedStream(compressStream, _bufferSize); - } + public override Stream OnFileOpened(Stream underlyingStream, Encoding _) + { + var compressStream = new GZipStream(underlyingStream, CompressionMode.Compress); + return new BufferedStream(compressStream, _bufferSize); } } diff --git a/test/Serilog.Sinks.File.Tests/Support/Some.cs b/test/Serilog.Sinks.File.Tests/Support/Some.cs index 4209102..05ddc11 100644 --- a/test/Serilog.Sinks.File.Tests/Support/Some.cs +++ b/test/Serilog.Sinks.File.Tests/Support/Some.cs @@ -1,102 +1,97 @@ -using System; -using System.IO; -using System.Linq; -using System.Threading; -using Serilog.Events; +using Serilog.Events; using Serilog.Parsing; using Xunit.Sdk; // ReSharper disable UnusedMember.Global -namespace Serilog.Sinks.File.Tests.Support +namespace Serilog.Sinks.File.Tests.Support; + +static class Some { - static class Some - { - static int _counter; + static int _counter; - public static int Int() - { - return Interlocked.Increment(ref _counter); - } + public static int Int() + { + return Interlocked.Increment(ref _counter); + } - public static decimal Decimal() - { - return Int() + 0.123m; - } + public static decimal Decimal() + { + return Int() + 0.123m; + } - public static string String(string? tag = null) - { - return (tag ?? "") + "__" + Int(); - } + public static string String(string? tag = null) + { + return (tag ?? "") + "__" + Int(); + } - public static TimeSpan TimeSpan() - { - return System.TimeSpan.FromMinutes(Int()); - } + public static TimeSpan TimeSpan() + { + return System.TimeSpan.FromMinutes(Int()); + } - public static DateTime Instant() - { - return new DateTime(2012, 10, 28) + TimeSpan(); - } + public static DateTime Instant() + { + return new DateTime(2012, 10, 28) + TimeSpan(); + } - public static DateTimeOffset OffsetInstant() - { - return new DateTimeOffset(Instant()); - } + public static DateTimeOffset OffsetInstant() + { + return new DateTimeOffset(Instant()); + } - public static LogEvent LogEvent(string messageTemplate, params object[] propertyValues) - { - var log = new LoggerConfiguration().CreateLogger(); + public static LogEvent LogEvent(string messageTemplate, params object[] propertyValues) + { + var log = new LoggerConfiguration().CreateLogger(); #pragma warning disable Serilog004 // Constant MessageTemplate verifier - if (!log.BindMessageTemplate(messageTemplate, propertyValues, out var template, out var 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."); - } - return new LogEvent(DateTimeOffset.Now, LogEventLevel.Information, null, template, properties); - } - - public static LogEvent LogEvent(DateTimeOffset? timestamp = null, LogEventLevel level = LogEventLevel.Information) { - return new LogEvent(timestamp ?? OffsetInstant(), level, - null, MessageTemplate(), Enumerable.Empty()); + throw new XunitException("Template could not be bound."); } + return new LogEvent(DateTimeOffset.Now, LogEventLevel.Information, null, template, properties); + } - public static LogEvent InformationEvent(DateTimeOffset? timestamp = null) - { - return LogEvent(timestamp); - } + public static LogEvent LogEvent(DateTimeOffset? timestamp = null, LogEventLevel level = LogEventLevel.Information) + { + return new LogEvent(timestamp ?? OffsetInstant(), level, + null, MessageTemplate(), Enumerable.Empty()); + } - public static LogEvent DebugEvent(DateTimeOffset? timestamp = null) - { - return LogEvent(timestamp, LogEventLevel.Debug); - } + public static LogEvent InformationEvent(DateTimeOffset? timestamp = null) + { + return LogEvent(timestamp); + } - public static LogEventProperty LogEventProperty() - { - return new LogEventProperty(String(), new ScalarValue(Int())); - } + public static LogEvent DebugEvent(DateTimeOffset? timestamp = null) + { + return LogEvent(timestamp, LogEventLevel.Debug); + } - public static string NonexistentTempFilePath() - { - return Path.Combine(Path.GetTempPath(), Guid.NewGuid() + ".txt"); - } + public static LogEventProperty LogEventProperty() + { + return new LogEventProperty(String(), new ScalarValue(Int())); + } - public static string TempFilePath() - { - return Path.GetTempFileName(); - } + public static string NonexistentTempFilePath() + { + return Path.Combine(Path.GetTempPath(), Guid.NewGuid() + ".txt"); + } - public static string TempFolderPath() - { - var dir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); - Directory.CreateDirectory(dir); - return dir; - } + public static string TempFilePath() + { + return Path.GetTempFileName(); + } - public static MessageTemplate MessageTemplate() - { - return new MessageTemplateParser().Parse(String()); - } + public static string TempFolderPath() + { + var dir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + Directory.CreateDirectory(dir); + return dir; + } + + public static MessageTemplate MessageTemplate() + { + return new MessageTemplateParser().Parse(String()); } } diff --git a/test/Serilog.Sinks.File.Tests/Support/TempFolder.cs b/test/Serilog.Sinks.File.Tests/Support/TempFolder.cs index 29682e0..10d751e 100644 --- a/test/Serilog.Sinks.File.Tests/Support/TempFolder.cs +++ b/test/Serilog.Sinks.File.Tests/Support/TempFolder.cs @@ -1,55 +1,52 @@ -using System; -using System.Diagnostics; -using System.IO; +using System.Diagnostics; using System.Runtime.CompilerServices; -namespace Serilog.Sinks.File.Tests.Support +namespace Serilog.Sinks.File.Tests.Support; + +class TempFolder : IDisposable { - class TempFolder : IDisposable - { - static readonly Guid Session = Guid.NewGuid(); + static readonly Guid Session = Guid.NewGuid(); - readonly string _tempFolder; + readonly string _tempFolder; - public TempFolder(string? name = null) - { - _tempFolder = System.IO.Path.Combine( - Environment.GetEnvironmentVariable("TMP") ?? Environment.GetEnvironmentVariable("TMPDIR") ?? "/tmp", - "Serilog.Sinks.File.Tests", - Session.ToString("n"), - name ?? Guid.NewGuid().ToString("n")); + public TempFolder(string? name = null) + { + _tempFolder = System.IO.Path.Combine( + Environment.GetEnvironmentVariable("TMP") ?? Environment.GetEnvironmentVariable("TMPDIR") ?? "/tmp", + "Serilog.Sinks.File.Tests", + Session.ToString("n"), + name ?? Guid.NewGuid().ToString("n")); - Directory.CreateDirectory(_tempFolder); - } + Directory.CreateDirectory(_tempFolder); + } - public string Path => _tempFolder; + public string Path => _tempFolder; - public void Dispose() + public void Dispose() + { + try { - try - { - if (Directory.Exists(_tempFolder)) - Directory.Delete(_tempFolder, true); - } - catch (Exception ex) - { - Debug.WriteLine(ex); - } + if (Directory.Exists(_tempFolder)) + Directory.Delete(_tempFolder, true); } - - public static TempFolder ForCaller([CallerMemberName] string? caller = null, [CallerFilePath] string sourceFileName = "") + catch (Exception ex) { - if (caller == null) throw new ArgumentNullException(nameof(caller)); - if (sourceFileName == null) throw new ArgumentNullException(nameof(sourceFileName)); - - var folderName = System.IO.Path.GetFileNameWithoutExtension(sourceFileName) + "_" + caller; - - return new TempFolder(folderName); + Debug.WriteLine(ex); } + } - public string AllocateFilename(string? ext = null) - { - return System.IO.Path.Combine(Path, Guid.NewGuid().ToString("n") + "." + (ext ?? "tmp")); - } + 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)); + + var folderName = System.IO.Path.GetFileNameWithoutExtension(sourceFileName) + "_" + caller; + + return new TempFolder(folderName); + } + + 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/ThrowingLogEventFormatter.cs b/test/Serilog.Sinks.File.Tests/Support/ThrowingLogEventFormatter.cs index 9170ef5..10de763 100644 --- a/test/Serilog.Sinks.File.Tests/Support/ThrowingLogEventFormatter.cs +++ b/test/Serilog.Sinks.File.Tests/Support/ThrowingLogEventFormatter.cs @@ -1,15 +1,12 @@ -using System; -using System.IO; -using Serilog.Events; +using Serilog.Events; using Serilog.Formatting; -namespace Serilog.Tests.Support +namespace Serilog.Tests.Support; + +public class ThrowingLogEventFormatter : ITextFormatter { - public class ThrowingLogEventFormatter : ITextFormatter + public void Format(LogEvent logEvent, TextWriter output) { - public void Format(LogEvent logEvent, TextWriter output) - { - throw new NotImplementedException(); - } + throw new NotImplementedException(); } } diff --git a/test/Serilog.Sinks.File.Tests/Support/TruncateFileHook.cs b/test/Serilog.Sinks.File.Tests/Support/TruncateFileHook.cs index 63f8497..aecc413 100644 --- a/test/Serilog.Sinks.File.Tests/Support/TruncateFileHook.cs +++ b/test/Serilog.Sinks.File.Tests/Support/TruncateFileHook.cs @@ -1,18 +1,16 @@ -using System.IO; using System.Text; -namespace Serilog.Sinks.File.Tests.Support +namespace Serilog.Sinks.File.Tests.Support; + +/// +/// +/// Demonstrates the use of , by emptying the file before it's written to +/// +public class TruncateFileHook : FileLifecycleHooks { - /// - /// - /// 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) { - public override Stream OnFileOpened(Stream underlyingStream, Encoding encoding) - { - underlyingStream.SetLength(0); - return base.OnFileOpened(underlyingStream, 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 65a974c..e34c335 100644 --- a/test/Serilog.Sinks.File.Tests/TemplatedPathRollerTests.cs +++ b/test/Serilog.Sinks.File.Tests/TemplatedPathRollerTests.cs @@ -1,100 +1,96 @@ -using System; -using System.IO; -using System.Linq; -using Xunit; +using Xunit; -namespace Serilog.Sinks.File.Tests +namespace Serilog.Sinks.File.Tests; + +public class PathRollerTests { - public class PathRollerTests + [Fact] + public void TheLogFileIncludesDateToken() { - [Fact] - 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); - roller.GetLogFilePath(now, null, out var path); - AssertEqualAbsolute(Path.Combine("Logs", "log-20130714.txt"), path); - } + var roller = new PathRoller(Path.Combine("Logs", "log-.txt"), RollingInterval.Day); + var now = new DateTime(2013, 7, 14, 3, 24, 9, 980); + roller.GetLogFilePath(now, null, out var path); + AssertEqualAbsolute(Path.Combine("Logs", "log-20130714.txt"), path); + } - [Fact] - 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); - roller.GetLogFilePath(now, 12, out var path); - AssertEqualAbsolute(Path.Combine("Logs", "log-20130714_012.txt"), path); - } + [Fact] + 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); + roller.GetLogFilePath(now, 12, out var path); + AssertEqualAbsolute(Path.Combine("Logs", "log-20130714_012.txt"), path); + } - static void AssertEqualAbsolute(string path1, string path2) - { - var abs1 = Path.GetFullPath(path1); - var abs2 = Path.GetFullPath(path2); - Assert.Equal(abs1, abs2); - } + static void AssertEqualAbsolute(string path1, string path2) + { + var abs1 = Path.GetFullPath(path1); + var abs2 = Path.GetFullPath(path2); + Assert.Equal(abs1, abs2); + } - [Fact] - public void TheRollerReturnsTheLogFileDirectory() - { - var roller = new PathRoller(Path.Combine("Logs", "log-.txt"), RollingInterval.Day); - AssertEqualAbsolute("Logs", roller.LogFileDirectory); - } + [Fact] + public void TheRollerReturnsTheLogFileDirectory() + { + var roller = new PathRoller(Path.Combine("Logs", "log-.txt"), RollingInterval.Day); + AssertEqualAbsolute("Logs", roller.LogFileDirectory); + } - [Fact] - public void TheLogFileIsNotRequiredToIncludeAnExtension() - { - var roller = new PathRoller(Path.Combine("Logs", "log-"), RollingInterval.Day); - var now = new DateTime(2013, 7, 14, 3, 24, 9, 980); - roller.GetLogFilePath(now, null, out var path); - AssertEqualAbsolute(Path.Combine("Logs", "log-20130714"), path); - } + [Fact] + public void TheLogFileIsNotRequiredToIncludeAnExtension() + { + var roller = new PathRoller(Path.Combine("Logs", "log-"), RollingInterval.Day); + var now = new DateTime(2013, 7, 14, 3, 24, 9, 980); + roller.GetLogFilePath(now, null, out var path); + AssertEqualAbsolute(Path.Combine("Logs", "log-20130714"), path); + } - [Fact] - public void TheLogFileIsNotRequiredToIncludeADirectory() - { - var roller = new PathRoller("log-", RollingInterval.Day); - var now = new DateTime(2013, 7, 14, 3, 24, 9, 980); - roller.GetLogFilePath(now, null, out var path); - AssertEqualAbsolute("log-20130714", path); - } + [Fact] + public void TheLogFileIsNotRequiredToIncludeADirectory() + { + var roller = new PathRoller("log-", RollingInterval.Day); + var now = new DateTime(2013, 7, 14, 3, 24, 9, 980); + roller.GetLogFilePath(now, null, out var path); + AssertEqualAbsolute("log-20130714", path); + } - [Fact] - public void MatchingExcludesSimilarButNonMatchingFiles() - { - var roller = new PathRoller("log-.txt", RollingInterval.Day); - const string similar1 = "log-0.txt"; - const string similar2 = "log-hello.txt"; - var matched = roller.SelectMatches(new[] { similar1, similar2 }); - Assert.Empty(matched); - } + [Fact] + public void MatchingExcludesSimilarButNonMatchingFiles() + { + var roller = new PathRoller("log-.txt", RollingInterval.Day); + const string similar1 = "log-0.txt"; + const string similar2 = "log-hello.txt"; + var matched = roller.SelectMatches(new[] { similar1, similar2 }); + Assert.Empty(matched); + } - [Fact] - public void TheDirectorSearchPatternUsesWildcardInPlaceOfDate() - { - var roller = new PathRoller(Path.Combine("Logs", "log-.txt"), RollingInterval.Day); - Assert.Equal("log-*.txt", roller.DirectorySearchPattern); - } + [Fact] + public void TheDirectorSearchPatternUsesWildcardInPlaceOfDate() + { + var roller = new PathRoller(Path.Combine("Logs", "log-.txt"), RollingInterval.Day); + Assert.Equal("log-*.txt", roller.DirectorySearchPattern); + } - [Theory] - [InlineData("log-.txt", "log-20131210.txt", "log-20131210_031.txt", RollingInterval.Day)] - [InlineData("log-.txt", "log-2013121013.txt", "log-2013121013_031.txt", RollingInterval.Hour)] - public void MatchingSelectsFiles(string template, string zeroth, string thirtyFirst, RollingInterval interval) - { - var roller = new PathRoller(template, interval); - var matched = roller.SelectMatches(new[] { zeroth, thirtyFirst }).ToArray(); - Assert.Equal(2, matched.Length); - Assert.Null(matched[0].SequenceNumber); - Assert.Equal(31, matched[1].SequenceNumber); - } + [Theory] + [InlineData("log-.txt", "log-20131210.txt", "log-20131210_031.txt", RollingInterval.Day)] + [InlineData("log-.txt", "log-2013121013.txt", "log-2013121013_031.txt", RollingInterval.Hour)] + public void MatchingSelectsFiles(string template, string zeroth, string thirtyFirst, RollingInterval interval) + { + var roller = new PathRoller(template, interval); + var matched = roller.SelectMatches(new[] { zeroth, thirtyFirst }).ToArray(); + Assert.Equal(2, matched.Length); + Assert.Null(matched[0].SequenceNumber); + Assert.Equal(31, matched[1].SequenceNumber); + } - [Theory] - [InlineData("log-.txt", "log-20150101.txt", "log-20141231.txt", RollingInterval.Day)] - [InlineData("log-.txt", "log-2015010110.txt", "log-2015010109.txt", RollingInterval.Hour)] - public void MatchingParsesSubstitutions(string template, string newer, string older, RollingInterval interval) - { - var roller = new PathRoller(template, interval); - var matched = roller.SelectMatches(new[] { older, newer }).OrderByDescending(m => m.DateTime).Select(m => m.Filename).ToArray(); - Assert.Equal(new[] { newer, older }, matched); - } + [Theory] + [InlineData("log-.txt", "log-20150101.txt", "log-20141231.txt", RollingInterval.Day)] + [InlineData("log-.txt", "log-2015010110.txt", "log-2015010109.txt", RollingInterval.Hour)] + public void MatchingParsesSubstitutions(string template, string newer, string older, RollingInterval interval) + { + var roller = new PathRoller(template, interval); + var matched = roller.SelectMatches(new[] { older, newer }).OrderByDescending(m => m.DateTime).Select(m => m.Filename).ToArray(); + Assert.Equal(new[] { newer, older }, matched); } } diff --git a/test/Serilog.Sinks.File.Tests/WriteCountingStreamTests.cs b/test/Serilog.Sinks.File.Tests/WriteCountingStreamTests.cs index 887ffe2..68a8cc8 100644 --- a/test/Serilog.Sinks.File.Tests/WriteCountingStreamTests.cs +++ b/test/Serilog.Sinks.File.Tests/WriteCountingStreamTests.cs @@ -1,83 +1,77 @@ -using System.IO; using System.Text; using Serilog.Sinks.File.Tests.Support; using Xunit; -namespace Serilog.Sinks.File.Tests +namespace Serilog.Sinks.File.Tests; + +public class WriteCountingStreamTests { - public class WriteCountingStreamTests + [Fact] + public void CountedLengthIsResetToStreamLengthIfNewSizeIsSmaller() { - [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))) { - // 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 + writer.WriteLine("Hello, world!"); + writer.Flush(); - using (var tmp = TempFolder.ForCaller()) - { - var path = tmp.AllocateFilename("txt"); + countingStream.SetLength(5); + streamLengthAfterSetLength = countingStream.Length; + countedLengthAfterSetLength = countingStream.CountedLength; + } - long streamLengthAfterSetLength; - long countedLengthAfterSetLength; + Assert.Equal(5, streamLengthAfterSetLength); + Assert.Equal(5, 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(); + var lines = System.IO.File.ReadAllLines(path); - countingStream.SetLength(5); - streamLengthAfterSetLength = countingStream.Length; - countedLengthAfterSetLength = countingStream.CountedLength; - } + Assert.Single(lines); + Assert.Equal("Hello", lines[0]); + } - Assert.Equal(5, streamLengthAfterSetLength); - Assert.Equal(5, countedLengthAfterSetLength); + [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 - var lines = System.IO.File.ReadAllLines(path); + using var tmp = TempFolder.ForCaller(); + var path = tmp.AllocateFilename("txt"); - Assert.Single(lines); - Assert.Equal("Hello", lines[0]); - } - } + long streamLengthBeforeSetLength; + long streamLengthAfterSetLength; + long countedLengthAfterSetLength; - [Fact] - public void CountedLengthRemainsTheSameIfNewSizeIsLarger() + 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))) { - // 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]); - } + 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]); } }