diff --git a/.gitignore b/.gitignore index 483adfa..d8704ba 100644 --- a/.gitignore +++ b/.gitignore @@ -26,6 +26,9 @@ bld/ # Uncomment if you have tasks that create the project's static files in wwwroot #wwwroot/ +# Rider cache/options directory +.idea + # MSTest test Results [Tt]est[Rr]esult*/ [Bb]uild[Ll]og.* diff --git a/README.md b/README.md index 9e4f19f..4267d32 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Serilog.Sinks.File [![Build status](https://ci.appveyor.com/api/projects/status/hh9gymy0n6tne46j?svg=true)](https://ci.appveyor.com/project/serilog/serilog-sinks-file) [![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) +# Serilog.Sinks.File [![Build status](https://ci.appveyor.com/api/projects/status/hh9gymy0n6tne46j?svg=true)](https://ci.appveyor.com/project/serilog/serilog-sinks-file) [![NuGet Version](http://img.shields.io/nuget/v/Serilog.Sinks.File.svg?style=flat)](https://www.nuget.org/packages/Serilog.Sinks.File/) [![Documentation](https://img.shields.io/badge/docs-wiki-yellow.svg)](https://github.com/serilog/serilog/wiki) [![Join the chat at https://gitter.im/serilog/serilog](https://img.shields.io/gitter/room/serilog/serilog.svg)](https://gitter.im/serilog/serilog) Writes [Serilog](https://serilog.net) events to one or more text files. diff --git a/appveyor.yml b/appveyor.yml index 71f05e3..67a6f2b 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,11 +1,18 @@ version: '{build}' skip_tags: true -image: Visual Studio 2017 +image: + - Visual Studio 2017 + - Ubuntu configuration: Release -install: - - ps: mkdir -Force ".\build\" | Out-Null build_script: - ps: ./Build.ps1 +for: +- + matrix: + only: + - image: Ubuntu + build_script: + - sh build.sh test: off artifacts: - path: artifacts/Serilog.*.nupkg diff --git a/build.sh b/build.sh index 56d265b..3bb5fec 100755 --- a/build.sh +++ b/build.sh @@ -1,11 +1,17 @@ -#!/bin/bash +#!/bin/bash + +set -e dotnet --info +dotnet --list-sdks dotnet restore +echo "🤖 Attempting to build..." for path in src/**/*.csproj; do - dotnet build -f netstandard1.3 -c Release ${path} -p:EnableSourceLink=true + dotnet build -f netstandard1.3 -c Release ${path} + dotnet build -f netstandard2.0 -c Release ${path} done +echo "🤖 Running tests..." for path in test/*.Tests/*.csproj; do dotnet test -f netcoreapp2.0 -c Release ${path} done diff --git a/serilog-sinks-file.sln b/serilog-sinks-file.sln index 71527e4..9c33a2b 100644 --- a/serilog-sinks-file.sln +++ b/serilog-sinks-file.sln @@ -8,7 +8,6 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "assets", "assets", "{E9D1B5E1-DEB9-4A04-8BAB-24EC7240ADAF}" ProjectSection(SolutionItems) = preProject .editorconfig = .editorconfig - .travis.yml = .travis.yml appveyor.yml = appveyor.yml Build.ps1 = Build.ps1 build.sh = build.sh diff --git a/src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs b/src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs index 5cb19e9..490803d 100644 --- a/src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs +++ b/src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs @@ -24,7 +24,7 @@ using Serilog.Formatting.Json; using Serilog.Sinks.File; -// ReSharper disable MethodOverloadWithOptionalParameter +// ReSharper disable RedundantArgumentDefaultValue, MethodOverloadWithOptionalParameter namespace Serilog { @@ -69,10 +69,8 @@ public static LoggerConfiguration File( bool shared, TimeSpan? flushToDiskInterval) { - // ReSharper disable once RedundantArgumentDefaultValue return File(sinkConfiguration, path, restrictedToMinimumLevel, outputTemplate, formatProvider, fileSizeLimitBytes, - levelSwitch, buffered, shared, flushToDiskInterval, RollingInterval.Infinite, false, - null, null); + levelSwitch, buffered, shared, flushToDiskInterval, RollingInterval.Infinite, false, null, null, null); } /// @@ -110,9 +108,8 @@ public static LoggerConfiguration File( bool shared, TimeSpan? flushToDiskInterval) { - // ReSharper disable once RedundantArgumentDefaultValue return File(sinkConfiguration, formatter, path, restrictedToMinimumLevel, fileSizeLimitBytes, levelSwitch, - buffered, shared, flushToDiskInterval, RollingInterval.Infinite, false, null, null); + buffered, shared, flushToDiskInterval, RollingInterval.Infinite, false, null, null, null); } /// @@ -135,13 +132,108 @@ public static LoggerConfiguration File( /// Allow the log file to be shared by multiple processes. The default is false. /// If provided, a full disk flush will be performed periodically at the specified interval. /// The interval at which logging will roll over to a new file. - /// If true, a new file will be created when the file size limit is reached. Filenames + /// If true, a new file will be created when the file size limit is reached. Filenames /// will have a number appended in the format _NNN, with the first filename given no number. /// The maximum number of log files that will be retained, /// including the current log file. For unlimited retention, pass null. The default is 31. /// Character encoding used to write the text file. The default is UTF-8 without BOM. /// 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, + RollingInterval rollingInterval, + bool rollOnFileSizeLimit, + int? retainedFileCountLimit, + Encoding encoding) + { + return File(sinkConfiguration, path, restrictedToMinimumLevel, outputTemplate, formatProvider, fileSizeLimitBytes, levelSwitch, buffered, + shared, flushToDiskInterval, rollingInterval, rollOnFileSizeLimit, retainedFileCountLimit, encoding, null); + } + + /// + /// Write log events to the specified file. + /// + /// Logger sink configuration. + /// A formatter, such as , to convert the log events into + /// text for the file. If control of regular text formatting is required, use the other + /// overload of + /// and specify the outputTemplate parameter instead. + /// + /// Path to the file. + /// The minimum level for + /// events passed through the sink. Ignored when is specified. + /// A switch allowing the pass-through minimum level + /// to be changed at runtime. + /// The approximate maximum size, in bytes, to which a log file will be allowed to grow. + /// For unrestricted growth, pass null. The default is 1 GB. To avoid writing partial events, the last event within the limit + /// will be written in full even if it exceeds the limit. + /// Indicates if flushing to the output file can be buffered or not. The default + /// is false. + /// Allow the log file to be shared by multiple processes. The default is false. + /// If provided, a full disk flush will be performed periodically at the specified interval. + /// The interval at which logging will roll over to a new file. + /// If true, a new file will be created when the file size limit is reached. Filenames + /// will have a number appended in the format _NNN, with the first filename given no number. + /// The maximum number of log files that will be retained, + /// including the current log file. For unlimited retention, pass null. The default is 31. + /// Character encoding used to write the text file. The default is UTF-8 without BOM. + /// Configuration object allowing method chaining. + [Obsolete("New code should not be compiled against this obsolete overload"), EditorBrowsable(EditorBrowsableState.Never)] + public static LoggerConfiguration File( + this LoggerSinkConfiguration sinkConfiguration, + ITextFormatter formatter, + string path, + LogEventLevel restrictedToMinimumLevel, + long? fileSizeLimitBytes, + LoggingLevelSwitch levelSwitch, + bool buffered, + bool shared, + TimeSpan? flushToDiskInterval, + RollingInterval rollingInterval, + bool rollOnFileSizeLimit, + int? retainedFileCountLimit, + Encoding encoding) + { + return File(sinkConfiguration, formatter, path, restrictedToMinimumLevel, fileSizeLimitBytes, levelSwitch, buffered, + shared, flushToDiskInterval, rollingInterval, rollOnFileSizeLimit, retainedFileCountLimit, encoding, null); + } + + /// + /// Write log events to the specified file. + /// + /// Logger sink configuration. + /// Path to the file. + /// The minimum level for + /// events passed through the sink. Ignored when is specified. + /// A switch allowing the pass-through minimum level + /// to be changed at runtime. + /// Supplies culture-specific formatting information, or null. + /// A message template describing the format used to write to the sink. + /// the default is "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception}". + /// The approximate maximum size, in bytes, to which a log file will be allowed to grow. + /// For unrestricted growth, pass null. The default is 1 GB. To avoid writing partial events, the last event within the limit + /// will be written in full even if it exceeds the limit. + /// Indicates if flushing to the output file can be buffered or not. The default + /// is false. + /// Allow the log file to be shared by multiple processes. The default is false. + /// If provided, a full disk flush will be performed periodically at the specified interval. + /// The interval at which logging will roll over to a new file. + /// If true, a new file will be created when the file size limit is reached. Filenames + /// will have a number appended in the format _NNN, with the first filename given no number. + /// The maximum number of log files that will be retained, + /// including the current log file. For unlimited retention, pass null. The default is 31. + /// Character encoding used to write the text file. The default is UTF-8 without BOM. + /// Optionally enables hooking into log file lifecycle events. + /// Configuration object allowing method chaining. public static LoggerConfiguration File( this LoggerSinkConfiguration sinkConfiguration, string path, @@ -156,7 +248,8 @@ public static LoggerConfiguration File( RollingInterval rollingInterval = RollingInterval.Infinite, bool rollOnFileSizeLimit = false, int? retainedFileCountLimit = DefaultRetainedFileCountLimit, - Encoding encoding = null) + Encoding encoding = null, + FileLifecycleHooks hooks = null) { if (sinkConfiguration == null) throw new ArgumentNullException(nameof(sinkConfiguration)); if (path == null) throw new ArgumentNullException(nameof(path)); @@ -165,7 +258,7 @@ public static LoggerConfiguration File( var formatter = new MessageTemplateTextFormatter(outputTemplate, formatProvider); return File(sinkConfiguration, formatter, path, restrictedToMinimumLevel, fileSizeLimitBytes, levelSwitch, buffered, shared, flushToDiskInterval, - rollingInterval, rollOnFileSizeLimit, retainedFileCountLimit, encoding); + rollingInterval, rollOnFileSizeLimit, retainedFileCountLimit, encoding, hooks); } /// @@ -174,7 +267,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. @@ -190,13 +283,13 @@ public static LoggerConfiguration File( /// Allow the log file to be shared by multiple processes. The default is false. /// If provided, a full disk flush will be performed periodically at the specified interval. /// The interval at which logging will roll over to a new file. - /// If true, a new file will be created when the file size limit is reached. Filenames + /// If true, a new file will be created when the file size limit is reached. Filenames /// will have a number appended in the format _NNN, with the first filename given no number. /// The maximum number of log files that will be retained, /// including the current log file. For unlimited retention, pass null. The default is 31. /// Character encoding used to write the text file. The default is UTF-8 without BOM. + /// Optionally enables hooking into log file lifecycle events. /// Configuration object allowing method chaining. - /// The file will be written using the UTF-8 character set. public static LoggerConfiguration File( this LoggerSinkConfiguration sinkConfiguration, ITextFormatter formatter, @@ -210,10 +303,16 @@ public static LoggerConfiguration File( RollingInterval rollingInterval = RollingInterval.Infinite, bool rollOnFileSizeLimit = false, int? retainedFileCountLimit = DefaultRetainedFileCountLimit, - Encoding encoding = null) + Encoding encoding = null, + FileLifecycleHooks hooks = null) { + if (sinkConfiguration == null) throw new ArgumentNullException(nameof(sinkConfiguration)); + if (formatter == null) throw new ArgumentNullException(nameof(formatter)); + if (path == null) throw new ArgumentNullException(nameof(path)); + return ConfigureFile(sinkConfiguration.Sink, formatter, path, restrictedToMinimumLevel, fileSizeLimitBytes, levelSwitch, - buffered, false, shared, flushToDiskInterval, encoding, rollingInterval, rollOnFileSizeLimit, retainedFileCountLimit); + buffered, false, shared, flushToDiskInterval, encoding, rollingInterval, rollOnFileSizeLimit, + retainedFileCountLimit, hooks); } /// @@ -230,29 +329,85 @@ public static LoggerConfiguration File( /// the default is "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception}". /// Configuration object allowing method chaining. /// The file will be written using the UTF-8 character set. + [Obsolete("New code should not be compiled against this obsolete overload"), EditorBrowsable(EditorBrowsableState.Never)] + public static LoggerConfiguration File( + this LoggerAuditSinkConfiguration sinkConfiguration, + string path, + LogEventLevel restrictedToMinimumLevel, + string outputTemplate, + IFormatProvider formatProvider, + LoggingLevelSwitch levelSwitch) + { + return File(sinkConfiguration, path, restrictedToMinimumLevel, outputTemplate, formatProvider, levelSwitch, null, null); + } + + /// + /// Write log events to the specified file. + /// + /// Logger sink configuration. + /// A formatter, such as , to convert the log events into + /// text for the file. If control of regular text formatting is required, use the other + /// overload of + /// and specify the outputTemplate parameter instead. + /// + /// Path to the file. + /// The minimum level for + /// events passed through the sink. Ignored when is specified. + /// A switch allowing the pass-through minimum level + /// to be changed at runtime. + /// Configuration object allowing method chaining. + /// The file will be written using the UTF-8 character set. + [Obsolete("New code should not be compiled against this obsolete overload"), EditorBrowsable(EditorBrowsableState.Never)] + public static LoggerConfiguration File( + this LoggerAuditSinkConfiguration sinkConfiguration, + ITextFormatter formatter, + string path, + LogEventLevel restrictedToMinimumLevel, + LoggingLevelSwitch levelSwitch) + { + return File(sinkConfiguration, formatter, path, restrictedToMinimumLevel, levelSwitch, null, null); + } + + /// + /// Write audit log events to the specified file. + /// + /// Logger sink configuration. + /// Path to the file. + /// The minimum level for + /// events passed through the sink. Ignored when is specified. + /// A switch allowing the pass-through minimum level + /// to be changed at runtime. + /// Supplies culture-specific formatting information, or null. + /// A message template describing the format used to write to the sink. + /// the default is "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception}". + /// Character encoding used to write the text file. The default is UTF-8 without BOM. + /// Optionally enables hooking into log file lifecycle events. + /// Configuration object allowing method chaining. public static LoggerConfiguration File( this LoggerAuditSinkConfiguration sinkConfiguration, string path, LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum, string outputTemplate = DefaultOutputTemplate, IFormatProvider formatProvider = null, - LoggingLevelSwitch levelSwitch = null) + LoggingLevelSwitch levelSwitch = null, + Encoding encoding = null, + FileLifecycleHooks hooks = null) { if (sinkConfiguration == null) throw new ArgumentNullException(nameof(sinkConfiguration)); if (path == null) throw new ArgumentNullException(nameof(path)); if (outputTemplate == null) throw new ArgumentNullException(nameof(outputTemplate)); var formatter = new MessageTemplateTextFormatter(outputTemplate, formatProvider); - return File(sinkConfiguration, formatter, path, restrictedToMinimumLevel, levelSwitch); + return File(sinkConfiguration, formatter, path, restrictedToMinimumLevel, levelSwitch, encoding, hooks); } /// - /// Write log events to the specified file. + /// Write audit log events to the specified file. /// /// Logger sink configuration. /// A formatter, such as , to convert the log events into /// text for the file. If control of regular text formatting is required, use the other - /// overload of + /// overload of /// and specify the outputTemplate parameter instead. /// /// Path to the file. @@ -260,17 +415,24 @@ public static LoggerConfiguration File( /// events passed through the sink. Ignored when is specified. /// A switch allowing the pass-through minimum level /// to be changed at runtime. + /// Character encoding used to write the text file. The default is UTF-8 without BOM. + /// Optionally enables hooking into log file lifecycle events. /// Configuration object allowing method chaining. - /// The file will be written using the UTF-8 character set. public static LoggerConfiguration File( this LoggerAuditSinkConfiguration sinkConfiguration, ITextFormatter formatter, string path, LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum, - LoggingLevelSwitch levelSwitch = null) + LoggingLevelSwitch levelSwitch = null, + Encoding encoding = null, + FileLifecycleHooks hooks = null) { + if (sinkConfiguration == null) throw new ArgumentNullException(nameof(sinkConfiguration)); + if (formatter == null) throw new ArgumentNullException(nameof(formatter)); + if (path == null) throw new ArgumentNullException(nameof(path)); + return ConfigureFile(sinkConfiguration.Sink, formatter, path, restrictedToMinimumLevel, null, levelSwitch, false, true, - false, null, null, RollingInterval.Infinite, false, null); + false, null, encoding, RollingInterval.Infinite, false, null, hooks); } static LoggerConfiguration ConfigureFile( @@ -287,7 +449,8 @@ static LoggerConfiguration ConfigureFile( Encoding encoding, RollingInterval rollingInterval, bool rollOnFileSizeLimit, - int? retainedFileCountLimit) + int? retainedFileCountLimit, + FileLifecycleHooks hooks) { if (addSink == null) throw new ArgumentNullException(nameof(addSink)); if (formatter == null) throw new ArgumentNullException(nameof(formatter)); @@ -295,27 +458,28 @@ static LoggerConfiguration ConfigureFile( 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)); + if (shared && hooks != null) throw new ArgumentException("File lifecycle hooks are not currently supported for shared log files.", nameof(hooks)); ILogEventSink sink; if (rollOnFileSizeLimit || rollingInterval != RollingInterval.Infinite) { - sink = new RollingFileSink(path, formatter, fileSizeLimitBytes, retainedFileCountLimit, encoding, buffered, shared, rollingInterval, rollOnFileSizeLimit); + sink = new RollingFileSink(path, formatter, fileSizeLimitBytes, retainedFileCountLimit, encoding, buffered, shared, rollingInterval, rollOnFileSizeLimit, hooks); } else { try { -#pragma warning disable 618 if (shared) { - sink = new SharedFileSink(path, formatter, fileSizeLimitBytes); +#pragma warning disable 618 + sink = new SharedFileSink(path, formatter, fileSizeLimitBytes, encoding); +#pragma warning restore 618 } else { - sink = new FileSink(path, formatter, fileSizeLimitBytes, buffered: buffered); + sink = new FileSink(path, formatter, fileSizeLimitBytes, encoding, buffered, hooks); } -#pragma warning restore 618 } catch (Exception ex) { @@ -330,7 +494,9 @@ static LoggerConfiguration ConfigureFile( if (flushToDiskInterval.HasValue) { +#pragma warning disable 618 sink = new PeriodicFlushToDiskSink(sink, flushToDiskInterval.Value); +#pragma warning restore 618 } return addSink(sink, restrictedToMinimumLevel, levelSwitch); diff --git a/src/Serilog.Sinks.File/Sinks/File/FileLifecycleHooks.cs b/src/Serilog.Sinks.File/Sinks/File/FileLifecycleHooks.cs new file mode 100644 index 0000000..3dbddeb --- /dev/null +++ b/src/Serilog.Sinks.File/Sinks/File/FileLifecycleHooks.cs @@ -0,0 +1,39 @@ +// Copyright 2019 Serilog Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.IO; +using System.Text; + +namespace Serilog.Sinks.File +{ + /// + /// Enables hooking into log file lifecycle events. + /// + public abstract class FileLifecycleHooks + { + /// + /// Initialize or wrap the opened on the log file. This can be used to write + /// file headers, or wrap the stream in another that adds buffering, compression, encryption, etc. The underlying + /// file may or may not be empty when this method is called. + /// + /// + /// A value must be returned from overrides of this method. Serilog will flush and/or dispose the returned value, but will not + /// dispose the stream initially passed in unless it is itself returned. + /// + /// The underlying opened on the log file. + /// The encoding to use when reading/writing to the stream. + /// The Serilog should use when writing events to the log file. + public virtual Stream OnFileOpened(Stream underlyingStream, Encoding encoding) => underlyingStream; + } +} diff --git a/src/Serilog.Sinks.File/Sinks/File/FileSink.cs b/src/Serilog.Sinks.File/Sinks/File/FileSink.cs index bfd288f..8a913fa 100644 --- a/src/Serilog.Sinks.File/Sinks/File/FileSink.cs +++ b/src/Serilog.Sinks.File/Sinks/File/FileSink.cs @@ -1,4 +1,4 @@ -// Copyright 2013-2016 Serilog Contributors +// Copyright 2013-2016 Serilog Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -23,7 +23,6 @@ namespace Serilog.Sinks.File /// /// Write log events to a disk file. /// - [Obsolete("This type will be removed from the public API in a future version; use `WriteTo.File()` instead.")] public sealed class FileSink : IFileSink, IDisposable { readonly TextWriter _output; @@ -44,15 +43,26 @@ public sealed class FileSink : IFileSink, IDisposable /// Indicates if flushing to the output file can be buffered or not. The default /// is false. /// Configuration object allowing method chaining. - /// The file will be written using the UTF-8 character set. + /// This constructor preserves compatibility with early versions of the public API. New code should not depend on this type. /// + [Obsolete("This type and constructor will be removed from the public API in a future version; use `WriteTo.File()` instead.")] public FileSink(string path, ITextFormatter textFormatter, long? fileSizeLimitBytes, Encoding encoding = null, bool buffered = false) + : this(path, textFormatter, fileSizeLimitBytes, encoding, buffered, null) + { + } + + // This overload should be used internally; the overload above maintains compatibility with the earlier public API. + internal FileSink( + string path, + ITextFormatter textFormatter, + long? fileSizeLimitBytes, + Encoding encoding, + bool buffered, + FileLifecycleHooks hooks) { 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."); - - _textFormatter = textFormatter; + _textFormatter = textFormatter ?? throw new ArgumentNullException(nameof(textFormatter)); _fileSizeLimitBytes = fileSizeLimitBytes; _buffered = buffered; @@ -68,7 +78,16 @@ public FileSink(string path, ITextFormatter textFormatter, long? fileSizeLimitBy outputStream = _countingStreamWrapper = new WriteCountingStream(_underlyingStream); } - _output = new StreamWriter(outputStream, encoding ?? new UTF8Encoding(encoderShouldEmitUTF8Identifier: false)); + // Parameter reassignment. + encoding = encoding ?? new UTF8Encoding(encoderShouldEmitUTF8Identifier: false); + + if (hooks != null) + { + outputStream = hooks.OnFileOpened(outputStream, encoding) ?? + throw new InvalidOperationException($"The file lifecycle hook `{nameof(FileLifecycleHooks.OnFileOpened)}(...)` returned `null`."); + } + + _output = new StreamWriter(outputStream, encoding); } bool IFileSink.EmitOrOverflow(LogEvent logEvent) diff --git a/src/Serilog.Sinks.File/Sinks/File/PeriodicFlushToDiskSink.cs b/src/Serilog.Sinks.File/Sinks/File/PeriodicFlushToDiskSink.cs index cafb72e..66b0868 100644 --- a/src/Serilog.Sinks.File/Sinks/File/PeriodicFlushToDiskSink.cs +++ b/src/Serilog.Sinks.File/Sinks/File/PeriodicFlushToDiskSink.cs @@ -1,4 +1,18 @@ -using System; +// Copyright 2016-2019 Serilog Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System; using System.Threading; using Serilog.Core; using Serilog.Debugging; @@ -9,6 +23,7 @@ namespace Serilog.Sinks.File /// /// A sink wrapper that periodically flushes the wrapped sink to disk. /// + [Obsolete("This type will be removed from the public API in a future version; use `WriteTo.File(flushToDiskInterval:)` instead.")] public class PeriodicFlushToDiskSink : ILogEventSink, IDisposable { readonly ILogEventSink _sink; @@ -21,15 +36,12 @@ public class PeriodicFlushToDiskSink : ILogEventSink, IDisposable /// /// The sink to wrap. /// The interval at which to flush the underlying sink. - /// + /// public PeriodicFlushToDiskSink(ILogEventSink sink, TimeSpan flushInterval) { - if (sink == null) throw new ArgumentNullException(nameof(sink)); - - _sink = sink; + _sink = sink ?? throw new ArgumentNullException(nameof(sink)); - var flushable = sink as IFlushableFileSink; - if (flushable != null) + if (sink is IFlushableFileSink flushable) { _timer = new Timer(_ => FlushToDisk(flushable), null, flushInterval, flushInterval); } diff --git a/src/Serilog.Sinks.File/Sinks/File/RollingFileSink.cs b/src/Serilog.Sinks.File/Sinks/File/RollingFileSink.cs index 644176f..2db6f24 100644 --- a/src/Serilog.Sinks.File/Sinks/File/RollingFileSink.cs +++ b/src/Serilog.Sinks.File/Sinks/File/RollingFileSink.cs @@ -12,8 +12,6 @@ // 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; @@ -35,6 +33,7 @@ sealed class RollingFileSink : ILogEventSink, IFlushableFileSink, IDisposable readonly bool _buffered; readonly bool _shared; readonly bool _rollOnFileSizeLimit; + readonly FileLifecycleHooks _hooks; readonly object _syncRoot = new object(); bool _isDisposed; @@ -50,11 +49,12 @@ public RollingFileSink(string path, bool buffered, bool shared, RollingInterval rollingInterval, - bool rollOnFileSizeLimit) + bool rollOnFileSizeLimit, + FileLifecycleHooks hooks) { if (path == null) throw new ArgumentNullException(nameof(path)); - if (fileSizeLimitBytes.HasValue && fileSizeLimitBytes < 0) throw new ArgumentException("Negative value provided; file size limit must be non-negative"); - if (retainedFileCountLimit.HasValue && retainedFileCountLimit < 1) throw new ArgumentException("Zero or negative value provided; retained file count limit must be at least 1"); + if (fileSizeLimitBytes.HasValue && fileSizeLimitBytes < 0) throw new ArgumentException("Negative value provided; file size limit must be non-negative."); + if (retainedFileCountLimit.HasValue && retainedFileCountLimit < 1) throw new ArgumentException("Zero or negative value provided; retained file count limit must be at least 1."); _roller = new PathRoller(path, rollingInterval); _textFormatter = textFormatter; @@ -64,6 +64,7 @@ public RollingFileSink(string path, _buffered = buffered; _shared = shared; _rollOnFileSizeLimit = rollOnFileSizeLimit; + _hooks = hooks; } public void Emit(LogEvent logEvent) @@ -117,8 +118,11 @@ void OpenFile(DateTime now, int? minSequence = null) var existingFiles = Enumerable.Empty(); try { - existingFiles = Directory.GetFiles(_roller.LogFileDirectory, _roller.DirectorySearchPattern) + if (Directory.Exists(_roller.LogFileDirectory)) + { + existingFiles = Directory.GetFiles(_roller.LogFileDirectory, _roller.DirectorySearchPattern) .Select(Path.GetFileName); + } } catch (DirectoryNotFoundException) { } @@ -143,8 +147,11 @@ void OpenFile(DateTime now, int? minSequence = null) try { _currentFile = _shared ? +#pragma warning disable 618 (IFileSink)new SharedFileSink(path, _textFormatter, _fileSizeLimitBytes, _encoding) : - new FileSink(path, _textFormatter, _fileSizeLimitBytes, _encoding, _buffered); +#pragma warning restore 618 + new FileSink(path, _textFormatter, _fileSizeLimitBytes, _encoding, _buffered, _hooks); + _currentFileSequence = sequence; } catch (IOException ex) diff --git a/src/Serilog.Sinks.File/Sinks/File/SharedFileSink.AtomicAppend.cs b/src/Serilog.Sinks.File/Sinks/File/SharedFileSink.AtomicAppend.cs index 805e786..866f807 100644 --- a/src/Serilog.Sinks.File/Sinks/File/SharedFileSink.AtomicAppend.cs +++ b/src/Serilog.Sinks.File/Sinks/File/SharedFileSink.AtomicAppend.cs @@ -1,4 +1,4 @@ -// Copyright 2013-2016 Serilog Contributors +// Copyright 2013-2019 Serilog Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -18,7 +18,6 @@ using System.IO; using System.Security.AccessControl; using System.Text; -using Serilog.Core; using Serilog.Events; using Serilog.Formatting; @@ -51,17 +50,14 @@ public sealed class SharedFileSink : IFileSink, IDisposable /// will be written in full even if it exceeds the limit. /// Character encoding used to write the text file. The default is UTF-8 without BOM. /// Configuration object allowing method chaining. - /// The file will be written using the UTF-8 character set. /// public SharedFileSink(string path, ITextFormatter textFormatter, long? fileSizeLimitBytes, Encoding encoding = null) { - 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"); - _path = path; - _textFormatter = textFormatter; + _path = path ?? throw new ArgumentNullException(nameof(path)); + _textFormatter = textFormatter ?? throw new ArgumentNullException(nameof(textFormatter)); _fileSizeLimitBytes = fileSizeLimitBytes; var directory = Path.GetDirectoryName(path); diff --git a/src/Serilog.Sinks.File/Sinks/File/SharedFileSink.OSMutex.cs b/src/Serilog.Sinks.File/Sinks/File/SharedFileSink.OSMutex.cs index a779bda..41a19ef 100644 --- a/src/Serilog.Sinks.File/Sinks/File/SharedFileSink.OSMutex.cs +++ b/src/Serilog.Sinks.File/Sinks/File/SharedFileSink.OSMutex.cs @@ -1,4 +1,4 @@ -// Copyright 2013-2016 Serilog Contributors +// Copyright 2013-2019 Serilog Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -17,7 +17,6 @@ using System; using System.IO; using System.Text; -using Serilog.Core; using Serilog.Events; using Serilog.Formatting; using System.Threading; @@ -28,6 +27,7 @@ namespace Serilog.Sinks.File /// /// Write log events to a disk file. /// + [Obsolete("This type will be removed from the public API in a future version; use `WriteTo.File(shared: true)` instead.")] public sealed class SharedFileSink : IFileSink, IDisposable { readonly TextWriter _output; @@ -53,11 +53,9 @@ public sealed class SharedFileSink : IFileSink, IDisposable public SharedFileSink(string path, ITextFormatter textFormatter, long? fileSizeLimitBytes, Encoding encoding = null) { if (path == null) throw new ArgumentNullException(nameof(path)); - if (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"); - - _textFormatter = textFormatter; + _textFormatter = textFormatter ?? throw new ArgumentNullException(nameof(textFormatter)); _fileSizeLimitBytes = fileSizeLimitBytes; var directory = Path.GetDirectoryName(path); diff --git a/src/Serilog.Sinks.File/Sinks/File/WriteCountingStream.cs b/src/Serilog.Sinks.File/Sinks/File/WriteCountingStream.cs index da8f0dd..fe0d5d3 100644 --- a/src/Serilog.Sinks.File/Sinks/File/WriteCountingStream.cs +++ b/src/Serilog.Sinks.File/Sinks/File/WriteCountingStream.cs @@ -1,4 +1,4 @@ -// Copyright 2013-2016 Serilog Contributors +// Copyright 2013-2019 Serilog Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -20,16 +20,14 @@ namespace Serilog.Sinks.File sealed class WriteCountingStream : Stream { readonly Stream _stream; - long _countedLength; public WriteCountingStream(Stream stream) { - if (stream == null) throw new ArgumentNullException(nameof(stream)); - _stream = stream; - _countedLength = stream.Length; + _stream = stream ?? throw new ArgumentNullException(nameof(stream)); + CountedLength = stream.Length; } - public long CountedLength => _countedLength; + public long CountedLength { get; private set; } protected override void Dispose(bool disposing) { @@ -42,7 +40,7 @@ protected override void Dispose(bool disposing) public override void Write(byte[] buffer, int offset, int count) { _stream.Write(buffer, offset, count); - _countedLength += count; + CountedLength += count; } public override void Flush() => _stream.Flush(); @@ -54,8 +52,8 @@ public override void Write(byte[] buffer, int offset, int count) public override long Position { - get { return _stream.Position; } - set { throw new NotSupportedException(); } + get => _stream.Position; + set => throw new NotSupportedException(); } public override long Seek(long offset, SeekOrigin origin) @@ -73,4 +71,4 @@ public override int Read(byte[] buffer, int offset, int count) throw new NotSupportedException(); } } -} \ No newline at end of file +} diff --git a/test/Serilog.Sinks.File.Tests/FileLoggerConfigurationExtensionsTests.cs b/test/Serilog.Sinks.File.Tests/FileLoggerConfigurationExtensionsTests.cs index 0515655..3dde37a 100644 --- a/test/Serilog.Sinks.File.Tests/FileLoggerConfigurationExtensionsTests.cs +++ b/test/Serilog.Sinks.File.Tests/FileLoggerConfigurationExtensionsTests.cs @@ -1,9 +1,10 @@ -using System; +using System; using System.Threading; using Serilog.Sinks.File.Tests.Support; using Serilog.Tests.Support; using Xunit; using System.IO; +using System.Text; namespace Serilog.Sinks.File.Tests { @@ -86,5 +87,34 @@ public void BufferingIsNotAvailableWhenSharingEnabled() new LoggerConfiguration() .WriteTo.File("logs", buffered: true, shared: true)); } + + [Fact] + public void HooksAreNotAvailableWhenSharingEnabled() + { + Assert.Throws(() => + new LoggerConfiguration() + .WriteTo.File("logs", shared: true, hooks: new GZipHooks())); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void SpecifiedEncodingIsPropagated(bool shared) + { + using (var tmp = TempFolder.ForCaller()) + { + var filename = tmp.AllocateFilename("txt"); + + using (var log = new LoggerConfiguration() + .WriteTo.File(filename, outputTemplate: "{Message}", encoding: Encoding.Unicode, shared: shared) + .CreateLogger()) + { + log.Information("ten chars."); + } + + // Don't forget the two-byte BOM :-) + Assert.Equal(22, System.IO.File.ReadAllBytes(filename).Length); + } + } } } diff --git a/test/Serilog.Sinks.File.Tests/FileSinkTests.cs b/test/Serilog.Sinks.File.Tests/FileSinkTests.cs index ea9a5d4..2d0f210 100644 --- a/test/Serilog.Sinks.File.Tests/FileSinkTests.cs +++ b/test/Serilog.Sinks.File.Tests/FileSinkTests.cs @@ -1,9 +1,11 @@ -using System.IO; +using System.Collections.Generic; +using System.IO; +using System.IO.Compression; +using System.Text; using Xunit; using Serilog.Formatting.Json; using Serilog.Sinks.File.Tests.Support; using Serilog.Tests.Support; -using System.Text; #pragma warning disable 618 @@ -141,13 +143,76 @@ public void WhenLimitIsNotSpecifiedAndEncodingHasNoPreambleDataIsCorrectlyAppend WriteTwoEventsAndCheckOutputFileLength(null, encoding); } + [Fact] + public void OnOpenedLifecycleHookCanWrapUnderlyingStream() + { + var gzipWrapper = new GZipHooks(); + + using (var tmp = TempFolder.ForCaller()) + { + var path = tmp.AllocateFilename("txt"); + var evt = Some.LogEvent("Hello, world!"); + + using (var sink = new FileSink(path, new JsonFormatter(), null, null, false, gzipWrapper)) + { + sink.Emit(evt); + sink.Emit(evt); + } + + // Ensure the data was written through the wrapping GZipStream, by decompressing and comparing against + // what we wrote + List lines; + using (var textStream = new MemoryStream()) + { + using (var fs = System.IO.File.OpenRead(path)) + using (var decompressStream = new GZipStream(fs, CompressionMode.Decompress)) + { + decompressStream.CopyTo(textStream); + } + + textStream.Position = 0; + lines = textStream.ReadAllLines(); + } + + Assert.Equal(2, lines.Count); + Assert.Contains("Hello, world!", lines[0]); + } + } + + [Fact] + public static void OnOpenedLifecycleHookCanWriteFileHeader() + { + using (var tmp = TempFolder.ForCaller()) + { + var headerWriter = new FileHeaderWriter("This is the file header"); + + var path = tmp.AllocateFilename("txt"); + using (new FileSink(path, new JsonFormatter(), null, new UTF8Encoding(false), false, headerWriter)) + { + // Open and write header + } + + using (var sink = new FileSink(path, new JsonFormatter(), null, new UTF8Encoding(false), false, headerWriter)) + { + // Length check should prevent duplicate header here + sink.Emit(Some.LogEvent()); + } + + var lines = System.IO.File.ReadAllLines(path); + + Assert.Equal(2, lines.Length); + Assert.Equal(headerWriter.Header, lines[0]); + Assert.Equal('{', lines[1][0]); + } + } + static void WriteTwoEventsAndCheckOutputFileLength(long? maxBytes, Encoding encoding) { using (var tmp = TempFolder.ForCaller()) { var path = tmp.AllocateFilename("txt"); var evt = Some.LogEvent("Irrelevant as it will be replaced by the formatter"); - var actualEventOutput = "x"; + const string actualEventOutput = "x"; var formatter = new FixedOutputFormatter(actualEventOutput); var eventOuputLength = encoding.GetByteCount(actualEventOutput); @@ -170,4 +235,3 @@ static void WriteTwoEventsAndCheckOutputFileLength(long? maxBytes, Encoding enco } } } - diff --git a/test/Serilog.Sinks.File.Tests/RollingFileSinkTests.cs b/test/Serilog.Sinks.File.Tests/RollingFileSinkTests.cs index 3efe3f9..2e9f613 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.IO.Compression; using System.Linq; using System.Reflection; using Xunit; @@ -96,6 +97,64 @@ public void WhenSizeLimitIsBreachedNewFilesCreated() } } + [Fact] + public void WhenStreamWrapperSpecifiedIsUsedForRolledFiles() + { + var gzipWrapper = new GZipHooks(); + var fileName = Some.String() + ".txt"; + + using (var temp = new TempFolder()) + { + string[] files; + var logEvents = new[] + { + Some.InformationEvent(), + Some.InformationEvent(), + Some.InformationEvent() + }; + + using (var log = new LoggerConfiguration() + .WriteTo.File(Path.Combine(temp.Path, fileName), rollOnFileSizeLimit: true, fileSizeLimitBytes: 1, hooks: gzipWrapper) + .CreateLogger()) + { + + foreach (var logEvent in logEvents) + { + log.Write(logEvent); + } + + files = Directory.GetFiles(temp.Path) + .OrderBy(p => p, StringComparer.OrdinalIgnoreCase) + .ToArray(); + + Assert.Equal(3, files.Length); + Assert.True(files[0].EndsWith(fileName), files[0]); + Assert.True(files[1].EndsWith("_001.txt"), files[1]); + Assert.True(files[2].EndsWith("_002.txt"), files[2]); + } + + // Ensure the data was written through the wrapping GZipStream, by decompressing and comparing against + // what we wrote + for (var i = 0; i < files.Length; i++) + { + using (var textStream = new MemoryStream()) + { + using (var fs = System.IO.File.OpenRead(files[i])) + using (var decompressStream = new GZipStream(fs, CompressionMode.Decompress)) + { + decompressStream.CopyTo(textStream); + } + + textStream.Position = 0; + var lines = textStream.ReadAllLines(); + + Assert.Equal(1, lines.Count); + Assert.True(lines[0].EndsWith(logEvents[i].MessageTemplate.Text)); + } + } + } + } + [Fact] public void IfTheLogFolderDoesNotExistItWillBeCreated() { diff --git a/test/Serilog.Sinks.File.Tests/Support/Extensions.cs b/test/Serilog.Sinks.File.Tests/Support/Extensions.cs index a31122d..f7fb775 100644 --- a/test/Serilog.Sinks.File.Tests/Support/Extensions.cs +++ b/test/Serilog.Sinks.File.Tests/Support/Extensions.cs @@ -1,4 +1,6 @@ -using Serilog.Events; +using System.Collections.Generic; +using System.IO; +using Serilog.Events; namespace Serilog.Sinks.File.Tests.Support { @@ -8,5 +10,21 @@ public static object LiteralValue(this LogEventPropertyValue @this) { return ((ScalarValue)@this).Value; } + + public static List ReadAllLines(this Stream @this) + { + var lines = new List(); + + using (var reader = new StreamReader(@this)) + { + string line; + while ((line = reader.ReadLine()) != null) + { + lines.Add(line); + } + } + + return lines; + } } } diff --git a/test/Serilog.Sinks.File.Tests/Support/FileHeaderWriter.cs b/test/Serilog.Sinks.File.Tests/Support/FileHeaderWriter.cs new file mode 100644 index 0000000..ae90604 --- /dev/null +++ b/test/Serilog.Sinks.File.Tests/Support/FileHeaderWriter.cs @@ -0,0 +1,28 @@ +using System.IO; +using System.Text; + +namespace Serilog.Sinks.File.Tests.Support +{ + class FileHeaderWriter : FileLifecycleHooks + { + public string Header { get; } + + public FileHeaderWriter(string header) + { + Header = header; + } + + public override Stream OnFileOpened(Stream underlyingStream, Encoding encoding) + { + if (underlyingStream.Length == 0) + { + var writer = new StreamWriter(underlyingStream, encoding); + writer.WriteLine(Header); + writer.Flush(); + underlyingStream.Flush(); + } + + return base.OnFileOpened(underlyingStream, encoding); + } + } +} diff --git a/test/Serilog.Sinks.File.Tests/Support/GZipHooks.cs b/test/Serilog.Sinks.File.Tests/Support/GZipHooks.cs new file mode 100644 index 0000000..40a77bb --- /dev/null +++ b/test/Serilog.Sinks.File.Tests/Support/GZipHooks.cs @@ -0,0 +1,26 @@ +using System.IO; +using System.IO.Compression; +using System.Text; + +namespace Serilog.Sinks.File.Tests.Support +{ + /// + /// + /// Demonstrates the use of , by compressing log output using GZip + /// + public class GZipHooks : FileLifecycleHooks + { + readonly int _bufferSize; + + public GZipHooks(int bufferSize = 1024 * 32) + { + _bufferSize = bufferSize; + } + + public override Stream OnFileOpened(Stream underlyingStream, Encoding _) + { + var compressStream = new GZipStream(underlyingStream, CompressionMode.Compress); + return new BufferedStream(compressStream, _bufferSize); + } + } +}