Skip to content

FileLifecycleHooks #80

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 20 commits into from
Apr 22, 2019
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
f7bfe4e
Enabled FileSink and RollingFileSink's output stream in another strea…
cocowalla Feb 1, 2019
9eee731
Rename StreamWrapper to FileLifecycleHooks
cocowalla Feb 14, 2019
83d817f
Ignore Rider cache/options files
cocowalla Feb 14, 2019
63c3601
Add docs re wrapped stream ownership
cocowalla Feb 14, 2019
fca6eb9
Check for directory existence before attempting access.
billrob Mar 12, 2019
0c65484
Merge pull request #88 from billrob/dev
nblumhardt Mar 13, 2019
e604418
Improve log message when FileLifecycleHooks provided for a shared log…
cocowalla Apr 20, 2019
873f6d4
Throw clear exception when wrapping the output stream returns null
cocowalla Apr 20, 2019
e310ab9
Throw when using hooks with a shared file
cocowalla Apr 20, 2019
1864db1
Merge branch 'feature/stream-wrapper' of https://github.com/cocowalla…
nblumhardt Apr 22, 2019
b660b51
Make FileSink constructor changes non-breaking; fix a bug whereby a s…
nblumhardt Apr 22, 2019
b904968
Reenable an old test that was lost
nblumhardt Apr 22, 2019
b6090cc
A test for the encoding fix
nblumhardt Apr 22, 2019
34344ad
Move FileLifecycleHooks down under Serilog.Sinks.File - avoids namesp…
nblumhardt Apr 22, 2019
0ae234e
Enable hooks and encoding for auditing methods
nblumhardt Apr 22, 2019
55fcb2b
Add backwards-compatible configuration overloads
nblumhardt Apr 22, 2019
d4fa80a
Expose encoding to OnOpened() hook; switch to AppVeyor Linux builds
nblumhardt Apr 22, 2019
9f7352d
Fix Linux build script targets
nblumhardt Apr 22, 2019
ca0ac8c
Test for header writing
nblumhardt Apr 22, 2019
5b3d64a
OnOpened() -> OnFileOpened()
nblumhardt Apr 22, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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.*
Expand Down
37 changes: 37 additions & 0 deletions src/Serilog.Sinks.File/FileLifecycleHooks.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// 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;

namespace Serilog
{
/// <summary>
/// Enables hooking into log file lifecycle events.
/// </summary>
public abstract class FileLifecycleHooks
{
/// <summary>
/// Initialize or wrap the <paramref name="underlyingStream"/> 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.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
/// <param name="underlyingStream">The underlying <see cref="Stream"/> opened on the log file.</param>
/// <returns>The <see cref="Stream"/> Serilog should use when writing events to the log file.</returns>
public virtual Stream OnOpened(Stream underlyingStream) => underlyingStream;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One of the target use cases is writing headers e.g. to CSV/W3C log files, so the name Wrap() might be too specific. I've tried OnOpened() here, with the possibility of adding OnOpening(path) and possibly an OnClosing(stream)/OnClosed(path) pair later on.

If we make this virtual rather than abstract, we won't introduce inconsistencies when future hooks are added (since these must be virtual to avoid breakage).

All sounds sane?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, we discussed this a while back in this thread, and it looks like I just forgot to rename this!

virtual reasoning also makes sense.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Back when we started discussing this there was the question of streaming vs 'static' extensibility points and this PR solves the streaming part.

Later bolting on OnOpening(path), OnClosing(stream)/OnClosed(path) would solve the 'static' part, enabling scenarios such as cryptographically signing files before they are closed, shipping closed files to WORM media, perform (non-streaming) compression and/or encryption etc 👍

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ha! I actually like the name we were using in the earlier comments a little better, on reflection. I'll rename to OnFileOpened() and merge away! :-)

}
}
35 changes: 21 additions & 14 deletions src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -135,11 +135,12 @@ public static LoggerConfiguration File(
/// <param name="shared">Allow the log file to be shared by multiple processes. The default is false.</param>
/// <param name="flushToDiskInterval">If provided, a full disk flush will be performed periodically at the specified interval.</param>
/// <param name="rollingInterval">The interval at which logging will roll over to a new file.</param>
/// <param name="rollOnFileSizeLimit">If <code>true</code>, a new file will be created when the file size limit is reached. Filenames
/// <param name="rollOnFileSizeLimit">If <code>true</code>, a new file will be created when the file size limit is reached. Filenames
/// will have a number appended in the format <code>_NNN</code>, with the first filename given no number.</param>
/// <param name="retainedFileCountLimit">The maximum number of log files that will be retained,
/// including the current log file. For unlimited retention, pass null. The default is 31.</param>
/// <param name="encoding">Character encoding used to write the text file. The default is UTF-8 without BOM.</param>
/// <param name="hooks">Optionally enables hooking into log file lifecycle events.</param>
/// <returns>Configuration object allowing method chaining.</returns>
/// <remarks>The file will be written using the UTF-8 character set.</remarks>
public static LoggerConfiguration File(
Expand All @@ -156,7 +157,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));
Expand All @@ -165,7 +167,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);
}

