Skip to content
Prev Previous commit
Next Next commit
Support ILoggingFailureListener; seals PeriodicFlushToDiskSink, so te…
…chnically a breaking change
  • Loading branch information
nblumhardt committed Mar 8, 2025
commit 9cfd3de8c9655e8a13f5e9ebe82b09eca6338839
13 changes: 11 additions & 2 deletions src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -549,17 +549,26 @@ static LoggerConfiguration ConfigureFile(
}
catch (Exception ex)
{
SelfLog.WriteLine("Unable to open file sink for {0}: {1}", path, ex);
// No logging failure listener can be configured here; in future we might allow for a static
// default listener, but in the meantime this improves `SelfLog` usefulness and consistency.
SelfLog.FailureListener.OnLoggingFailed(
typeof(FileLoggerConfigurationExtensions),
LoggingFailureKind.Final,
$"unable to open file sink for {path}",
events: null,
ex);

if (propagateExceptions)
throw;

return addSink(new NullSink(), LevelAlias.Maximum, null);
return addSink(new FailedSink(), restrictedToMinimumLevel, levelSwitch);
}

if (flushToDiskInterval.HasValue)
{
#pragma warning disable 618
// `LoggerSinkConfiguration.Wrap()` is not used here because the target sink is expected
// to support `ILogEventSink`.
sink = new PeriodicFlushToDiskSink(sink, flushToDiskInterval.Value);
#pragma warning restore 618
}
Expand Down
4 changes: 2 additions & 2 deletions src/Serilog.Sinks.File/Serilog.Sinks.File.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

<PropertyGroup>
<Description>Write Serilog events to text files in plain or JSON format.</Description>
<VersionPrefix>6.0.1</VersionPrefix>
<VersionPrefix>7.0.0</VersionPrefix>
<Authors>Serilog Contributors</Authors>
<!-- .NET Framework version targeting is frozen at these two TFMs. -->
<TargetFrameworks Condition=" '$(OS)' == 'Windows_NT'">net471;net462</TargetFrameworks>
Expand All @@ -26,7 +26,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Serilog" Version="4.0.0" />
<PackageReference Include="Serilog" Version="4.2.0" />
<PackageReference Include="Nullable" Version="1.3.1" PrivateAssets="All" />
</ItemGroup>

Expand Down
34 changes: 34 additions & 0 deletions src/Serilog.Sinks.File/Sinks/File/FailedSink.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// Copyright © 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.Debugging;
using Serilog.Events;

namespace Serilog.Sinks.File;

sealed class FailedSink : ILogEventSink, ISetLoggingFailureListener
{
ILoggingFailureListener _failureListener = SelfLog.FailureListener;

public void Emit(LogEvent logEvent)
{
_failureListener.OnLoggingFailed(this, LoggingFailureKind.Final, "the sink could not be initialized", [logEvent], exception: null);
}

public void SetFailureListener(ILoggingFailureListener failureListener)
{
_failureListener = failureListener ?? throw new ArgumentNullException(nameof(failureListener));
}
}
24 changes: 21 additions & 3 deletions src/Serilog.Sinks.File/Sinks/File/FileSink.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
// limitations under the License.

using System.Text;
using Serilog.Core;
using Serilog.Debugging;
using Serilog.Events;
using Serilog.Formatting;

Expand All @@ -21,7 +23,7 @@ namespace Serilog.Sinks.File;
/// <summary>
/// Write log events to a disk file.
/// </summary>
public sealed class FileSink : IFileSink, IDisposable
public sealed class FileSink : IFileSink, IDisposable, ISetLoggingFailureListener
{
readonly TextWriter _output;
readonly FileStream _underlyingStream;
Expand All @@ -31,6 +33,8 @@ public sealed class FileSink : IFileSink, IDisposable
readonly object _syncRoot = new();
readonly WriteCountingStream? _countingStreamWrapper;

ILoggingFailureListener _failureListener = SelfLog.FailureListener;

/// <summary>Construct a <see cref="FileSink"/>.</summary>
/// <param name="path">Path to the file.</param>
/// <param name="textFormatter">Formatter used to convert log events to text.</param>
Expand Down Expand Up @@ -98,7 +102,7 @@ internal FileSink(
}
catch
{
outputStream?.Dispose();
outputStream.Dispose();
throw;
}
}
Expand Down Expand Up @@ -132,7 +136,16 @@ bool IFileSink.EmitOrOverflow(LogEvent logEvent)
/// <exception cref="ArgumentNullException">When <paramref name="logEvent"/> is <code>null</code></exception>
public void Emit(LogEvent logEvent)
{
((IFileSink) this).EmitOrOverflow(logEvent);
if (!((IFileSink)this).EmitOrOverflow(logEvent))
{
// Support fallback chains without the overhead of throwing an exception.
_failureListener.OnLoggingFailed(
this,
LoggingFailureKind.Permanent,
"the log file size limit has been reached and no rolling behavior was specified",
[logEvent],
exception: null);
}
}

