Skip to content

Commit 9cfd3de

Browse files
committed
Support ILoggingFailureListener; seals PeriodicFlushToDiskSink, so technically a breaking change
1 parent 5697cc1 commit 9cfd3de

13 files changed

+226
-35
lines changed

src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -549,17 +549,26 @@ static LoggerConfiguration ConfigureFile(
549549
}
550550
catch (Exception ex)
551551
{
552-
SelfLog.WriteLine("Unable to open file sink for {0}: {1}", path, ex);
552+
// No logging failure listener can be configured here; in future we might allow for a static
553+
// default listener, but in the meantime this improves `SelfLog` usefulness and consistency.
554+
SelfLog.FailureListener.OnLoggingFailed(
555+
typeof(FileLoggerConfigurationExtensions),
556+
LoggingFailureKind.Final,
557+
$"unable to open file sink for {path}",
558+
events: null,
559+
ex);
553560

554561
if (propagateExceptions)
555562
throw;
556563

557-
return addSink(new NullSink(), LevelAlias.Maximum, null);
564+
return addSink(new FailedSink(), restrictedToMinimumLevel, levelSwitch);
558565
}
559566

560567
if (flushToDiskInterval.HasValue)
561568
{
562569
#pragma warning disable 618
570+
// `LoggerSinkConfiguration.Wrap()` is not used here because the target sink is expected
571+
// to support `ILogEventSink`.
563572
sink = new PeriodicFlushToDiskSink(sink, flushToDiskInterval.Value);
564573
#pragma warning restore 618
565574
}

src/Serilog.Sinks.File/Serilog.Sinks.File.csproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

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

2828
<ItemGroup>
29-
<PackageReference Include="Serilog" Version="4.0.0" />
29+
<PackageReference Include="Serilog" Version="4.2.0" />
3030
<PackageReference Include="Nullable" Version="1.3.1" PrivateAssets="All" />
3131
</ItemGroup>
3232

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// Copyright © Serilog Contributors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
using Serilog.Core;
16+
using Serilog.Debugging;
17+
using Serilog.Events;
18+
19+
namespace Serilog.Sinks.File;
20+
21+
sealed class FailedSink : ILogEventSink, ISetLoggingFailureListener
22+
{
23+
ILoggingFailureListener _failureListener = SelfLog.FailureListener;
24+
25+
public void Emit(LogEvent logEvent)
26+
{
27+
_failureListener.OnLoggingFailed(this, LoggingFailureKind.Final, "the sink could not be initialized", [logEvent], exception: null);
28+
}
29+
30+
public void SetFailureListener(ILoggingFailureListener failureListener)
31+
{
32+
_failureListener = failureListener ?? throw new ArgumentNullException(nameof(failureListener));
33+
}
34+
}

src/Serilog.Sinks.File/Sinks/File/FileSink.cs

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
// limitations under the License.
1414

1515
using System.Text;
16+
using Serilog.Core;
17+
using Serilog.Debugging;
1618
using Serilog.Events;
1719
using Serilog.Formatting;
1820

@@ -21,7 +23,7 @@ namespace Serilog.Sinks.File;
2123
/// <summary>
2224
/// Write log events to a disk file.
2325
/// </summary>
24-
public sealed class FileSink : IFileSink, IDisposable
26+
public sealed class FileSink : IFileSink, IDisposable, ISetLoggingFailureListener
2527
{
2628
readonly TextWriter _output;
2729
readonly FileStream _underlyingStream;
@@ -31,6 +33,8 @@ public sealed class FileSink : IFileSink, IDisposable
3133
readonly object _syncRoot = new();
3234
readonly WriteCountingStream? _countingStreamWrapper;
3335

36+
ILoggingFailureListener _failureListener = SelfLog.FailureListener;
37+
3438
/// <summary>Construct a <see cref="FileSink"/>.</summary>
3539
/// <param name="path">Path to the file.</param>
3640
/// <param name="textFormatter">Formatter used to convert log events to text.</param>
@@ -98,7 +102,7 @@ internal FileSink(
98102
}
99103
catch
100104
{
101-
outputStream?.Dispose();
105+
outputStream.Dispose();
102106
throw;
103107
}
104108
}
@@ -132,7 +136,16 @@ bool IFileSink.EmitOrOverflow(LogEvent logEvent)
132136
/// <exception cref="ArgumentNullException">When <paramref name="logEvent"/> is <code>null</code></exception>
133137
public void Emit(LogEvent logEvent)
134138
{
135-
((IFileSink) this).EmitOrOverflow(logEvent);
139+
if (!((IFileSink)this).EmitOrOverflow(logEvent))
140+
{
141+
// Support fallback chains without the overhead of throwing an exception.
142+
_failureListener.OnLoggingFailed(
143+
this,
144+
LoggingFailureKind.Permanent,
145+
"the log file size limit has been reached and no rolling behavior was specified",
146+
[logEvent],
147+
exception: null);
148+
}
136149
}
137150