/// <summary>
Expand All @@ -174,7 +176,7 @@ public static LoggerConfiguration File(
/// <param name="sinkConfiguration">Logger sink configuration.</param>
/// <param name="formatter">A formatter, such as <see cref="JsonFormatter"/>, to convert the log events into
/// text for the file. If control of regular text formatting is required, use the other
/// overload of <see cref="File(LoggerSinkConfiguration, string, LogEventLevel, string, IFormatProvider, long?, LoggingLevelSwitch, bool, bool, TimeSpan?, RollingInterval, bool, int?, Encoding)"/>
/// overload of <see cref="File(LoggerSinkConfiguration, string, LogEventLevel, string, IFormatProvider, long?, LoggingLevelSwitch, bool, bool, TimeSpan?, RollingInterval, bool, int?, Encoding, FileLifecycleHooks)"/>
/// and specify the outputTemplate parameter instead.
/// </param>
/// <param name="path">Path to the file.</param>
Expand All @@ -190,11 +192,12 @@ public static LoggerConfiguration File(
/// <param name="shared">Allow the log file to be shared by multiple processes. The default is false.</param>
/// <param name="flushToDiskInterval">If provided, a full disk flush will be performed periodically at the specified interval.</param>
/// <param name="rollingInterval">The interval at which logging will roll over to a new file.</param>
/// <param name="rollOnFileSizeLimit">If <code>true</code>, a new file will be created when the file size limit is reached. Filenames
/// <param name="rollOnFileSizeLimit">If <code>true</code>, a new file will be created when the file size limit is reached. Filenames
/// will have a number appended in the format <code>_NNN</code>, with the first filename given no number.</param>
/// <param name="retainedFileCountLimit">The maximum number of log files that will be retained,
/// including the current log file. For unlimited retention, pass null. The default is 31.</param>
/// <param name="encoding">Character encoding used to write the text file. The default is UTF-8 without BOM.</param>
/// <param name="hooks">Optionally enables hooking into log file lifecycle events.</param>
/// <returns>Configuration object allowing method chaining.</returns>
/// <remarks>The file will be written using the UTF-8 character set.</remarks>
public static LoggerConfiguration File(
Expand All @@ -210,10 +213,12 @@ public static LoggerConfiguration File(
RollingInterval rollingInterval = RollingInterval.Infinite,
bool rollOnFileSizeLimit = false,
int? retainedFileCountLimit = DefaultRetainedFileCountLimit,
Encoding encoding = null)
Encoding encoding = null,
FileLifecycleHooks hooks = null)
{
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);
}

/// <summary>
Expand Down Expand Up @@ -270,7 +275,7 @@ public static LoggerConfiguration File(
LoggingLevelSwitch levelSwitch = null)
{
return ConfigureFile(sinkConfiguration.Sink, formatter, path, restrictedToMinimumLevel, null, levelSwitch, false, true,
false, null, null, RollingInterval.Infinite, false, null);
false, null, null, RollingInterval.Infinite, false, null, null);
}