/// <inheritdoc />
Expand All @@ -153,4 +166,9 @@ public void FlushToDisk()
_underlyingStream.Flush(true);
}
}

void ISetLoggingFailureListener.SetFailureListener(ILoggingFailureListener failureListener)
{
_failureListener = failureListener ?? throw new ArgumentNullException(nameof(failureListener));
}
}
32 changes: 29 additions & 3 deletions src/Serilog.Sinks.File/Sinks/File/PeriodicFlushToDiskSink.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,14 @@ namespace Serilog.Sinks.File;
/// A sink wrapper that periodically flushes the wrapped sink to disk.
/// </summary>
[Obsolete("This type will be removed from the public API in a future version; use `WriteTo.File(flushToDiskInterval:)` instead.")]
public class PeriodicFlushToDiskSink : ILogEventSink, IDisposable
public sealed class PeriodicFlushToDiskSink : ILogEventSink, IDisposable, ISetLoggingFailureListener
{
readonly ILogEventSink _sink;
readonly Timer _timer;
int _flushRequired;

ILoggingFailureListener _failureListener = SelfLog.FailureListener;

/// <summary>
/// Construct a <see cref="PeriodicFlushToDiskSink"/> that wraps
/// <paramref name="sink"/> and flushes it at the specified <paramref name="flushInterval"/>.
Expand All @@ -46,7 +48,17 @@ public PeriodicFlushToDiskSink(ILogEventSink sink, TimeSpan flushInterval)
else
{
_timer = new Timer(_ => { }, null, Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan);
SelfLog.WriteLine("{0} configured to flush {1}, but {2} not implemented", typeof(PeriodicFlushToDiskSink), sink, nameof(IFlushableFileSink));

// May be an opportunity to improve the failure listener API for these cases - the failure
// is important, but not exactly `Final`.
SelfLog.FailureListener.OnLoggingFailed(
// Class must be sealed in order for this to be safe - `this` may be partially constructed
// otherwise.
this,
LoggingFailureKind.Final,
$"configured to flush {sink}, but {nameof(IFlushableFileSink)} not implemented",
events: null,
exception: null);
}
}

Expand Down Expand Up @@ -77,7 +89,21 @@ void FlushToDisk(IFlushableFileSink flushable)
}
catch (Exception ex)
{
SelfLog.WriteLine("{0} could not flush the underlying sink to disk: {1}", typeof(PeriodicFlushToDiskSink), ex);
_failureListener.OnLoggingFailed(
this,
LoggingFailureKind.Temporary,
"could not flush the underlying file to disk",
events: null,
ex);
}
}

void ISetLoggingFailureListener.SetFailureListener(ILoggingFailureListener failureListener)
{
_failureListener = failureListener ?? throw new ArgumentNullException(nameof(failureListener));
if (_sink is ISetLoggingFailureListener setLoggingFailureListener)
{
setLoggingFailureListener.SetFailureListener(failureListener);
}
}
}
51 changes: 39 additions & 12 deletions src/Serilog.Sinks.File/Sinks/File/RollingFileSink.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@

namespace Serilog.Sinks.File;

sealed class RollingFileSink : ILogEventSink, IFlushableFileSink, IDisposable
sealed class RollingFileSink : ILogEventSink, IFlushableFileSink, IDisposable, ISetLoggingFailureListener
{
readonly PathRoller _roller;
readonly ITextFormatter _textFormatter;
Expand All @@ -33,6 +33,8 @@ sealed class RollingFileSink : ILogEventSink, IFlushableFileSink, IDisposable
readonly bool _rollOnFileSizeLimit;
readonly FileLifecycleHooks? _hooks;

ILoggingFailureListener _failureListener = SelfLog.FailureListener;

readonly object _syncRoot = new();
bool _isDisposed;
DateTime? _nextCheckpoint;
Expand Down Expand Up @@ -72,6 +74,7 @@ public void Emit(LogEvent logEvent)
{
if (logEvent == null) throw new ArgumentNullException(nameof(logEvent));

bool failed;
lock (_syncRoot)
{
if (_isDisposed) throw new ObjectDisposedException("The log file has been disposed.");
Expand All @@ -84,12 +87,18 @@ public void Emit(LogEvent logEvent)
AlignCurrentFileTo(now, nextSequence: true);
}

/* TODO: We REALLY should add this to avoid stuff become missing undetected.
if (_currentFile == null)
{
SelfLog.WriteLine("Log event {0} was lost since it was not possible to open the file or create a new one.", logEvent.RenderMessage());
}
*/
failed = _currentFile == null;
}

if (failed)
{
// Support fallback chains without the overhead of throwing an exception.
_failureListener.OnLoggingFailed(
this,
LoggingFailureKind.Permanent,
"the target file could not be opened or created",
[logEvent],
exception: null);
}
}