138151
/// <inheritdoc />
@@ -153,4 +166,9 @@ public void FlushToDisk()
153166
_underlyingStream.Flush(true);
154167
}
155168
}
169+
170+
void ISetLoggingFailureListener.SetFailureListener(ILoggingFailureListener failureListener)
171+
{
172+
_failureListener = failureListener ?? throw new ArgumentNullException(nameof(failureListener));
173+
}
156174
}

src/Serilog.Sinks.File/Sinks/File/PeriodicFlushToDiskSink.cs

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,14 @@ namespace Serilog.Sinks.File;
2222
/// A sink wrapper that periodically flushes the wrapped sink to disk.
2323
/// </summary>
2424
[Obsolete("This type will be removed from the public API in a future version; use `WriteTo.File(flushToDiskInterval:)` instead.")]
25-
public class PeriodicFlushToDiskSink : ILogEventSink, IDisposable
25+
public sealed class PeriodicFlushToDiskSink : ILogEventSink, IDisposable, ISetLoggingFailureListener
2626
{
2727
readonly ILogEventSink _sink;
2828
readonly Timer _timer;
2929
int _flushRequired;
3030

31+
ILoggingFailureListener _failureListener = SelfLog.FailureListener;
32+
3133
/// <summary>
3234
/// Construct a <see cref="PeriodicFlushToDiskSink"/> that wraps
3335
/// <paramref name="sink"/> and flushes it at the specified <paramref name="flushInterval"/>.
@@ -46,7 +48,17 @@ public PeriodicFlushToDiskSink(ILogEventSink sink, TimeSpan flushInterval)
4648
else
4749
{
4850
_timer = new Timer(_ => { }, null, Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan);
49-
SelfLog.WriteLine("{0} configured to flush {1}, but {2} not implemented", typeof(PeriodicFlushToDiskSink), sink, nameof(IFlushableFileSink));
51+
52+
// May be an opportunity to improve the failure listener API for these cases - the failure
53+
// is important, but not exactly `Final`.
54+
SelfLog.FailureListener.OnLoggingFailed(
55+
// Class must be sealed in order for this to be safe - `this` may be partially constructed
56+
// otherwise.
57+
this,
58+
LoggingFailureKind.Final,
59+
$"configured to flush {sink}, but {nameof(IFlushableFileSink)} not implemented",
60+
events: null,
61+
exception: null);
5062
}
5163
}
5264

@@ -77,7 +89,21 @@ void FlushToDisk(IFlushableFileSink flushable)
7789
}
7890
catch (Exception ex)
7991
{
80-
SelfLog.WriteLine("{0} could not flush the underlying sink to disk: {1}", typeof(PeriodicFlushToDiskSink), ex);
92+
_failureListener.OnLoggingFailed(
93+
this,
94+
LoggingFailureKind.Temporary,
95+
"could not flush the underlying file to disk",
96+
events: null,
97+
ex);
98+
}
99+
}
100+
101+
void ISetLoggingFailureListener.SetFailureListener(ILoggingFailureListener failureListener)
102+
{
103+
_failureListener = failureListener ?? throw new ArgumentNullException(nameof(failureListener));
104+
if (_sink is ISetLoggingFailureListener setLoggingFailureListener)
105+
{
106+
setLoggingFailureListener.SetFailureListener(failureListener);
81107
}
82108
}
83109
}

src/Serilog.Sinks.File/Sinks/File/RollingFileSink.cs

Lines changed: 39 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020

2121
namespace Serilog.Sinks.File;
2222

23-
sealed class RollingFileSink : ILogEventSink, IFlushableFileSink, IDisposable
23+
sealed class RollingFileSink : ILogEventSink, IFlushableFileSink, IDisposable, ISetLoggingFailureListener
2424
{
2525
readonly PathRoller _roller;
2626
readonly ITextFormatter _textFormatter;
@@ -33,6 +33,8 @@ sealed class RollingFileSink : ILogEventSink, IFlushableFileSink, IDisposable
3333
readonly bool _rollOnFileSizeLimit;
3434
readonly FileLifecycleHooks? _hooks;
3535

36+
ILoggingFailureListener _failureListener = SelfLog.FailureListener;
37+
3638
readonly object _syncRoot = new();
3739
bool _isDisposed;
3840
DateTime? _nextCheckpoint;
@@ -72,6 +74,7 @@ public void Emit(LogEvent logEvent)
7274
{
7375
if (logEvent == null) throw new ArgumentNullException(nameof(logEvent));
7476

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

87-
/* TODO: We REALLY should add this to avoid stuff become missing undetected.
88-
if (_currentFile == null)
89-
{
90-
SelfLog.WriteLine("Log event {0} was lost since it was not possible to open the file or create a new one.", logEvent.RenderMessage());
91-
}
92-
*/
90+
failed = _currentFile == null;
91+
}
92+
93+
if (failed)
94+
{
95+
// Support fallback chains without the overhead of throwing an exception.
96+
_failureListener.OnLoggingFailed(
97+
this,
98+
LoggingFailureKind.Permanent,
99+
"the target file could not be opened or created",
100+
[logEvent],
101+
exception: null);
93102
}
94103
}
95104

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