static LoggerConfiguration ConfigureFile(
Expand All @@ -287,35 +292,37 @@ 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));
if (path == null) throw new ArgumentNullException(nameof(path));
if (fileSizeLimitBytes.HasValue && fileSizeLimitBytes < 0) throw new ArgumentException("Negative value provided; file size limit must be non-negative.", nameof(fileSizeLimitBytes));
if (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);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure why encoding was not propagated, here and a few lines above - I think it's a bug (though it's hard to argue with an enforced UTF-8 no-BOM default :-) )

}
#pragma warning restore 618
}
catch (Exception ex)
{
Expand Down
28 changes: 22 additions & 6 deletions src/Serilog.Sinks.File/Sinks/File/FileSink.cs
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -23,7 +23,6 @@ namespace Serilog.Sinks.File
/// <summary>
/// Write log events to a disk file.
/// </summary>
[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;
Expand All @@ -44,15 +43,26 @@ public sealed class FileSink : IFileSink, IDisposable
/// <param name="buffered">Indicates if flushing to the output file can be buffered or not. The default
/// is false.</param>
/// <returns>Configuration object allowing method chaining.</returns>
/// <remarks>The file will be written using the UTF-8 character set.</remarks>
/// <remarks>This constructor preserves compatibility with early versions of the public API. New code should not depend on this type.</remarks>
/// <exception cref="IOException"></exception>
[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;

Expand All @@ -68,6 +78,12 @@ public FileSink(string path, ITextFormatter textFormatter, long? fileSizeLimitBy
outputStream = _countingStreamWrapper = new WriteCountingStream(_underlyingStream);
}

if (hooks != null)
{
outputStream = hooks.OnOpened(outputStream) ??
throw new InvalidOperationException($"The file lifecycle hooks `{nameof(FileLifecycleHooks.OnOpened)}()` returned `null` when called with the output stream.");
}

_output = new StreamWriter(outputStream, encoding ?? new UTF8Encoding(encoderShouldEmitUTF8Identifier: false));
}

Expand Down
21 changes: 14 additions & 7 deletions src/Serilog.Sinks.File/Sinks/File/RollingFileSink.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand All @@ -64,6 +64,7 @@ public RollingFileSink(string path,
_buffered = buffered;
_shared = shared;
_rollOnFileSizeLimit = rollOnFileSizeLimit;
_hooks = hooks;
}

public void Emit(LogEvent logEvent)
Expand Down Expand Up @@ -117,8 +118,11 @@ void OpenFile(DateTime now, int? minSequence = null)
var existingFiles = Enumerable.Empty<string>();
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) { }

Expand All @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
using System.IO;
using System.Security.AccessControl;
using System.Text;
using Serilog.Core;
using Serilog.Events;
using Serilog.Formatting;

Expand Down Expand Up @@ -51,17 +50,14 @@ public sealed class SharedFileSink : IFileSink, IDisposable
/// will be written in full even if it exceeds the limit.</param>
/// <param name="encoding">Character encoding used to write the text file. The default is UTF-8 without BOM.</param>
/// <returns>Configuration object allowing method chaining.</returns>
/// <remarks>The file will be written using the UTF-8 character set.</remarks>
/// <exception cref="IOException"></exception>
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);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System;
using System;
using System.Threading;
using Serilog.Sinks.File.Tests.Support;
using Serilog.Tests.Support;
Expand Down Expand Up @@ -80,11 +80,11 @@ public void WhenFlushingToDiskReportedSharedFileSinkCanBeCreatedAndDisposed()
}

[Fact]
public void BufferingIsNotAvailableWhenSharingEnabled()
public void HooksAreNotAvailableWhenSharingEnabled()
{
Assert.Throws<ArgumentException>(() =>
new LoggerConfiguration()
.WriteTo.File("logs", buffered: true, shared: true));
.WriteTo.File("logs", shared: true, hooks: new GZipHooks()));
}
}
}
Loading