Expand Down Expand Up @@ -170,14 +179,22 @@ void OpenFile(DateTime now, int? minSequence = null)
new FileSink(path, _textFormatter, _fileSizeLimitBytes, _encoding, _buffered, _hooks);

_currentFileSequence = sequence;

if (_currentFile is ISetLoggingFailureListener setLoggingFailureListener)
{
setLoggingFailureListener.SetFailureListener(_failureListener);
}
}
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);
_failureListener.OnLoggingFailed(
this,
LoggingFailureKind.Temporary,
$"file target {path} was locked, attempting to open next in sequence (attempt {attempt + 1})",
events: null,
exception: null);
sequence = (sequence ?? 0) + 1;
continue;
}
Expand Down Expand Up @@ -216,7 +233,7 @@ void ApplyRetentionPolicy(string currentFilePath, DateTime now)
// ReSharper disable once ConvertClosureToMethodGroup
var potentialMatches = Directory.GetFiles(_roller.LogFileDirectory, _roller.DirectorySearchPattern)
.Select(f => Path.GetFileName(f))
.Union(new[] { currentFileName });
.Union([currentFileName]);

var newestFirst = _roller
.SelectMatches(potentialMatches)
Expand All @@ -239,7 +256,12 @@ void ApplyRetentionPolicy(string currentFilePath, DateTime now)
}
catch (Exception ex)
{
SelfLog.WriteLine("Error {0} while processing obsolete log file {1}", ex, fullPath);
_failureListener.OnLoggingFailed(
this,
LoggingFailureKind.Temporary,
$"error while processing obsolete log file {fullPath}",
events: null,
ex);
}
}
}
Expand Down Expand Up @@ -286,4 +308,9 @@ public void FlushToDisk()
_currentFile?.FlushToDisk();
}
}

public void SetFailureListener(ILoggingFailureListener failureListener)
{
_failureListener = failureListener ?? throw new ArgumentNullException(nameof(failureListener));
}
}
24 changes: 21 additions & 3 deletions src/Serilog.Sinks.File/Sinks/File/SharedFileSink.AtomicAppend.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@

using System.Security.AccessControl;
using System.Text;
using Serilog.Core;
using Serilog.Debugging;
using Serilog.Events;
using Serilog.Formatting;

Expand All @@ -25,7 +27,7 @@ namespace Serilog.Sinks.File;
/// 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(shared: true)` instead.")]
public sealed class SharedFileSink : IFileSink, IDisposable
public sealed class SharedFileSink : IFileSink, IDisposable, ISetLoggingFailureListener
{
readonly MemoryStream _writeBuffer;
readonly string _path;
Expand All @@ -34,6 +36,8 @@ public sealed class SharedFileSink : IFileSink, IDisposable
readonly long? _fileSizeLimitBytes;
readonly object _syncRoot = new();

ILoggingFailureListener _failureListener = SelfLog.FailureListener;

// The stream is reopened with a larger buffer if atomic writes beyond the current buffer size are needed.
FileStream _fileOutput;
int _fileStreamBufferLength = DefaultFileStreamBufferLength;
Expand All @@ -59,7 +63,7 @@ public sealed class SharedFileSink : IFileSink, IDisposable
public SharedFileSink(string path, ITextFormatter textFormatter, long? fileSizeLimitBytes, Encoding? encoding = null)
{
if (fileSizeLimitBytes.HasValue && fileSizeLimitBytes < 1)
throw new ArgumentException("Invalid value provided; file size limit must be at least 1 byte, or null");
throw new ArgumentException("Invalid value provided; file size limit must be at least 1 byte, or null.");

_path = path ?? throw new ArgumentNullException(nameof(path));
_textFormatter = textFormatter ?? throw new ArgumentNullException(nameof(textFormatter));
Expand Down Expand Up @@ -149,7 +153,16 @@ bool IFileSink.EmitOrOverflow(LogEvent logEvent)
/// <exception cref="ArgumentNullException">When <paramref name="logEvent"/> is <code>null</code></exception>
public void Emit(LogEvent logEvent)
{
((IFileSink)this).EmitOrOverflow(logEvent);
if (!((IFileSink)this).EmitOrOverflow(logEvent))
{
// Support fallback chains without the overhead of throwing an exception.
_failureListener.OnLoggingFailed(
this,
LoggingFailureKind.Permanent,
"the log file size limit has been reached and no rolling behavior was specified",
[logEvent],
exception: null);
}
}

/// <inheritdoc />
Expand All @@ -170,6 +183,11 @@ public void FlushToDisk()
_fileOutput.Flush(true);
}
}

void ISetLoggingFailureListener.SetFailureListener(ILoggingFailureListener failureListener)
{
_failureListener = failureListener ?? throw new ArgumentNullException(nameof(failureListener));
}
}

#endif
Loading