diff --git a/.travis.yml b/.travis.yml index e9daee9..6a880da 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,49 +1,11 @@ language: csharp -#dotnet cli require Ubuntu 14.04 -sudo: required -dist: trusty - -#dotnet cli require OSX 10.10 -osx_image: xcode7.1 - -addons: - apt: - packages: - - gettext - - libcurl4-openssl-dev - - libicu-dev - - libssl-dev - - libunwind8 - - zlib1g - -os: - - linux - -env: - global: - - DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true - - TMP: /tmp - - matrix: - - CLI_VERSION=1.0.0-preview2-003121 - - CLI_VERSION=Latest - matrix: - allow_failures: - - env: CLI_VERSION=Latest - -before_install: - - if test "$TRAVIS_OS_NAME" == "osx"; then brew update; brew install openssl; brew link --force openssl; fi - # Download script to install dotnet cli - - if test "$CLI_OBTAIN_URL" == ""; then export CLI_OBTAIN_URL="https://raw.githubusercontent.com/dotnet/cli/rel/1.0.0-preview2/scripts/obtain/dotnet-install.sh"; fi - - curl -L --create-dirs $CLI_OBTAIN_URL -o ./scripts/obtain/install.sh - - find ./scripts -name "*.sh" -exec chmod +x {} \; - - export DOTNET_INSTALL_DIR="$PWD/.dotnetcli" - # use bash to workaround bug https://github.com/dotnet/cli/issues/1725 - - sudo bash ./scripts/obtain/install.sh --channel "preview" --version "$CLI_VERSION" --install-dir "$DOTNET_INSTALL_DIR" --no-path - # add dotnet to PATH - - export PATH="$DOTNET_INSTALL_DIR:$PATH" - + include: + - os: linux + dist: trusty + sudo: required + dotnet: 2.0.0 + group: edge script: - - ./build.sh \ No newline at end of file + - ./build.sh diff --git a/Build.ps1 b/Build.ps1 index 0515652..ee4117d 100644 --- a/Build.ps1 +++ b/Build.ps1 @@ -1,27 +1,46 @@ +echo "build: Build started" + Push-Location $PSScriptRoot -if(Test-Path .\artifacts) { Remove-Item .\artifacts -Force -Recurse } +if(Test-Path .\artifacts) { + echo "build: Cleaning .\artifacts" + Remove-Item .\artifacts -Force -Recurse +} & dotnet restore --no-cache $branch = @{ $true = $env:APPVEYOR_REPO_BRANCH; $false = $(git symbolic-ref --short -q HEAD) }[$env:APPVEYOR_REPO_BRANCH -ne $NULL]; $revision = @{ $true = "{0:00000}" -f [convert]::ToInt32("0" + $env:APPVEYOR_BUILD_NUMBER, 10); $false = "local" }[$env:APPVEYOR_BUILD_NUMBER -ne $NULL]; -$suffix = @{ $true = ""; $false = "$branch-$revision"}[$branch -eq "master" -and $revision -ne "local"] +$suffix = @{ $true = ""; $false = "$($branch.Substring(0, [math]::Min(10,$branch.Length)))-$revision"}[$branch -eq "master" -and $revision -ne "local"] +$commitHash = $(git rev-parse --short HEAD) +$buildSuffix = @{ $true = "$($suffix)-$($commitHash)"; $false = "$($branch)-$($commitHash)" }[$suffix -ne ""] + +echo "build: Package version suffix is $suffix" +echo "build: Build version suffix is $buildSuffix" -foreach ($src in ls src/Serilog.*) { +foreach ($src in ls src/*) { Push-Location $src - & dotnet pack -c Release -o ..\..\.\artifacts --version-suffix=$suffix + echo "build: Packaging project in $src" + + & dotnet build -c Release --version-suffix=$buildSuffix + if ($suffix) { + & dotnet pack -c Release --include-source -o ..\..\artifacts --version-suffix=$suffix --no-build + } else { + & dotnet pack -c Release --include-source -o ..\..\artifacts --no-build + } if($LASTEXITCODE -ne 0) { exit 1 } Pop-Location } -foreach ($test in ls test/Serilog.*.Tests) { +foreach ($test in ls test/*.Tests) { Push-Location $test + echo "build: Testing project in $test" + & dotnet test -c Release - if($LASTEXITCODE -ne 0) { exit 2 } + if($LASTEXITCODE -ne 0) { exit 3 } Pop-Location } diff --git a/README.md b/README.md index 7a6f5d4..e04fcc4 100644 --- a/README.md +++ b/README.md @@ -1,49 +1,189 @@ # Serilog.Sinks.File [![Build status](https://ci.appveyor.com/api/projects/status/hh9gymy0n6tne46j?svg=true)](https://ci.appveyor.com/project/serilog/serilog-sinks-file) [![Travis build](https://travis-ci.org/serilog/serilog-sinks-file.svg)](https://travis-ci.org/serilog/serilog-sinks-file) [![NuGet Version](http://img.shields.io/nuget/v/Serilog.Sinks.File.svg?style=flat)](https://www.nuget.org/packages/Serilog.Sinks.File/) [![Documentation](https://img.shields.io/badge/docs-wiki-yellow.svg)](https://github.com/serilog/serilog/wiki) [![Join the chat at https://gitter.im/serilog/serilog](https://img.shields.io/gitter/room/serilog/serilog.svg)](https://gitter.im/serilog/serilog) -Writes [Serilog](https://serilog.net) events to a text file. +Writes [Serilog](https://serilog.net) events to one or more text files. + +### Getting started + +Install the [Serilog.Sinks.File](https://nuget.org/serilog/serilog-sinks-file) package from NuGet: + +```powershell +Install-Package Serilog.Sinks.File +``` + +To configure the sink in C# code, call `WriteTo.File()` during logger configuration: ```csharp var log = new LoggerConfiguration() - .WriteTo.File("log.txt") + .WriteTo.File("log.txt", rollingInterval: RollingInterval.Day) .CreateLogger(); ``` +This will append the time period to the filename, creating a file set like: + +``` +log20180631.txt +log20180701.txt +log20180702.txt +``` + +> **Important**: By default, only one process may write to a log file at a given time. See _Shared log files_ below for information on multi-process sharing. + +### Limits + To avoid bringing down apps with runaway disk usage the file sink **limits file size to 1GB by default**. The limit can be increased or removed using the `fileSizeLimitBytes` parameter. ```csharp .WriteTo.File("log.txt", fileSizeLimitBytes: null) ``` -> **Important:** By default only one process may use a log file at a given time. See _Shared log files_ below if multi-process logging is required. +For the same reason, only **the most recent 31 files** are retained by default (i.e. one long month). To change or remove this limit, pass the `retainedFileCountLimit` parameter. + +```csharp + .WriteTo.RollingFile("log.txt", rollingInterval: RollingInterval.Day, retainedFileCountLimit: null) +``` + +### Rolling policies + +To create a log file per day or other time period, specify a `rollingInterval` as shown in the examples above. + +To roll when the file reaches `fileSizeLimitBytes`, specify `rollOnFileSizeLimit`: + +```csharp + .WriteTo.File("log.txt", rollOnFileSizeLimit: true) +``` + +This will create a file set like: + +``` +log.txt +log_001.txt +log_002.txt +``` + +Specifying both `rollingInterval` and `rollOnFileSizeLimit` will cause both policies to be applied, while specifying neither will result in all events being written to a single file. + +Old files will be cleaned up as per `retainedFileCountLimit` - the default is 31. + +### XML `` configuration + +To use the file sink with the [Serilog.Settings.AppSettings](https://github.com/serilog/serilog-settings-appsettings) package, first install that package if you haven't already done so: + +```powershell +Install-Package Serilog.Settings.AppSettings +``` + +Instead of configuring the logger in code, call `ReadFrom.AppSettings()`: + +```csharp +var log = new LoggerConfiguration() + .ReadFrom.AppSettings() + .CreateLogger(); +``` + +In your application's `App.config` or `Web.config` file, specify the file sink assembly and required path format under the `` node: + +```xml + + + + +``` + +The parameters that can be set through the `serilog:write-to:File` keys are the method parameters accepted by the `WriteTo.File()` configuration method. This means, for example, that the `fileSizeLimitBytes` parameter can be set with: + +```xml + +``` + +Omitting the `value` will set the parameter to `null`: -### `` configuration +```xml + +``` -The sink can be configured in XML [app-settings format](https://github.com/serilog/serilog/wiki/AppSettings) if the _Serilog.Settings.AppSettings_ package is in use: +In XML and JSON configuration formats, environment variables can be used in setting values. This means, for instance, that the log file path can be based on `TMP` or `APPDATA`: ```xml - - - + ``` -### JSON formatting +### JSON `appsettings.json` configuration + +To use the file sink with _Microsoft.Extensions.Configuration_, for example with ASP.NET Core or .NET Core, use the [Serilog.Settings.Configuration](https://github.com/serilog/serilog-settings-configuration) package. First install that package if you have not already done so: + +```powershell +Install-Package Serilog.Settings.Configuration +``` -To emit JSON, rather than plain text, a formatter can be specified: +Instead of configuring the file directly in code, call `ReadFrom.Configuration()`: ```csharp - .WriteTo.File(new JsonFormatter(), "log.txt") +var configuration = new ConfigurationBuilder() + .AddJsonFile("appsettings.json") + .Build(); + +var logger = new LoggerConfiguration() + .ReadFrom.Configuration(configuration) + .CreateLogger(); +``` + +In your `appsettings.json` file, under the `Serilog` node, : + +```json +{ + "Serilog": { + "WriteTo": [ + { "Name": "File", "Args": { "path": "log.txt", "rollingInterval": "Day" } } + ] + } +} ``` -To configure an alternative formatter in XML ``, specify the formatter's assembly-qualified type name as the setting `value`. +See the XML `` example above for a discussion of available `Args` options. + +### Controlling event formatting + +The file sink creates events in a fixed text format by default: + +``` +2018-07-06 09:02:17.148 +10:00 [INF] HTTP GET / responded 200 in 1994 ms +``` + +The format is controlled using an _output template_, which the file configuration method accepts as an `outputTemplate` parameter. + +The default format above corresponds to an output template like: + +```csharp + .WriteTo.File("log.txt", + outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{u3}] {Message:lj}{NewLine}{Exception}") +``` + +##### JSON event formatting + +To write events to the file in an alternative format such as JSON, pass an `ITextFormatter` as the first argument: + +```csharp + .WriteTo.File(new JsonFormatter(), "log.txt") +``` ### Shared log files -Multiple processes can concurrently write to the same log file if the `shared` parameter is set to `true`: +To enable multi-process shared log files, set `shared` to `true`: ```csharp .WriteTo.File("log.txt", shared: true) ``` +### Auditing + +The file sink can operate as an audit file through `AuditTo`: + +```csharp + .AuditTo.File("audit.txt") +``` + +Only a limited subset of configuration options are currently available in this mode. + ### Performance By default, the file sink will flush each event written through it to disk. To improve write performance, specifying `buffered: true` will permit the underlying stream to buffer writes. diff --git a/appveyor.yml b/appveyor.yml index 19d0d28..7e5f9b5 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,13 +1,9 @@ version: '{build}' skip_tags: true -image: Visual Studio 2015 +image: Visual Studio 2017 configuration: Release install: - ps: mkdir -Force ".\build\" | Out-Null - - ps: Invoke-WebRequest "https://raw.githubusercontent.com/dotnet/cli/rel/1.0.0-preview2/scripts/obtain/dotnet-install.ps1" -OutFile ".\build\installcli.ps1" - - ps: $env:DOTNET_INSTALL_DIR = "$pwd\.dotnetcli" - - ps: '& .\build\installcli.ps1 -InstallDir "$env:DOTNET_INSTALL_DIR" -NoPath -Version 1.0.0-preview2-003121' - - ps: $env:Path = "$env:DOTNET_INSTALL_DIR;$env:Path" build_script: - ps: ./Build.ps1 test: off diff --git a/build.sh b/build.sh index 39408c4..4e46f40 100755 --- a/build.sh +++ b/build.sh @@ -1,12 +1,11 @@ #!/bin/bash -dotnet restore --no-cache -for path in src/*/project.json; do - dirname="$(dirname "${path}")" - dotnet build ${dirname} -f netstandard1.3 -c Release +dotnet --info +dotnet restore + +for path in src/**/*.csproj; do + dotnet build -f netstandard1.3 -c Release ${path} done -for path in test/*.Tests/project.json; do - dirname="$(dirname "${path}")" - dotnet build ${dirname} -f netcoreapp1.0 -c Release - dotnet test ${dirname} -f netcoreapp1.0 -c Release +for path in test/*.Tests/*.csproj; do + dotnet test -f netcoreapp2.0 -c Release ${path} done diff --git a/example/Sample/Sample.csproj b/example/Sample/Sample.csproj new file mode 100644 index 0000000..ec04f95 --- /dev/null +++ b/example/Sample/Sample.csproj @@ -0,0 +1,24 @@ + + + + netcoreapp2.0;net47 + Sample + Exe + Sample + true + + + + + + + + + + + + + + + + diff --git a/example/Sample/Sample.xproj b/example/Sample/Sample.xproj deleted file mode 100644 index 000aa06..0000000 --- a/example/Sample/Sample.xproj +++ /dev/null @@ -1,21 +0,0 @@ - - - - 14.0 - $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) - - - - - a34235a2-a717-4a1c-bf5c-f4a9e06e1260 - Sample - .\obj - .\bin\ - v4.5.2 - - - - 2.0 - - - diff --git a/example/Sample/project.json b/example/Sample/project.json deleted file mode 100644 index 525d510..0000000 --- a/example/Sample/project.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "buildOptions": { - "emitEntryPoint": true - }, - - "dependencies": { - "Serilog.Sinks.File": { "target": "project" } - }, - - "frameworks": { - "netcoreapp1.0": { - "imports": "dnxcore50", - "dependencies": { - "Microsoft.NETCore.App": { - "type": "platform", - "version": "1.0.0" - } - } - }, - "net4.5": {} - }, - "runtimes": { "win10-x64": {} } -} diff --git a/global.json b/global.json deleted file mode 100644 index a2b2a41..0000000 --- a/global.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "projects": [ "src", "test" ], - "sdk": { - "version": "1.0.0-preview2-003121" - } -} diff --git a/serilog-sinks-file.sln b/serilog-sinks-file.sln index 536f0ab..71527e4 100644 --- a/serilog-sinks-file.sln +++ b/serilog-sinks-file.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 14 -VisualStudioVersion = 14.0.25420.1 +# Visual Studio 15 +VisualStudioVersion = 15.0.26730.15 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{037440DE-440B-4129-9F7A-09B42D00397E}" EndProject @@ -12,21 +12,20 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "assets", "assets", "{E9D1B5 appveyor.yml = appveyor.yml Build.ps1 = Build.ps1 build.sh = build.sh - global.json = global.json NuGet.Config = NuGet.Config README.md = README.md assets\Serilog.snk = assets\Serilog.snk EndProjectSection EndProject -Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Serilog.Sinks.File", "src\Serilog.Sinks.File\Serilog.Sinks.File.xproj", "{57E0ED0E-0F45-48AB-A73D-6A92B7C32095}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{7B927378-9F16-4F6F-B3F6-156395136646}" EndProject -Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Serilog.Sinks.File.Tests", "test\Serilog.Sinks.File.Tests\Serilog.Sinks.File.Tests.xproj", "{3C2D8E01-5580-426A-BDD9-EC59CD98E618}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "example", "example", "{196B1544-C617-4D7C-96D1-628713BDD52A}" EndProject -Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Sample", "example\Sample\Sample.xproj", "{A34235A2-A717-4A1C-BF5C-F4A9E06E1260}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Serilog.Sinks.File", "src\Serilog.Sinks.File\Serilog.Sinks.File.csproj", "{57E0ED0E-0F45-48AB-A73D-6A92B7C32095}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Serilog.Sinks.File.Tests", "test\Serilog.Sinks.File.Tests\Serilog.Sinks.File.Tests.csproj", "{3C2D8E01-5580-426A-BDD9-EC59CD98E618}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sample", "example\Sample\Sample.csproj", "{A34235A2-A717-4A1C-BF5C-F4A9E06E1260}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -55,4 +54,7 @@ Global {3C2D8E01-5580-426A-BDD9-EC59CD98E618} = {7B927378-9F16-4F6F-B3F6-156395136646} {A34235A2-A717-4A1C-BF5C-F4A9E06E1260} = {196B1544-C617-4D7C-96D1-628713BDD52A} EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {EA0197B4-FCA8-4DF2-BF34-274FA41333D1} + EndGlobalSection EndGlobal diff --git a/src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs b/src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs index 353e6c6..5cb19e9 100644 --- a/src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs +++ b/src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs @@ -1,4 +1,4 @@ -// Copyright 2013-2016 Serilog Contributors +// Copyright 2013-2017 Serilog Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -13,6 +13,8 @@ // limitations under the License. using System; +using System.ComponentModel; +using System.Text; using Serilog.Configuration; using Serilog.Core; using Serilog.Debugging; @@ -22,13 +24,96 @@ using Serilog.Formatting.Json; using Serilog.Sinks.File; +// ReSharper disable MethodOverloadWithOptionalParameter + namespace Serilog { /// Extends with methods to add file sinks. public static class FileLoggerConfigurationExtensions { + const int DefaultRetainedFileCountLimit = 31; // A long month of logs const long DefaultFileSizeLimitBytes = 1L * 1024 * 1024 * 1024; - const string DefaultOutputTemplate = "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level}] {Message}{NewLine}{Exception}"; + const string DefaultOutputTemplate = "{Timestamp: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) + { + // ReSharper disable once RedundantArgumentDefaultValue + return File(sinkConfiguration, path, restrictedToMinimumLevel, outputTemplate, formatProvider, fileSizeLimitBytes, + levelSwitch, buffered, shared, flushToDiskInterval, RollingInterval.Infinite, false, + 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) + { + // ReSharper disable once RedundantArgumentDefaultValue + return File(sinkConfiguration, formatter, path, restrictedToMinimumLevel, fileSizeLimitBytes, levelSwitch, + buffered, shared, flushToDiskInterval, RollingInterval.Infinite, false, null, null); + } /// /// Write log events to the specified file. @@ -41,7 +126,7 @@ public static class FileLoggerConfigurationExtensions /// to be changed at runtime. /// Supplies culture-specific formatting information, or null. /// A message template describing the format used to write to the sink. - /// the default is "{Timestamp} [{Level}] {Message}{NewLine}{Exception}". + /// the default is "{Timestamp: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. @@ -49,6 +134,12 @@ public static class FileLoggerConfigurationExtensions /// 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. /// The file will be written using the UTF-8 character set. public static LoggerConfiguration File( @@ -61,14 +152,20 @@ public static LoggerConfiguration File( LoggingLevelSwitch levelSwitch = null, bool buffered = false, bool shared = false, - TimeSpan? flushToDiskInterval = null) + TimeSpan? flushToDiskInterval = null, + RollingInterval rollingInterval = RollingInterval.Infinite, + bool rollOnFileSizeLimit = false, + int? retainedFileCountLimit = DefaultRetainedFileCountLimit, + Encoding encoding = 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: buffered, shared: shared, flushToDiskInterval: flushToDiskInterval); + return File(sinkConfiguration, formatter, path, restrictedToMinimumLevel, fileSizeLimitBytes, + levelSwitch, buffered, shared, flushToDiskInterval, + rollingInterval, rollOnFileSizeLimit, retainedFileCountLimit, encoding); } /// @@ -77,7 +174,7 @@ public static LoggerConfiguration File( /// Logger sink configuration. /// A formatter, such as , to convert the log events into /// text for the file. If control of regular text formatting is required, use the other - /// overload of + /// overload of /// and specify the outputTemplate parameter instead. /// /// Path to the file. @@ -92,6 +189,12 @@ public static LoggerConfiguration File( /// is false. /// Allow the log file to be shared by multiple processes. The default is false. /// If provided, a full disk flush will be performed periodically at the specified interval. + /// The interval at which logging will roll over to a new file. + /// If true, a new file will be created when the file size limit is reached. Filenames + /// will have a number appended in the format _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. /// The file will be written using the UTF-8 character set. public static LoggerConfiguration File( @@ -103,9 +206,14 @@ public static LoggerConfiguration File( LoggingLevelSwitch levelSwitch = null, bool buffered = false, bool shared = false, - TimeSpan? flushToDiskInterval = null) + TimeSpan? flushToDiskInterval = null, + RollingInterval rollingInterval = RollingInterval.Infinite, + bool rollOnFileSizeLimit = false, + int? retainedFileCountLimit = DefaultRetainedFileCountLimit, + Encoding encoding = null) { - return ConfigureFile(sinkConfiguration.Sink, formatter, path, restrictedToMinimumLevel, fileSizeLimitBytes, levelSwitch, buffered: buffered, shared: shared, flushToDiskInterval: flushToDiskInterval); + return ConfigureFile(sinkConfiguration.Sink, formatter, path, restrictedToMinimumLevel, fileSizeLimitBytes, levelSwitch, + buffered, false, shared, flushToDiskInterval, encoding, rollingInterval, rollOnFileSizeLimit, retainedFileCountLimit); } /// @@ -119,7 +227,7 @@ public static LoggerConfiguration File( /// to be changed at runtime. /// Supplies culture-specific formatting information, or null. /// A message template describing the format used to write to the sink. - /// the default is "{Timestamp} [{Level}] {Message}{NewLine}{Exception}". + /// the default is "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception}". /// Configuration object allowing method chaining. /// The file will be written using the UTF-8 character set. public static LoggerConfiguration File( @@ -161,48 +269,63 @@ public static LoggerConfiguration File( LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum, LoggingLevelSwitch levelSwitch = null) { - return ConfigureFile(sinkConfiguration.Sink, formatter, path, restrictedToMinimumLevel, null, levelSwitch, false, true); + return ConfigureFile(sinkConfiguration.Sink, formatter, path, restrictedToMinimumLevel, null, levelSwitch, false, true, + false, null, null, RollingInterval.Infinite, false, null); } static LoggerConfiguration ConfigureFile( this Func addSink, ITextFormatter formatter, string path, - LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum, - long? fileSizeLimitBytes = DefaultFileSizeLimitBytes, - LoggingLevelSwitch levelSwitch = null, - bool buffered = false, - bool propagateExceptions = false, - bool shared = false, - TimeSpan? flushToDiskInterval = null) + LogEventLevel restrictedToMinimumLevel, + long? fileSizeLimitBytes, + LoggingLevelSwitch levelSwitch, + bool buffered, + bool propagateExceptions, + bool shared, + TimeSpan? flushToDiskInterval, + Encoding encoding, + RollingInterval rollingInterval, + bool rollOnFileSizeLimit, + int? retainedFileCountLimit) { if (addSink == null) throw new ArgumentNullException(nameof(addSink)); if (formatter == null) throw new ArgumentNullException(nameof(formatter)); if (path == null) throw new ArgumentNullException(nameof(path)); - if (fileSizeLimitBytes.HasValue && fileSizeLimitBytes < 0) throw new ArgumentException("Negative value provided; file size limit must be non-negative"); - if (shared && buffered) - throw new ArgumentException("Buffered writes are not available when file sharing is enabled.", nameof(buffered)); + if (fileSizeLimitBytes.HasValue && fileSizeLimitBytes < 0) throw new ArgumentException("Negative value provided; file size limit must be non-negative.", nameof(fileSizeLimitBytes)); + if (retainedFileCountLimit.HasValue && retainedFileCountLimit < 1) throw new ArgumentException("At least one file must be retained.", nameof(retainedFileCountLimit)); + if (shared && buffered) throw new ArgumentException("Buffered writes are not available when file sharing is enabled.", nameof(buffered)); ILogEventSink sink; - try + + if (rollOnFileSizeLimit || rollingInterval != RollingInterval.Infinite) + { + sink = new RollingFileSink(path, formatter, fileSizeLimitBytes, retainedFileCountLimit, encoding, buffered, shared, rollingInterval, rollOnFileSizeLimit); + } + else { - if (shared) + try { - sink = new SharedFileSink(path, formatter, fileSizeLimitBytes); +#pragma warning disable 618 + if (shared) + { + sink = new SharedFileSink(path, formatter, fileSizeLimitBytes); + } + else + { + sink = new FileSink(path, formatter, fileSizeLimitBytes, buffered: buffered); + } +#pragma warning restore 618 } - else + catch (Exception ex) { - sink = new FileSink(path, formatter, fileSizeLimitBytes, buffered: buffered); - } - } - catch (Exception ex) - { - SelfLog.WriteLine("Unable to open file sink for {0}: {1}", path, ex); + SelfLog.WriteLine("Unable to open file sink for {0}: {1}", path, ex); - if (propagateExceptions) - throw; + if (propagateExceptions) + throw; - return addSink(new NullSink(), LevelAlias.Maximum, null); + return addSink(new NullSink(), LevelAlias.Maximum, null); + } } if (flushToDiskInterval.HasValue) @@ -212,5 +335,5 @@ static LoggerConfiguration ConfigureFile( return addSink(sink, restrictedToMinimumLevel, levelSwitch); } - } + } } diff --git a/src/Serilog.Sinks.File/RollingInterval.cs b/src/Serilog.Sinks.File/RollingInterval.cs new file mode 100644 index 0000000..9fac848 --- /dev/null +++ b/src/Serilog.Sinks.File/RollingInterval.cs @@ -0,0 +1,52 @@ +// Copyright 2017 Serilog Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace Serilog +{ + /// + /// Specifies the frequency at which the log file should roll. + /// + public enum RollingInterval + { + /// + /// The log file will never roll; no time period information will be appended to the log file name. + /// + Infinite, + + /// + /// Roll every year. Filenames will have a four-digit year appended in the pattern yyyy. + /// + Year, + + /// + /// Roll every calendar month. Filenames will have yyyyMM appended. + /// + Month, + + /// + /// Roll every day. Filenames will have yyyyMMdd appended. + /// + Day, + + /// + /// Roll every hour. Filenames will have yyyyMMddHH appended. + /// + Hour, + + /// + /// Roll every minute. Filenames will have yyyyMMddHHmm appended. + /// + Minute + } +} diff --git a/src/Serilog.Sinks.File/Serilog.Sinks.File.csproj b/src/Serilog.Sinks.File/Serilog.Sinks.File.csproj new file mode 100644 index 0000000..569e5b0 --- /dev/null +++ b/src/Serilog.Sinks.File/Serilog.Sinks.File.csproj @@ -0,0 +1,54 @@ + + + + Write Serilog events to text files in plain or JSON format. + 4.0.0 + Serilog Contributors + net45;netstandard1.3 + true + Serilog.Sinks.File + ../../assets/Serilog.snk + true + true + Serilog.Sinks.File + serilog;file + http://serilog.net/images/serilog-sink-nuget.png + http://serilog.net + http://www.apache.org/licenses/LICENSE-2.0 + false + Serilog + true + Serilog.Sinks.File + + true + + + + + + + + + + + + + + $(DefineConstants);ATOMIC_APPEND;HRESULTS + + + + $(DefineConstants);OS_MUTEX + + + + + + + + + + + + + diff --git a/src/Serilog.Sinks.File/Serilog.Sinks.File.xproj b/src/Serilog.Sinks.File/Serilog.Sinks.File.xproj deleted file mode 100644 index c8d4c28..0000000 --- a/src/Serilog.Sinks.File/Serilog.Sinks.File.xproj +++ /dev/null @@ -1,18 +0,0 @@ - - - - 14.0 - $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) - - - - 57e0ed0e-0f45-48ab-a73d-6a92b7c32095 - Serilog - .\obj - .\bin\ - - - 2.0 - - - \ No newline at end of file diff --git a/src/Serilog.Sinks.File/Sinks/File/Clock.cs b/src/Serilog.Sinks.File/Sinks/File/Clock.cs new file mode 100644 index 0000000..b7cf3cc --- /dev/null +++ b/src/Serilog.Sinks.File/Sinks/File/Clock.cs @@ -0,0 +1,38 @@ +// Copyright 2013-2016 Serilog Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System; + +namespace Serilog.Sinks.File +{ + static class Clock + { + static Func _dateTimeNow = () => DateTime.Now; + + [ThreadStatic] + static DateTime _testDateTimeNow; + + public static DateTime DateTimeNow => _dateTimeNow(); + + // Time is set per thread to support parallel + // If any thread uses the clock in test mode, all threads + // must use it in test mode; once set to test mode only + // terminating the application returns it to normal use. + public static void SetTestDateTimeNow(DateTime now) + { + _testDateTimeNow = now; + _dateTimeNow = () => _testDateTimeNow; + } + } +} diff --git a/src/Serilog.Sinks.File/Sinks/File/FileSink.cs b/src/Serilog.Sinks.File/Sinks/File/FileSink.cs index 443519a..bfd288f 100644 --- a/src/Serilog.Sinks.File/Sinks/File/FileSink.cs +++ b/src/Serilog.Sinks.File/Sinks/File/FileSink.cs @@ -15,7 +15,6 @@ using System; using System.IO; using System.Text; -using Serilog.Core; using Serilog.Events; using Serilog.Formatting; @@ -24,7 +23,8 @@ namespace Serilog.Sinks.File /// /// Write log events to a disk file. /// - public sealed class FileSink : ILogEventSink, IFlushableFileSink, IDisposable + [Obsolete("This type will be removed from the public API in a future version; use `WriteTo.File()` instead.")] + public sealed class FileSink : IFileSink, IDisposable { readonly TextWriter _output; readonly FileStream _underlyingStream; @@ -50,7 +50,7 @@ public FileSink(string path, ITextFormatter textFormatter, long? fileSizeLimitBy { if (path == null) throw new ArgumentNullException(nameof(path)); if (textFormatter == null) throw new ArgumentNullException(nameof(textFormatter)); - if (fileSizeLimitBytes.HasValue && fileSizeLimitBytes < 0) throw new ArgumentException("Negative value provided; file size limit must be non-negative"); + if (fileSizeLimitBytes.HasValue && fileSizeLimitBytes < 0) throw new ArgumentException("Negative value provided; file size limit must be non-negative."); _textFormatter = textFormatter; _fileSizeLimitBytes = fileSizeLimitBytes; @@ -71,11 +71,7 @@ public FileSink(string path, ITextFormatter textFormatter, long? fileSizeLimitBy _output = new StreamWriter(outputStream, encoding ?? new UTF8Encoding(encoderShouldEmitUTF8Identifier: false)); } - /// - /// Emit the provided log event to the sink. - /// - /// The log event to write. - public void Emit(LogEvent logEvent) + bool IFileSink.EmitOrOverflow(LogEvent logEvent) { if (logEvent == null) throw new ArgumentNullException(nameof(logEvent)); lock (_syncRoot) @@ -83,15 +79,26 @@ public void Emit(LogEvent logEvent) if (_fileSizeLimitBytes != null) { if (_countingStreamWrapper.CountedLength >= _fileSizeLimitBytes.Value) - return; + return false; } _textFormatter.Format(logEvent, _output); if (!_buffered) _output.Flush(); + + return true; } } + /// + /// Emit the provided log event to the sink. + /// + /// The log event to write. + public void Emit(LogEvent logEvent) + { + ((IFileSink) this).EmitOrOverflow(logEvent); + } + /// public void Dispose() { diff --git a/src/Serilog.Sinks.File/Sinks/File/IFileSink.cs b/src/Serilog.Sinks.File/Sinks/File/IFileSink.cs new file mode 100644 index 0000000..89268ab --- /dev/null +++ b/src/Serilog.Sinks.File/Sinks/File/IFileSink.cs @@ -0,0 +1,29 @@ +// Copyright 2017 Serilog Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using Serilog.Core; +using Serilog.Events; + +namespace Serilog.Sinks.File +{ + /// + /// Exists only for the convenience of , which + /// switches implementations based on sharing. Would refactor, but preserving + /// backwards compatibility. + /// + interface IFileSink : ILogEventSink, IFlushableFileSink + { + bool EmitOrOverflow(LogEvent logEvent); + } +} diff --git a/src/Serilog.Sinks.File/Sinks/File/IFlushableFileSink.cs b/src/Serilog.Sinks.File/Sinks/File/IFlushableFileSink.cs index c74727e..75d6e52 100644 --- a/src/Serilog.Sinks.File/Sinks/File/IFlushableFileSink.cs +++ b/src/Serilog.Sinks.File/Sinks/File/IFlushableFileSink.cs @@ -1,4 +1,18 @@ -namespace Serilog.Sinks.File +// Copyright 2017 Serilog Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace Serilog.Sinks.File { /// /// Supported by (file-based) sinks that can be explicitly flushed. diff --git a/src/Serilog.Sinks.File/Sinks/File/IOErrors.cs b/src/Serilog.Sinks.File/Sinks/File/IOErrors.cs new file mode 100644 index 0000000..36fe8bc --- /dev/null +++ b/src/Serilog.Sinks.File/Sinks/File/IOErrors.cs @@ -0,0 +1,31 @@ +// Copyright 2013-2016 Serilog Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.IO; + +namespace Serilog.Sinks.File +{ + static class IOErrors + { + public static bool IsLockedFile(IOException ex) + { +#if HRESULTS + var errorCode = System.Runtime.InteropServices.Marshal.GetHRForException(ex) & ((1 << 16) - 1); + return errorCode == 32 || errorCode == 33; +#else + return true; +#endif + } + } +} diff --git a/src/Serilog.Sinks.File/Sinks/File/PathRoller.cs b/src/Serilog.Sinks.File/Sinks/File/PathRoller.cs new file mode 100644 index 0000000..17c496b --- /dev/null +++ b/src/Serilog.Sinks.File/Sinks/File/PathRoller.cs @@ -0,0 +1,116 @@ +// Copyright 2013-2016 Serilog Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Text.RegularExpressions; + +namespace Serilog.Sinks.File +{ + class PathRoller + { + const string PeriodMatchGroup = "period"; + const string SequenceNumberMatchGroup = "sequence"; + + readonly string _directory; + readonly string _filenamePrefix; + readonly string _filenameSuffix; + readonly Regex _filenameMatcher; + + readonly RollingInterval _interval; + readonly string _periodFormat; + + public PathRoller(string path, RollingInterval interval) + { + if (path == null) throw new ArgumentNullException(nameof(path)); + _interval = interval; + _periodFormat = interval.GetFormat(); + + var pathDirectory = Path.GetDirectoryName(path); + if (string.IsNullOrEmpty(pathDirectory)) + pathDirectory = Directory.GetCurrentDirectory(); + + _directory = Path.GetFullPath(pathDirectory); + _filenamePrefix = Path.GetFileNameWithoutExtension(path); + _filenameSuffix = Path.GetExtension(path); + _filenameMatcher = new Regex( + "^" + + Regex.Escape(_filenamePrefix) + + "(?<" + PeriodMatchGroup + ">\\d{" + _periodFormat.Length + "})" + + "(?<" + SequenceNumberMatchGroup + ">_[0-9]{3,}){0,1}" + + Regex.Escape(_filenameSuffix) + + "$"); + + DirectorySearchPattern = $"{_filenamePrefix}*{_filenameSuffix}"; + } + + public string LogFileDirectory => _directory; + + public string DirectorySearchPattern { get; } + + public void GetLogFilePath(DateTime date, int? sequenceNumber, out string path) + { + var currentCheckpoint = GetCurrentCheckpoint(date); + + var tok = currentCheckpoint?.ToString(_periodFormat, CultureInfo.InvariantCulture) ?? ""; + + if (sequenceNumber != null) + tok += "_" + sequenceNumber.Value.ToString("000", CultureInfo.InvariantCulture); + + path = Path.Combine(_directory, _filenamePrefix + tok + _filenameSuffix); + } + + public IEnumerable SelectMatches(IEnumerable filenames) + { + foreach (var filename in filenames) + { + var match = _filenameMatcher.Match(filename); + if (!match.Success) + continue; + + int? inc = null; + var incGroup = match.Groups[SequenceNumberMatchGroup]; + if (incGroup.Captures.Count != 0) + { + var incPart = incGroup.Captures[0].Value.Substring(1); + inc = int.Parse(incPart, CultureInfo.InvariantCulture); + } + + DateTime? period = null; + var periodGroup = match.Groups[PeriodMatchGroup]; + if (periodGroup.Captures.Count != 0) + { + var dateTimePart = periodGroup.Captures[0].Value; + if (DateTime.TryParseExact( + dateTimePart, + _periodFormat, + CultureInfo.InvariantCulture, + DateTimeStyles.None, + out var dateTime)) + { + period = dateTime; + } + } + + yield return new RollingLogFile(filename, period, inc); + } + } + + public DateTime? GetCurrentCheckpoint(DateTime instant) => _interval.GetCurrentCheckpoint(instant); + + public DateTime? GetNextCheckpoint(DateTime instant) => _interval.GetNextCheckpoint(instant); + } +} diff --git a/src/Serilog.Sinks.File/Sinks/File/RollingFileSink.cs b/src/Serilog.Sinks.File/Sinks/File/RollingFileSink.cs new file mode 100644 index 0000000..644176f --- /dev/null +++ b/src/Serilog.Sinks.File/Sinks/File/RollingFileSink.cs @@ -0,0 +1,233 @@ +// 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. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma warning disable 618 + +using System; +using System.IO; +using System.Linq; +using System.Text; +using Serilog.Core; +using Serilog.Debugging; +using Serilog.Events; +using Serilog.Formatting; + +namespace Serilog.Sinks.File +{ + sealed class RollingFileSink : ILogEventSink, IFlushableFileSink, IDisposable + { + readonly PathRoller _roller; + readonly ITextFormatter _textFormatter; + readonly long? _fileSizeLimitBytes; + readonly int? _retainedFileCountLimit; + readonly Encoding _encoding; + readonly bool _buffered; + readonly bool _shared; + readonly bool _rollOnFileSizeLimit; + + readonly object _syncRoot = new object(); + bool _isDisposed; + DateTime? _nextCheckpoint; + IFileSink _currentFile; + int? _currentFileSequence; + + public RollingFileSink(string path, + ITextFormatter textFormatter, + long? fileSizeLimitBytes, + int? retainedFileCountLimit, + Encoding encoding, + bool buffered, + bool shared, + RollingInterval rollingInterval, + bool rollOnFileSizeLimit) + { + if (path == null) throw new ArgumentNullException(nameof(path)); + if (fileSizeLimitBytes.HasValue && fileSizeLimitBytes < 0) throw new ArgumentException("Negative value provided; file size limit must be non-negative"); + if (retainedFileCountLimit.HasValue && retainedFileCountLimit < 1) throw new ArgumentException("Zero or negative value provided; retained file count limit must be at least 1"); + + _roller = new PathRoller(path, rollingInterval); + _textFormatter = textFormatter; + _fileSizeLimitBytes = fileSizeLimitBytes; + _retainedFileCountLimit = retainedFileCountLimit; + _encoding = encoding; + _buffered = buffered; + _shared = shared; + _rollOnFileSizeLimit = rollOnFileSizeLimit; + } + + public void Emit(LogEvent logEvent) + { + if (logEvent == null) throw new ArgumentNullException(nameof(logEvent)); + + lock (_syncRoot) + { + if (_isDisposed) throw new ObjectDisposedException("The log file has been disposed."); + + var now = Clock.DateTimeNow; + AlignCurrentFileTo(now); + + while (_currentFile?.EmitOrOverflow(logEvent) == false && _rollOnFileSizeLimit) + { + AlignCurrentFileTo(now, nextSequence: true); + } + } + } + + void AlignCurrentFileTo(DateTime now, bool nextSequence = false) + { + if (!_nextCheckpoint.HasValue) + { + OpenFile(now); + } + else if (nextSequence || now >= _nextCheckpoint.Value) + { + int? minSequence = null; + if (nextSequence) + { + if (_currentFileSequence == null) + minSequence = 1; + else + minSequence = _currentFileSequence.Value + 1; + } + + CloseFile(); + OpenFile(now, minSequence); + } + } + + void OpenFile(DateTime now, int? minSequence = null) + { + var currentCheckpoint = _roller.GetCurrentCheckpoint(now); + + // We only try periodically because repeated failures + // to open log files REALLY slow an app down. + _nextCheckpoint = _roller.GetNextCheckpoint(now) ?? now.AddMinutes(30); + + var existingFiles = Enumerable.Empty(); + try + { + existingFiles = Directory.GetFiles(_roller.LogFileDirectory, _roller.DirectorySearchPattern) + .Select(Path.GetFileName); + } + catch (DirectoryNotFoundException) { } + + var latestForThisCheckpoint = _roller + .SelectMatches(existingFiles) + .Where(m => m.DateTime == currentCheckpoint) + .OrderByDescending(m => m.SequenceNumber) + .FirstOrDefault(); + + var sequence = latestForThisCheckpoint?.SequenceNumber; + if (minSequence != null) + { + if (sequence == null || sequence.Value < minSequence.Value) + sequence = minSequence; + } + + const int maxAttempts = 3; + for (var attempt = 0; attempt < maxAttempts; attempt++) + { + _roller.GetLogFilePath(now, sequence, out var path); + + try + { + _currentFile = _shared ? + (IFileSink)new SharedFileSink(path, _textFormatter, _fileSizeLimitBytes, _encoding) : + new FileSink(path, _textFormatter, _fileSizeLimitBytes, _encoding, _buffered); + _currentFileSequence = sequence; + } + catch (IOException ex) + { + if (IOErrors.IsLockedFile(ex)) + { + SelfLog.WriteLine("File target {0} was locked, attempting to open next in sequence (attempt {1})", path, attempt + 1); + sequence = (sequence ?? 0) + 1; + continue; + } + + throw; + } + + ApplyRetentionPolicy(path); + return; + } + } + + void ApplyRetentionPolicy(string currentFilePath) + { + if (_retainedFileCountLimit == null) return; + + var currentFileName = Path.GetFileName(currentFilePath); + + // We consider the current file to exist, even if nothing's been written yet, + // because files are only opened on response to an event being processed. + var potentialMatches = Directory.GetFiles(_roller.LogFileDirectory, _roller.DirectorySearchPattern) + .Select(Path.GetFileName) + .Union(new [] { currentFileName }); + + var newestFirst = _roller + .SelectMatches(potentialMatches) + .OrderByDescending(m => m.DateTime) + .ThenByDescending(m => m.SequenceNumber) + .Select(m => m.Filename); + + var toRemove = newestFirst + .Where(n => StringComparer.OrdinalIgnoreCase.Compare(currentFileName, n) != 0) + .Skip(_retainedFileCountLimit.Value - 1) + .ToList(); + + foreach (var obsolete in toRemove) + { + var fullPath = Path.Combine(_roller.LogFileDirectory, obsolete); + try + { + System.IO.File.Delete(fullPath); + } + catch (Exception ex) + { + SelfLog.WriteLine("Error {0} while removing obsolete log file {1}", ex, fullPath); + } + } + } + + public void Dispose() + { + lock (_syncRoot) + { + if (_currentFile == null) return; + CloseFile(); + _isDisposed = true; + } + } + + void CloseFile() + { + if (_currentFile != null) + { + (_currentFile as IDisposable)?.Dispose(); + _currentFile = null; + } + + _nextCheckpoint = null; + } + + public void FlushToDisk() + { + lock (_syncRoot) + { + _currentFile?.FlushToDisk(); + } + } + } +} diff --git a/src/Serilog.Sinks.File/Sinks/File/RollingIntervalExtensions.cs b/src/Serilog.Sinks.File/Sinks/File/RollingIntervalExtensions.cs new file mode 100644 index 0000000..2c9e2fd --- /dev/null +++ b/src/Serilog.Sinks.File/Sinks/File/RollingIntervalExtensions.cs @@ -0,0 +1,86 @@ +// Copyright 2017 Serilog Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System; + +namespace Serilog.Sinks.File +{ + static class RollingIntervalExtensions + { + public static string GetFormat(this RollingInterval interval) + { + switch (interval) + { + case RollingInterval.Infinite: + return ""; + case RollingInterval.Year: + return "yyyy"; + case RollingInterval.Month: + return "yyyyMM"; + case RollingInterval.Day: + return "yyyyMMdd"; + case RollingInterval.Hour: + return "yyyyMMddHH"; + case RollingInterval.Minute: + return "yyyyMMddHHmm"; + default: + throw new ArgumentException("Invalid rolling interval"); + } + } + + public static DateTime? GetCurrentCheckpoint(this RollingInterval interval, DateTime instant) + { + switch (interval) + { + case RollingInterval.Infinite: + return null; + case RollingInterval.Year: + return new DateTime(instant.Year, 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"); + } + } + + public static DateTime? GetNextCheckpoint(this RollingInterval interval, DateTime instant) + { + var current = GetCurrentCheckpoint(interval, instant); + if (current == null) + return null; + + switch (interval) + { + case RollingInterval.Year: + return current.Value.AddYears(1); + case RollingInterval.Month: + return current.Value.AddMonths(1); + case RollingInterval.Day: + return current.Value.AddDays(1); + case RollingInterval.Hour: + return current.Value.AddHours(1); + case RollingInterval.Minute: + return current.Value.AddMinutes(1); + default: + throw new ArgumentException("Invalid rolling interval"); + } + } + } +} diff --git a/src/Serilog.Sinks.File/Sinks/File/RollingLogFile.cs b/src/Serilog.Sinks.File/Sinks/File/RollingLogFile.cs new file mode 100644 index 0000000..be64c4e --- /dev/null +++ b/src/Serilog.Sinks.File/Sinks/File/RollingLogFile.cs @@ -0,0 +1,34 @@ +// 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. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System; + +namespace Serilog.Sinks.File +{ + class RollingLogFile + { + public RollingLogFile(string filename, DateTime? dateTime, int? sequenceNumber) + { + Filename = filename; + DateTime = dateTime; + SequenceNumber = sequenceNumber; + } + + public string Filename { get; } + + public DateTime? DateTime { get; } + + public int? SequenceNumber { get; } + } +} diff --git a/src/Serilog.Sinks.File/Sinks/File/SharedFileSink.AtomicAppend.cs b/src/Serilog.Sinks.File/Sinks/File/SharedFileSink.AtomicAppend.cs index 4ea5022..805e786 100644 --- a/src/Serilog.Sinks.File/Sinks/File/SharedFileSink.AtomicAppend.cs +++ b/src/Serilog.Sinks.File/Sinks/File/SharedFileSink.AtomicAppend.cs @@ -27,7 +27,8 @@ namespace Serilog.Sinks.File /// /// Write log events to a disk file. /// - public sealed class SharedFileSink : ILogEventSink, IFlushableFileSink, IDisposable + [Obsolete("This type will be removed from the public API in a future version; use `WriteTo.File(shared: true)` instead.")] + public sealed class SharedFileSink : IFileSink, IDisposable { readonly MemoryStream _writeBuffer; readonly string _path; @@ -84,11 +85,7 @@ public SharedFileSink(string path, ITextFormatter textFormatter, long? fileSizeL encoding ?? new UTF8Encoding(encoderShouldEmitUTF8Identifier: false)); } - /// - /// Emit the provided log event to the sink. - /// - /// The log event to write. - public void Emit(LogEvent logEvent) + bool IFileSink.EmitOrOverflow(LogEvent logEvent) { if (logEvent == null) throw new ArgumentNullException(nameof(logEvent)); @@ -121,13 +118,14 @@ public void Emit(LogEvent logEvent) try { if (_fileOutput.Length >= _fileSizeLimitBytes.Value) - return; + return false; } catch (FileNotFoundException) { } // Cheaper and more reliable than checking existence } _fileOutput.Write(bytes, 0, length); _fileOutput.Flush(); + return true; } catch { @@ -143,6 +141,14 @@ public void Emit(LogEvent logEvent) } } + /// + /// Emit the provided log event to the sink. + /// + /// The log event to write. + public void Emit(LogEvent logEvent) + { + ((IFileSink)this).EmitOrOverflow(logEvent); + } /// public void Dispose() diff --git a/src/Serilog.Sinks.File/Sinks/File/SharedFileSink.OSMutex.cs b/src/Serilog.Sinks.File/Sinks/File/SharedFileSink.OSMutex.cs index d3cf809..a779bda 100644 --- a/src/Serilog.Sinks.File/Sinks/File/SharedFileSink.OSMutex.cs +++ b/src/Serilog.Sinks.File/Sinks/File/SharedFileSink.OSMutex.cs @@ -28,7 +28,7 @@ namespace Serilog.Sinks.File /// /// Write log events to a disk file. /// - public sealed class SharedFileSink : ILogEventSink, IFlushableFileSink, IDisposable + public sealed class SharedFileSink : IFileSink, IDisposable { readonly TextWriter _output; readonly FileStream _underlyingStream; @@ -72,18 +72,14 @@ public SharedFileSink(string path, ITextFormatter textFormatter, long? fileSizeL _output = new StreamWriter(_underlyingStream, encoding ?? new UTF8Encoding(encoderShouldEmitUTF8Identifier: false)); } - /// - /// Emit the provided log event to the sink. - /// - /// The log event to write. - public void Emit(LogEvent logEvent) + bool IFileSink.EmitOrOverflow(LogEvent logEvent) { if (logEvent == null) throw new ArgumentNullException(nameof(logEvent)); lock (_syncRoot) { if (!TryAcquireMutex()) - return; + return true; // We didn't overflow, but, roll-on-size should not be attempted try { @@ -91,12 +87,13 @@ public void Emit(LogEvent logEvent) if (_fileSizeLimitBytes != null) { if (_underlyingStream.Length >= _fileSizeLimitBytes.Value) - return; + return false; } _textFormatter.Format(logEvent, _output); _output.Flush(); _underlyingStream.Flush(); + return true; } finally { @@ -105,6 +102,15 @@ public void Emit(LogEvent logEvent) } } + /// + /// Emit the provided log event to the sink. + /// + /// The log event to write. + public void Emit(LogEvent logEvent) + { + ((IFileSink)this).EmitOrOverflow(logEvent); + } + /// public void Dispose() { diff --git a/src/Serilog.Sinks.File/project.json b/src/Serilog.Sinks.File/project.json deleted file mode 100644 index 50c5c75..0000000 --- a/src/Serilog.Sinks.File/project.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "version": "3.2.1-*", - "description": "Write Serilog events to a text file in plain or JSON format.", - "authors": [ "Serilog Contributors" ], - "packOptions": { - "tags": [ "serilog", "file", "io" ], - "projectUrl": "http://serilog.net", - "licenseUrl": "http://www.apache.org/licenses/LICENSE-2.0", - "iconUrl": "http://serilog.net/images/serilog-sink-nuget.png" - }, - "dependencies": { - "Serilog": "2.3.0" - }, - "buildOptions": { - "keyFile": "../../assets/Serilog.snk", - "xmlDoc": true - }, - "frameworks": { - "net4.5": { - "buildOptions": { "define": [ "ATOMIC_APPEND" ] } - }, - "netstandard1.3": { - "buildOptions": { "define": [ "OS_MUTEX" ] }, - "dependencies": { - "System.IO": "4.1.0", - "System.IO.FileSystem": "4.0.1", - "System.IO.FileSystem.Primitives": "4.0.1", - "System.Text.Encoding.Extensions": "4.0.11", - "System.Threading.Timer": "4.0.1", - "System.Threading": "4.0.11" - } - } - } -} diff --git a/test/Serilog.Sinks.File.Tests/FileLoggerConfigurationExtensionsTests.cs b/test/Serilog.Sinks.File.Tests/FileLoggerConfigurationExtensionsTests.cs index e39cccb..0515655 100644 --- a/test/Serilog.Sinks.File.Tests/FileLoggerConfigurationExtensionsTests.cs +++ b/test/Serilog.Sinks.File.Tests/FileLoggerConfigurationExtensionsTests.cs @@ -5,7 +5,7 @@ using Xunit; using System.IO; -namespace Serilog.Tests +namespace Serilog.Sinks.File.Tests { public class FileLoggerConfigurationExtensionsTests { @@ -78,5 +78,13 @@ public void WhenFlushingToDiskReportedSharedFileSinkCanBeCreatedAndDisposed() Thread.Sleep(TimeSpan.FromSeconds(1)); } } + + [Fact] + public void BufferingIsNotAvailableWhenSharingEnabled() + { + Assert.Throws(() => + new LoggerConfiguration() + .WriteTo.File("logs", buffered: true, shared: true)); + } } } diff --git a/test/Serilog.Sinks.File.Tests/FileSinkTests.cs b/test/Serilog.Sinks.File.Tests/FileSinkTests.cs index 0c0a13d..ea9a5d4 100644 --- a/test/Serilog.Sinks.File.Tests/FileSinkTests.cs +++ b/test/Serilog.Sinks.File.Tests/FileSinkTests.cs @@ -1,11 +1,11 @@ -using System; -using System.IO; +using System.IO; using Xunit; using Serilog.Formatting.Json; using Serilog.Sinks.File.Tests.Support; using Serilog.Tests.Support; using System.Text; -using Serilog.Tests; + +#pragma warning disable 618 namespace Serilog.Sinks.File.Tests { @@ -116,11 +116,10 @@ public void WhenLimitIsSpecifiedAndEncodingHasPreambleDataIsCorrectlyAppendedToF [Fact] public void WhenLimitIsNotSpecifiedAndEncodingHasPreambleDataIsCorrectlyAppendedToFileSink() { - long? maxBytes = null; var encoding = Encoding.UTF8; Assert.True(encoding.GetPreamble().Length > 0); - WriteTwoEventsAndCheckOutputFileLength(maxBytes, encoding); + WriteTwoEventsAndCheckOutputFileLength(null, encoding); } [Fact] @@ -136,11 +135,10 @@ public void WhenLimitIsSpecifiedAndEncodingHasNoPreambleDataIsCorrectlyAppendedT [Fact] public void WhenLimitIsNotSpecifiedAndEncodingHasNoPreambleDataIsCorrectlyAppendedToFileSink() { - long? maxBytes = null; var encoding = new UTF8Encoding(false); Assert.Equal(0, encoding.GetPreamble().Length); - WriteTwoEventsAndCheckOutputFileLength(maxBytes, encoding); + WriteTwoEventsAndCheckOutputFileLength(null, encoding); } static void WriteTwoEventsAndCheckOutputFileLength(long? maxBytes, Encoding encoding) diff --git a/test/Serilog.Sinks.File.Tests/RollingFileSinkTests.cs b/test/Serilog.Sinks.File.Tests/RollingFileSinkTests.cs new file mode 100644 index 0000000..3efe3f9 --- /dev/null +++ b/test/Serilog.Sinks.File.Tests/RollingFileSinkTests.cs @@ -0,0 +1,177 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using Xunit; +using Serilog.Events; +using Serilog.Sinks.File.Tests.Support; +using Serilog.Configuration; + +namespace Serilog.Sinks.File.Tests +{ + public class RollingFileSinkTests + { + [Fact] + public void LogEventsAreEmittedToTheFileNamedAccordingToTheEventTimestamp() + { + TestRollingEventSequence(Some.InformationEvent()); + } + + [Fact] + public void EventsAreWrittenWhenSharingIsEnabled() + { + TestRollingEventSequence( + (pf, wt) => wt.File(pf, shared: true, rollingInterval: RollingInterval.Day), + new[] { Some.InformationEvent() }); + } + + [Fact] + public void EventsAreWrittenWhenBufferingIsEnabled() + { + TestRollingEventSequence( + (pf, wt) => wt.File(pf, buffered: true, rollingInterval: RollingInterval.Day), + new[] { Some.InformationEvent() }); + } + + [Fact] + public void EventsAreWrittenWhenDiskFlushingIsEnabled() + { + // Doesn't test flushing, but ensures we haven't broken basic logging + TestRollingEventSequence( + (pf, wt) => wt.File(pf, flushToDiskInterval: TimeSpan.FromMilliseconds(50), rollingInterval: RollingInterval.Day), + new[] { Some.InformationEvent() }); + } + + [Fact] + public void WhenTheDateChangesTheCorrectFileIsWritten() + { + var e1 = Some.InformationEvent(); + var e2 = Some.InformationEvent(e1.Timestamp.AddDays(1)); + TestRollingEventSequence(e1, e2); + } + + [Fact] + public void WhenRetentionCountIsSetOldFilesAreDeleted() + { + LogEvent e1 = Some.InformationEvent(), + e2 = Some.InformationEvent(e1.Timestamp.AddDays(1)), + e3 = Some.InformationEvent(e2.Timestamp.AddDays(5)); + + TestRollingEventSequence( + (pf, wt) => wt.File(pf, retainedFileCountLimit: 2, rollingInterval: RollingInterval.Day), + new[] {e1, e2, e3}, + files => + { + Assert.Equal(3, files.Count); + Assert.True(!System.IO.File.Exists(files[0])); + Assert.True(System.IO.File.Exists(files[1])); + Assert.True(System.IO.File.Exists(files[2])); + }); + } + + [Fact] + public void 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]); + } + } + + [Fact] + public void IfTheLogFolderDoesNotExistItWillBeCreated() + { + var fileName = Some.String() + "-{Date}.txt"; + var temp = Some.TempFolderPath(); + var folder = Path.Combine(temp, Guid.NewGuid().ToString()); + var pathFormat = Path.Combine(folder, fileName); + + ILogger log = null; + + try + { + log = new LoggerConfiguration() + .WriteTo.File(pathFormat, retainedFileCountLimit: 3, rollingInterval: RollingInterval.Day) + .CreateLogger(); + + log.Write(Some.InformationEvent()); + + Assert.True(Directory.Exists(folder)); + } + finally + { + var disposable = (IDisposable)log; + if (disposable != null) disposable.Dispose(); + Directory.Delete(temp, true); + } + } + + [Fact] + public void AssemblyVersionIsFixedAt200() + { + var assembly = typeof(FileLoggerConfigurationExtensions).GetTypeInfo().Assembly; + Assert.Equal("2.0.0.0", assembly.GetName().Version.ToString(4)); + } + + static void TestRollingEventSequence(params LogEvent[] events) + { + TestRollingEventSequence( + (pf, wt) => wt.File(pf, retainedFileCountLimit: null, rollingInterval: RollingInterval.Day), + events); + } + + static void TestRollingEventSequence( + Action configureFile, + IEnumerable events, + Action> verifyWritten = null) + { + var fileName = Some.String() + "-.txt"; + var folder = Some.TempFolderPath(); + var pathFormat = Path.Combine(folder, fileName); + + var config = new LoggerConfiguration(); + configureFile(pathFormat, config.WriteTo); + var log = config.CreateLogger(); + + var verified = new List(); + + try + { + foreach (var @event in events) + { + Clock.SetTestDateTimeNow(@event.Timestamp.DateTime); + log.Write(@event); + + var expected = pathFormat.Replace(".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); + } + } + } +} diff --git a/test/Serilog.Sinks.File.Tests/RollingIntervalExtensionsTests.cs b/test/Serilog.Sinks.File.Tests/RollingIntervalExtensionsTests.cs new file mode 100644 index 0000000..2d97d1b --- /dev/null +++ b/test/Serilog.Sinks.File.Tests/RollingIntervalExtensionsTests.cs @@ -0,0 +1,34 @@ +using System; +using Xunit; + +namespace Serilog.Sinks.File.Tests +{ + public class RollingIntervalExtensionsTests + { + public static object[][] IntervalInstantCurrentNextCheckpoint => new[] + { + new object[]{ RollingInterval.Infinite, new DateTime(2018, 01, 01), null, null }, + new object[]{ RollingInterval.Year, new DateTime(2018, 01, 01), new DateTime(2018, 01, 01), new DateTime(2019, 01, 01) }, + new object[]{ RollingInterval.Year, new DateTime(2018, 06, 01), new DateTime(2018, 01, 01), new DateTime(2019, 01, 01) }, + new object[]{ RollingInterval.Month, new DateTime(2018, 01, 01), new DateTime(2018, 01, 01), new DateTime(2018, 02, 01) }, + new object[]{ RollingInterval.Month, new DateTime(2018, 01, 14), new DateTime(2018, 01, 01), new DateTime(2018, 02, 01) }, + new object[]{ RollingInterval.Day, new DateTime(2018, 01, 01), new DateTime(2018, 01, 01), new DateTime(2018, 01, 02) }, + new object[]{ RollingInterval.Day, new DateTime(2018, 01, 01, 12, 0, 0), new DateTime(2018, 01, 01), new DateTime(2018, 01, 02) }, + new object[]{ RollingInterval.Hour, new DateTime(2018, 01, 01, 0, 0, 0), new DateTime(2018, 01, 01), new DateTime(2018, 01, 01, 1, 0, 0) }, + new object[]{ RollingInterval.Hour, new DateTime(2018, 01, 01, 0, 30, 0), new DateTime(2018, 01, 01), new DateTime(2018, 01, 01, 1, 0, 0) }, + new object[]{ RollingInterval.Minute, new DateTime(2018, 01, 01, 0, 0, 0), new DateTime(2018, 01, 01), new DateTime(2018, 01, 01, 0, 1, 0) }, + new object[]{ RollingInterval.Minute, new DateTime(2018, 01, 01, 0, 0, 30), new DateTime(2018, 01, 01), new DateTime(2018, 01, 01, 0, 1, 0) } + }; + + [Theory] + [MemberData(nameof(IntervalInstantCurrentNextCheckpoint))] + public void NextIntervalTests(RollingInterval interval, DateTime instant, DateTime? currentCheckpoint, DateTime? nextCheckpoint) + { + var current = interval.GetCurrentCheckpoint(instant); + Assert.Equal(currentCheckpoint, current); + + var next = interval.GetNextCheckpoint(instant); + Assert.Equal(nextCheckpoint, next); + } + } +} diff --git a/test/Serilog.Sinks.File.Tests/Serilog.Sinks.File.Tests.csproj b/test/Serilog.Sinks.File.Tests/Serilog.Sinks.File.Tests.csproj new file mode 100644 index 0000000..3491e32 --- /dev/null +++ b/test/Serilog.Sinks.File.Tests/Serilog.Sinks.File.Tests.csproj @@ -0,0 +1,36 @@ + + + + net452;netcoreapp1.0;netcoreapp2.0 + true + Serilog.Sinks.File.Tests + ../../assets/Serilog.snk + true + true + Serilog.Sinks.RollingFile.Tests + true + $(PackageTargetFallback);dnxcore50;portable-net45+win8 + 1.0.4 + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/Serilog.Sinks.File.Tests/Serilog.Sinks.File.Tests.xproj b/test/Serilog.Sinks.File.Tests/Serilog.Sinks.File.Tests.xproj deleted file mode 100644 index 3234f8a..0000000 --- a/test/Serilog.Sinks.File.Tests/Serilog.Sinks.File.Tests.xproj +++ /dev/null @@ -1,21 +0,0 @@ - - - - 14.0 - $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) - - - - 3c2d8e01-5580-426a-bdd9-ec59cd98e618 - Serilog.Tests - .\obj - .\bin\ - - - 2.0 - - - - - - \ No newline at end of file diff --git a/test/Serilog.Sinks.File.Tests/SharedFileSinkTests.cs b/test/Serilog.Sinks.File.Tests/SharedFileSinkTests.cs index f63eac1..565be9b 100644 --- a/test/Serilog.Sinks.File.Tests/SharedFileSinkTests.cs +++ b/test/Serilog.Sinks.File.Tests/SharedFileSinkTests.cs @@ -2,7 +2,8 @@ using Xunit; using Serilog.Formatting.Json; using Serilog.Sinks.File.Tests.Support; -using Serilog.Tests.Support; + +#pragma warning disable 618 namespace Serilog.Sinks.File.Tests { diff --git a/test/Serilog.Sinks.File.Tests/Support/CollectingSink.cs b/test/Serilog.Sinks.File.Tests/Support/CollectingSink.cs new file mode 100644 index 0000000..244ae5c --- /dev/null +++ b/test/Serilog.Sinks.File.Tests/Support/CollectingSink.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; +using System.Linq; +using Serilog.Core; +using Serilog.Events; + +namespace Serilog.Sinks.File.Tests.Support +{ + class CollectingSink : ILogEventSink + { + readonly List _events = new List(); + + public List Events { get { return _events; } } + + public LogEvent SingleEvent { get { return _events.Single(); } } + + public void Emit(LogEvent logEvent) + { + _events.Add(logEvent); + } + } +} diff --git a/test/Serilog.Sinks.File.Tests/Support/DelegateDisposable.cs b/test/Serilog.Sinks.File.Tests/Support/DelegateDisposable.cs new file mode 100644 index 0000000..3ac9974 --- /dev/null +++ b/test/Serilog.Sinks.File.Tests/Support/DelegateDisposable.cs @@ -0,0 +1,24 @@ +using System; + +namespace Serilog.Sinks.File.Tests.Support +{ + public class DelegateDisposable : IDisposable + { + private readonly Action _disposeAction; + private bool _disposed; + + public DelegateDisposable(Action disposeAction) + { + _disposeAction = disposeAction; + } + + public void Dispose() + { + if (_disposed) + return; + + _disposeAction(); + _disposed = true; + } + } +} diff --git a/test/Serilog.Sinks.File.Tests/Support/DelegatingEnricher.cs b/test/Serilog.Sinks.File.Tests/Support/DelegatingEnricher.cs new file mode 100644 index 0000000..0a480fb --- /dev/null +++ b/test/Serilog.Sinks.File.Tests/Support/DelegatingEnricher.cs @@ -0,0 +1,22 @@ +using System; +using Serilog.Core; +using Serilog.Events; + +namespace Serilog.Sinks.File.Tests.Support +{ + class DelegatingEnricher : ILogEventEnricher + { + readonly Action _enrich; + + public DelegatingEnricher(Action enrich) + { + if (enrich == null) throw new ArgumentNullException(nameof(enrich)); + _enrich = enrich; + } + + public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory) + { + _enrich(logEvent, propertyFactory); + } + } +} diff --git a/test/Serilog.Sinks.File.Tests/Support/DelegatingSink.cs b/test/Serilog.Sinks.File.Tests/Support/DelegatingSink.cs new file mode 100644 index 0000000..9d81cc2 --- /dev/null +++ b/test/Serilog.Sinks.File.Tests/Support/DelegatingSink.cs @@ -0,0 +1,33 @@ +using System; +using Serilog.Core; +using Serilog.Events; + +namespace Serilog.Sinks.File.Tests.Support +{ + public class DelegatingSink : ILogEventSink + { + readonly Action _write; + + public DelegatingSink(Action write) + { + if (write == null) throw new ArgumentNullException(nameof(write)); + _write = write; + } + + public void Emit(LogEvent logEvent) + { + _write(logEvent); + } + + public static LogEvent GetLogEvent(Action writeAction) + { + LogEvent result = null; + var l = new LoggerConfiguration() + .WriteTo.Sink(new DelegatingSink(le => result = le)) + .CreateLogger(); + + writeAction(l); + return result; + } + } +} diff --git a/test/Serilog.Sinks.File.Tests/Support/DisposableLogger.cs b/test/Serilog.Sinks.File.Tests/Support/DisposableLogger.cs new file mode 100644 index 0000000..befcbd4 --- /dev/null +++ b/test/Serilog.Sinks.File.Tests/Support/DisposableLogger.cs @@ -0,0 +1,422 @@ +using System; +using System.Collections.Generic; +using Serilog.Core; +using Serilog.Events; + +namespace Serilog.Sinks.File.Tests.Support +{ + public class DisposableLogger : ILogger, IDisposable + { + public bool Disposed { get; set; } + + public void Dispose() + { + Disposed = true; + } + + public ILogger ForContext(ILogEventEnricher enricher) + { + throw new NotImplementedException(); + } + + public ILogger ForContext(IEnumerable enrichers) + { + throw new NotImplementedException(); + } + + public ILogger ForContext(string propertyName, object value, bool destructureObjects = false) + { + throw new NotImplementedException(); + } + + public ILogger ForContext() + { + throw new NotImplementedException(); + } + + public ILogger ForContext(Type source) + { + throw new NotImplementedException(); + } + + public void Write(LogEvent logEvent) + { + throw new NotImplementedException(); + } + + public void Write(LogEventLevel level, string messageTemplate) + { + throw new NotImplementedException(); + } + + public void Write(LogEventLevel level, string messageTemplate, T propertyValue) + { + throw new NotImplementedException(); + } + + public void Write(LogEventLevel level, string messageTemplate, T0 propertyValue0, T1 propertyValue1) + { + throw new NotImplementedException(); + } + + public void Write(LogEventLevel level, string messageTemplate, T0 propertyValue0, T1 propertyValue1, + T2 propertyValue2) + { + throw new NotImplementedException(); + } + + public void Write(LogEventLevel level, string messageTemplate, params object[] propertyValues) + { + throw new NotImplementedException(); + } + + public void Write(LogEventLevel level, Exception exception, string messageTemplate) + { + throw new NotImplementedException(); + } + + public void Write(LogEventLevel level, Exception exception, string messageTemplate, T propertyValue) + { + throw new NotImplementedException(); + } + + public void Write(LogEventLevel level, Exception exception, string messageTemplate, T0 propertyValue0, + T1 propertyValue1) + { + throw new NotImplementedException(); + } + + public void Write(LogEventLevel level, Exception exception, string messageTemplate, T0 propertyValue0, + T1 propertyValue1, T2 propertyValue2) + { + throw new NotImplementedException(); + } + + public void Write(LogEventLevel level, Exception exception, string messageTemplate, params object[] propertyValues) + { + throw new NotImplementedException(); + } + + public bool IsEnabled(LogEventLevel level) + { + throw new NotImplementedException(); + } + + public void Verbose(string messageTemplate) + { + throw new NotImplementedException(); + } + + public void Verbose(string messageTemplate, T propertyValue) + { + throw new NotImplementedException(); + } + + public void Verbose(string messageTemplate, T0 propertyValue0, T1 propertyValue1) + { + throw new NotImplementedException(); + } + + public void Verbose(string messageTemplate, T0 propertyValue0, T1 propertyValue1, T2 propertyValue2) + { + throw new NotImplementedException(); + } + + public void Verbose(string messageTemplate, params object[] propertyValues) + { + throw new NotImplementedException(); + } + + public void Verbose(Exception exception, string messageTemplate) + { + throw new NotImplementedException(); + } + + public void Verbose(Exception exception, string messageTemplate, T propertyValue) + { + throw new NotImplementedException(); + } + + public void Verbose(Exception exception, string messageTemplate, T0 propertyValue0, T1 propertyValue1) + { + throw new NotImplementedException(); + } + + public void Verbose(Exception exception, string messageTemplate, T0 propertyValue0, T1 propertyValue1, + T2 propertyValue2) + { + throw new NotImplementedException(); + } + + public void Verbose(Exception exception, string messageTemplate, params object[] propertyValues) + { + throw new NotImplementedException(); + } + + public void Debug(string messageTemplate) + { + throw new NotImplementedException(); + } + + public void Debug(string messageTemplate, T propertyValue) + { + throw new NotImplementedException(); + } + + public void Debug(string messageTemplate, T0 propertyValue0, T1 propertyValue1) + { + throw new NotImplementedException(); + } + + public void Debug(string messageTemplate, T0 propertyValue0, T1 propertyValue1, T2 propertyValue2) + { + throw new NotImplementedException(); + } + + public void Debug(string messageTemplate, params object[] propertyValues) + { + throw new NotImplementedException(); + } + + public void Debug(Exception exception, string messageTemplate) + { + throw new NotImplementedException(); + } + + public void Debug(Exception exception, string messageTemplate, T propertyValue) + { + throw new NotImplementedException(); + } + + public void Debug(Exception exception, string messageTemplate, T0 propertyValue0, T1 propertyValue1) + { + throw new NotImplementedException(); + } + + public void Debug(Exception exception, string messageTemplate, T0 propertyValue0, T1 propertyValue1, + T2 propertyValue2) + { + throw new NotImplementedException(); + } + + public void Debug(Exception exception, string messageTemplate, params object[] propertyValues) + { + throw new NotImplementedException(); + } + + public void Information(string messageTemplate) + { + throw new NotImplementedException(); + } + + public void Information(string messageTemplate, T propertyValue) + { + throw new NotImplementedException(); + } + + public void Information(string messageTemplate, T0 propertyValue0, T1 propertyValue1) + { + throw new NotImplementedException(); + } + + public void Information(string messageTemplate, T0 propertyValue0, T1 propertyValue1, T2 propertyValue2) + { + throw new NotImplementedException(); + } + + public void Information(string messageTemplate, params object[] propertyValues) + { + throw new NotImplementedException(); + } + + public void Information(Exception exception, string messageTemplate) + { + throw new NotImplementedException(); + } + + public void Information(Exception exception, string messageTemplate, T propertyValue) + { + throw new NotImplementedException(); + } + + public void Information(Exception exception, string messageTemplate, T0 propertyValue0, T1 propertyValue1) + { + throw new NotImplementedException(); + } + + public void Information(Exception exception, string messageTemplate, T0 propertyValue0, T1 propertyValue1, + T2 propertyValue2) + { + throw new NotImplementedException(); + } + + public void Information(Exception exception, string messageTemplate, params object[] propertyValues) + { + throw new NotImplementedException(); + } + + public void Warning(string messageTemplate) + { + throw new NotImplementedException(); + } + + public void Warning(string messageTemplate, T propertyValue) + { + throw new NotImplementedException(); + } + + public void Warning(string messageTemplate, T0 propertyValue0, T1 propertyValue1) + { + throw new NotImplementedException(); + } + + public void Warning(string messageTemplate, T0 propertyValue0, T1 propertyValue1, T2 propertyValue2) + { + throw new NotImplementedException(); + } + + public void Warning(string messageTemplate, params object[] propertyValues) + { + throw new NotImplementedException(); + } + + public void Warning(Exception exception, string messageTemplate) + { + throw new NotImplementedException(); + } + + public void Warning(Exception exception, string messageTemplate, T propertyValue) + { + throw new NotImplementedException(); + } + + public void Warning(Exception exception, string messageTemplate, T0 propertyValue0, T1 propertyValue1) + { + throw new NotImplementedException(); + } + + public void Warning(Exception exception, string messageTemplate, T0 propertyValue0, T1 propertyValue1, + T2 propertyValue2) + { + throw new NotImplementedException(); + } + + public void Warning(Exception exception, string messageTemplate, params object[] propertyValues) + { + throw new NotImplementedException(); + } + + public void Error(string messageTemplate) + { + throw new NotImplementedException(); + } + + public void Error(string messageTemplate, T propertyValue) + { + throw new NotImplementedException(); + } + + public void Error(string messageTemplate, T0 propertyValue0, T1 propertyValue1) + { + throw new NotImplementedException(); + } + + public void Error(string messageTemplate, T0 propertyValue0, T1 propertyValue1, T2 propertyValue2) + { + throw new NotImplementedException(); + } + + public void Error(string messageTemplate, params object[] propertyValues) + { + throw new NotImplementedException(); + } + + public void Error(Exception exception, string messageTemplate) + { + throw new NotImplementedException(); + } + + public void Error(Exception exception, string messageTemplate, T propertyValue) + { + throw new NotImplementedException(); + } + + public void Error(Exception exception, string messageTemplate, T0 propertyValue0, T1 propertyValue1) + { + throw new NotImplementedException(); + } + + public void Error(Exception exception, string messageTemplate, T0 propertyValue0, T1 propertyValue1, + T2 propertyValue2) + { + throw new NotImplementedException(); + } + + public void Error(Exception exception, string messageTemplate, params object[] propertyValues) + { + throw new NotImplementedException(); + } + + public void Fatal(string messageTemplate) + { + throw new NotImplementedException(); + } + + public void Fatal(string messageTemplate, T propertyValue) + { + throw new NotImplementedException(); + } + + public void Fatal(string messageTemplate, T0 propertyValue0, T1 propertyValue1) + { + throw new NotImplementedException(); + } + + public void Fatal(string messageTemplate, T0 propertyValue0, T1 propertyValue1, T2 propertyValue2) + { + throw new NotImplementedException(); + } + + public void Fatal(string messageTemplate, params object[] propertyValues) + { + throw new NotImplementedException(); + } + + public void Fatal(Exception exception, string messageTemplate) + { + throw new NotImplementedException(); + } + + public void Fatal(Exception exception, string messageTemplate, T propertyValue) + { + throw new NotImplementedException(); + } + + public void Fatal(Exception exception, string messageTemplate, T0 propertyValue0, T1 propertyValue1) + { + throw new NotImplementedException(); + } + + public void Fatal(Exception exception, string messageTemplate, T0 propertyValue0, T1 propertyValue1, + T2 propertyValue2) + { + throw new NotImplementedException(); + } + + public void Fatal(Exception exception, string messageTemplate, params object[] propertyValues) + { + throw new NotImplementedException(); + } + + public bool BindMessageTemplate(string messageTemplate, object[] propertyValues, out MessageTemplate parsedTemplate, + out IEnumerable boundProperties) + { + throw new NotImplementedException(); + } + + public bool BindProperty(string propertyName, object value, bool destructureObjects, out LogEventProperty property) + { + throw new NotImplementedException(); + } + } +} diff --git a/test/Serilog.Sinks.File.Tests/Support/DisposeTrackingSink.cs b/test/Serilog.Sinks.File.Tests/Support/DisposeTrackingSink.cs new file mode 100644 index 0000000..29cac56 --- /dev/null +++ b/test/Serilog.Sinks.File.Tests/Support/DisposeTrackingSink.cs @@ -0,0 +1,20 @@ +using System; +using Serilog.Core; +using Serilog.Events; + +namespace Serilog.Sinks.File.Tests.Support +{ + class DisposeTrackingSink : ILogEventSink, IDisposable + { + public bool IsDisposed { get; set; } + + public void Emit(LogEvent logEvent) + { + } + + public void Dispose() + { + IsDisposed = true; + } + } +} diff --git a/test/Serilog.Sinks.File.Tests/Support/Extensions.cs b/test/Serilog.Sinks.File.Tests/Support/Extensions.cs new file mode 100644 index 0000000..a31122d --- /dev/null +++ b/test/Serilog.Sinks.File.Tests/Support/Extensions.cs @@ -0,0 +1,12 @@ +using Serilog.Events; + +namespace Serilog.Sinks.File.Tests.Support +{ + public static class Extensions + { + public static object LiteralValue(this LogEventPropertyValue @this) + { + return ((ScalarValue)@this).Value; + } + } +} diff --git a/test/Serilog.Sinks.File.Tests/Support/Some.cs b/test/Serilog.Sinks.File.Tests/Support/Some.cs index a831492..2d29d4d 100644 --- a/test/Serilog.Sinks.File.Tests/Support/Some.cs +++ b/test/Serilog.Sinks.File.Tests/Support/Some.cs @@ -1,12 +1,48 @@ using System; using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; using Serilog.Events; +using Serilog.Parsing; using Xunit.Sdk; -namespace Serilog.Tests.Support +namespace Serilog.Sinks.File.Tests.Support { static class Some { + static int _counter; + + public static int Int() + { + return Interlocked.Increment(ref _counter); + } + + public static decimal Decimal() + { + return Int() + 0.123m; + } + + public static string String(string tag = null) + { + return (tag ?? "") + "__" + Int(); + } + + public static TimeSpan TimeSpan() + { + return System.TimeSpan.FromMinutes(Int()); + } + + public static DateTime Instant() + { + return new DateTime(2012, 10, 28) + TimeSpan(); + } + + public static DateTimeOffset OffsetInstant() + { + return new DateTimeOffset(Instant()); + } + public static LogEvent LogEvent(string messageTemplate, params object[] propertyValues) { var log = new LoggerConfiguration().CreateLogger(); @@ -20,5 +56,48 @@ public static LogEvent LogEvent(string messageTemplate, params object[] property } 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()); + } + + public static LogEvent InformationEvent(DateTimeOffset? timestamp = null) + { + return LogEvent(timestamp, LogEventLevel.Information); + } + + public static LogEvent DebugEvent(DateTimeOffset? timestamp = null) + { + return LogEvent(timestamp, LogEventLevel.Debug); + } + + public static LogEventProperty LogEventProperty() + { + return new LogEventProperty(String(), new ScalarValue(Int())); + } + + public static string NonexistentTempFilePath() + { + return Path.Combine(Path.GetTempPath(), Guid.NewGuid() + ".txt"); + } + + public static string TempFilePath() + { + return Path.GetTempFileName(); + } + + public static string TempFolderPath() + { + var dir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + Directory.CreateDirectory(dir); + return dir; + } + + public static MessageTemplate MessageTemplate() + { + return new MessageTemplateParser().Parse(String()); + } } } diff --git a/test/Serilog.Sinks.File.Tests/Support/TempFolder.cs b/test/Serilog.Sinks.File.Tests/Support/TempFolder.cs index f809c05..7ff90f8 100644 --- a/test/Serilog.Sinks.File.Tests/Support/TempFolder.cs +++ b/test/Serilog.Sinks.File.Tests/Support/TempFolder.cs @@ -11,13 +11,13 @@ class TempFolder : IDisposable readonly string _tempFolder; - public TempFolder(string name) + public TempFolder(string name = null) { _tempFolder = System.IO.Path.Combine( - Environment.GetEnvironmentVariable("TMP"), + Environment.GetEnvironmentVariable("TMP") ?? Environment.GetEnvironmentVariable("TMPDIR") ?? "/tmp", "Serilog.Sinks.File.Tests", Session.ToString("n"), - name); + name ?? Guid.NewGuid().ToString("n")); Directory.CreateDirectory(_tempFolder); } diff --git a/test/Serilog.Sinks.File.Tests/TemplatedPathRollerTests.cs b/test/Serilog.Sinks.File.Tests/TemplatedPathRollerTests.cs new file mode 100644 index 0000000..5e1b015 --- /dev/null +++ b/test/Serilog.Sinks.File.Tests/TemplatedPathRollerTests.cs @@ -0,0 +1,104 @@ +using System; +using System.IO; +using System.Linq; +using Xunit; + +namespace Serilog.Sinks.File.Tests +{ + public class PathRollerTests + { + [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); + string path; + roller.GetLogFilePath(now, null, out 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); + string path; + roller.GetLogFilePath(now, 12, out 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); + } + + [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); + string path; + roller.GetLogFilePath(now, null, out 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); + string path; + roller.GetLogFilePath(now, null, out 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-helloyou.txt"; + var matched = roller.SelectMatches(new[] { similar1, similar2 }); + Assert.Equal(0, matched.Count()); + } + + [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.Equal(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); + } + } +} + diff --git a/test/Serilog.Sinks.File.Tests/project.json b/test/Serilog.Sinks.File.Tests/project.json deleted file mode 100644 index 3f14b0e..0000000 --- a/test/Serilog.Sinks.File.Tests/project.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "testRunner": "xunit", - "dependencies": { - "Serilog.Sinks.File": { "target": "project" }, - "xunit": "2.1.0", - "dotnet-test-xunit": "1.0.0-rc2-build10025" - }, - "frameworks": { - "netcoreapp1.0": { - "dependencies": { - "Microsoft.NETCore.App": { - "type": "platform", - "version": "1.0.0" - } - }, - "imports": [ - "dnxcore50", - "portable-net45+win8" - ] - }, - "net4.5.2": { - } - } -}