172181
_currentFileSequence = sequence;
182+
183+
if (_currentFile is ISetLoggingFailureListener setLoggingFailureListener)
184+
{
185+
setLoggingFailureListener.SetFailureListener(_failureListener);
186+
}
173187
}
174188
catch (IOException ex)
175189
{
176190
if (IOErrors.IsLockedFile(ex))
177191
{
178-
SelfLog.WriteLine(
179-
"File target {0} was locked, attempting to open next in sequence (attempt {1})", path,
180-
attempt + 1);
192+
_failureListener.OnLoggingFailed(
193+
this,
194+
LoggingFailureKind.Temporary,
195+
$"file target {path} was locked, attempting to open next in sequence (attempt {attempt + 1})",
196+
events: null,
197+
exception: null);
181198
sequence = (sequence ?? 0) + 1;
182199
continue;
183200
}
@@ -216,7 +233,7 @@ void ApplyRetentionPolicy(string currentFilePath, DateTime now)
216233
// ReSharper disable once ConvertClosureToMethodGroup
217234
var potentialMatches = Directory.GetFiles(_roller.LogFileDirectory, _roller.DirectorySearchPattern)
218235
.Select(f => Path.GetFileName(f))
219-
.Union(new[] { currentFileName });
236+
.Union([currentFileName]);
220237

221238
var newestFirst = _roller
222239
.SelectMatches(potentialMatches)
@@ -239,7 +256,12 @@ void ApplyRetentionPolicy(string currentFilePath, DateTime now)
239256
}
240257
catch (Exception ex)
241258
{
242-
SelfLog.WriteLine("Error {0} while processing obsolete log file {1}", ex, fullPath);
259+
_failureListener.OnLoggingFailed(
260+
this,
261+
LoggingFailureKind.Temporary,
262+
$"error while processing obsolete log file {fullPath}",
263+
events: null,
264+
ex);
243265
}
244266
}
245267
}
@@ -286,4 +308,9 @@ public void FlushToDisk()
286308
_currentFile?.FlushToDisk();
287309
}
288310
}
311+
312+
public void SetFailureListener(ILoggingFailureListener failureListener)
313+
{
314+
_failureListener = failureListener ?? throw new ArgumentNullException(nameof(failureListener));
315+
}
289316
}

src/Serilog.Sinks.File/Sinks/File/SharedFileSink.AtomicAppend.cs

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616

1717
using System.Security.AccessControl;
1818
using System.Text;
19+
using Serilog.Core;
20+
using Serilog.Debugging;
1921
using Serilog.Events;
2022
using Serilog.Formatting;
2123

@@ -25,7 +27,7 @@ namespace Serilog.Sinks.File;
2527
/// Write log events to a disk file.
2628
/// </summary>
2729
[Obsolete("This type will be removed from the public API in a future version; use `WriteTo.File(shared: true)` instead.")]
28-
public sealed class SharedFileSink : IFileSink, IDisposable
30+
public sealed class SharedFileSink : IFileSink, IDisposable, ISetLoggingFailureListener
2931
{
3032
readonly MemoryStream _writeBuffer;
3133
readonly string _path;
@@ -34,6 +36,8 @@ public sealed class SharedFileSink : IFileSink, IDisposable
3436
readonly long? _fileSizeLimitBytes;
3537
readonly object _syncRoot = new();
3638

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

6468
_path = path ?? throw new ArgumentNullException(nameof(path));
6569
_textFormatter = textFormatter ?? throw new ArgumentNullException(nameof(textFormatter));
@@ -149,7 +153,16 @@ bool IFileSink.EmitOrOverflow(LogEvent logEvent)
149153
/// <exception cref="ArgumentNullException">When <paramref name="logEvent"/> is <code>null</code></exception>
150154
public void Emit(LogEvent logEvent)
151155
{
152-
((IFileSink)this).EmitOrOverflow(logEvent);
156+
if (!((IFileSink)this).EmitOrOverflow(logEvent))
157+
{
158+
// Support fallback chains without the overhead of throwing an exception.
159+
_failureListener.OnLoggingFailed(
160+
this,
161+
LoggingFailureKind.Permanent,
162+
"the log file size limit has been reached and no rolling behavior was specified",
163+
[logEvent],
164+
exception: null);
165+
}
153166
}
154167

155168
/// <inheritdoc />
@@ -170,6 +183,11 @@ public void FlushToDisk()
170183
_fileOutput.Flush(true);
171184
}
172185
}
186+
187+
void ISetLoggingFailureListener.SetFailureListener(ILoggingFailureListener failureListener)
188+
{
189+
_failureListener = failureListener ?? throw new ArgumentNullException(nameof(failureListener));
190+
}
173191
}
174192

175193
#endif

0 commit comments

Comments
 (0)