Skip to content

Commit cd58cbd

Browse files
committed
Add monitor facility
1 parent 870c52e commit cd58cbd

File tree

5 files changed

+96
-8
lines changed

5 files changed

+96
-8
lines changed

README.md

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,35 @@ The default memory buffer feeding the worker thread is capped to 10,000 items, a
3636

3737
```csharp
3838
// Reduce the buffer to 500 events
39-
.WriteTo.Async(a => a.RollingFile("logs/myapp-{Date}.txt"), 500)
39+
.WriteTo.Async(a => a.File("logs/myapp.log"), bufferSize: 500)
40+
```
41+
42+
### Monitoring
43+
44+
Typically, one should assign adequate buffer capacity to enable the wrapped sinks to ingest the events as they are processed without ever approaching the limit. In order to gain awareness of the processing backlog becoming abnormal, it's possible to instrument the Async sink by suppling a `monitor` callback that allows for periodic inspection of the backlog
45+
46+
```csharp
47+
void LogBufferMonitor(buffer : BlockingQueue<Serilog.Events.LogEvent> queue)
48+
{
49+
var usagePct = queue.Count * 100 / queue.BoundedCapacity;
50+
if (usagePct > 50) SelfLog.WriteLine("Log buffer exceeded {0:p0} usage (limit: {1})", usage, queue.BoundedCapacity);
51+
}
52+
53+
// Wait for any queued event to be accepted by the `File` log before allowing the calling thread
54+
// to resume its application work after a logging call when there are 10,000 LogEvents waiting
55+
.WriteTo.Async(a => a.File("logs/myapp.log"), monitorIntervalSeconds: 60, monitor: LogBufferMonitor)
56+
```
57+
58+
### Blocking
59+
60+
Warning: For the same reason one typically does not want exceptions from logging to leak into the execution path, one typically does not want a logger to be able to have the side-efect of actually interrupting application processing until the log propagation has been unblocked.
61+
62+
When the buffer size limit is reached, the default behavior is to drop any further attempted writes until the queue abates, reporting each such failure to the `Serilog.Debugging.SelfLog`. To replace this with a blocking behaviour, set `blockWhenFull` to `true`.
63+
64+
```csharp
65+
// Wait for any queued event to be accepted by the `File` log before allowing the calling thread
66+
// to resume its application work after a logging call when there are 10,000 LogEvents waiting
67+
.WriteTo.Async(a => a.File("logs/myapp.log"), blockWhenFull: true)
4068
```
4169

4270
### XML `<appSettings>` and JSON configuration

src/Serilog.Sinks.Async/LoggerConfigurationAsyncExtensions.cs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,16 +38,20 @@ public static LoggerConfiguration Async(
3838
/// <paramref name="blockWhenFull"/> the queue will block or subsequent events will be dropped until
3939
/// room is made in the queue.</param>
4040
/// <param name="blockWhenFull">Block when the queue is full, instead of dropping events.</param>
41+
/// <param name="monitorIntervalSeconds">Interval between invocations of <paramref name="monitor"/>.</param>
42+
/// <param name="monitor">Callback to facilitate health checking the internal queue. Frequency is controlled by <paramref name="monitorIntervalSeconds"/>.</param>
4143
/// <returns>A <see cref="LoggerConfiguration"/> allowing configuration to continue.</returns>
4244
public static LoggerConfiguration Async(
4345
this LoggerSinkConfiguration loggerSinkConfiguration,
4446
Action<LoggerSinkConfiguration> configure,
4547
int bufferSize = 10000,
46-
bool blockWhenFull = false)
48+
bool blockWhenFull = false,
49+
int monitorIntervalSeconds = 10,
50+
Action<System.Collections.Concurrent.BlockingCollection<Events.LogEvent>> monitor = null)
4751
{
4852
return LoggerSinkConfiguration.Wrap(
4953
loggerSinkConfiguration,
50-
wrappedSink => new BackgroundWorkerSink(wrappedSink, bufferSize, blockWhenFull),
54+
wrappedSink => new BackgroundWorkerSink(wrappedSink, bufferSize, blockWhenFull, monitorIntervalSeconds, monitor),
5155
configure);
5256
}
5357

src/Serilog.Sinks.Async/Serilog.Sinks.Async.csproj

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
<AssemblyVersion>1.0.0</AssemblyVersion>
66
<VersionPrefix>1.2.0</VersionPrefix>
77
<Authors>Jezz Santos;Serilog Contributors</Authors>
8-
<TargetFrameworks>net45;netstandard1.1</TargetFrameworks>
8+
<TargetFrameworks>net45;netstandard1.1;netstandard1.2</TargetFrameworks>
99
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
1010
<GenerateDocumentationFile>true</GenerateDocumentationFile>
1111
<AssemblyName>Serilog.Sinks.Async</AssemblyName>
@@ -26,6 +26,10 @@
2626
<PackageReference Include="Serilog" Version="2.5.0" />
2727
</ItemGroup>
2828

29+
<PropertyGroup Condition=" '$(TargetFramework)' == 'netstandard1.1' ">
30+
<DefineConstants>$(DefineConstants);NETSTANDARD_NO_TIMER</DefineConstants>
31+
</PropertyGroup>
32+
2933
<ItemGroup Condition=" '$(TargetFramework)' == 'net45' ">
3034
<Reference Include="System" />
3135
<Reference Include="Microsoft.CSharp" />
@@ -35,4 +39,9 @@
3539
<PackageReference Include="System.Collections.Concurrent" Version="4.0.12" />
3640
</ItemGroup>
3741

42+
<ItemGroup Condition=" '$(TargetFramework)' == 'netstandard1.2' ">
43+
<PackageReference Include="System.Collections.Concurrent" Version="4.0.12" />
44+
<PackageReference Include="System.Threading.Timer" Version="4.0.1" />
45+
</ItemGroup>
46+
3847
</Project>

src/Serilog.Sinks.Async/Sinks/Async/BackgroundWorkerSink.cs

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,33 @@ sealed class BackgroundWorkerSink : ILogEventSink, IDisposable
1515
readonly bool _blockWhenFull;
1616
readonly BlockingCollection<LogEvent> _queue;
1717
readonly Task _worker;
18-
19-
public BackgroundWorkerSink(ILogEventSink pipeline, int bufferCapacity, bool blockWhenFull)
18+
#if! NETSTANDARD_NO_TIMER
19+
readonly Timer _monitorCallbackInvocationTimer;
20+
#endif
21+
public BackgroundWorkerSink(
22+
ILogEventSink pipeline, int bufferCapacity,
23+
bool blockWhenFull,
24+
int monitorIntervalSeconds = 0, Action<BlockingCollection<LogEvent>> monitor = null)
2025
{
2126
if (pipeline == null) throw new ArgumentNullException(nameof(pipeline));
2227
if (bufferCapacity <= 0) throw new ArgumentOutOfRangeException(nameof(bufferCapacity));
28+
if (monitorIntervalSeconds < 0) throw new ArgumentOutOfRangeException(nameof(monitorIntervalSeconds));
2329
_pipeline = pipeline;
2430
_bufferCapacity = bufferCapacity;
2531
_blockWhenFull = blockWhenFull;
2632
_queue = new BlockingCollection<LogEvent>(_bufferCapacity);
2733
_worker = Task.Factory.StartNew(Pump, CancellationToken.None, TaskCreationOptions.LongRunning | TaskCreationOptions.DenyChildAttach, TaskScheduler.Default);
34+
35+
if (monitor != null)
36+
{
37+
if (monitorIntervalSeconds < 1) throw new ArgumentOutOfRangeException(nameof(monitorIntervalSeconds), "must be >=1");
38+
#if! NETSTANDARD_NO_TIMER
39+
var interval = TimeSpan.FromSeconds(monitorIntervalSeconds);
40+
_monitorCallbackInvocationTimer = new Timer(queue => monitor((BlockingCollection<LogEvent>)queue), _queue, interval, interval);
41+
#else
42+
throw new PlatformNotSupportedException($"Please use a platform supporting .netstandard1.2 or later to avail of the ${nameof(monitor)} facility.");
43+
#endif
44+
}
2845
}
2946

3047
public void Emit(LogEvent logEvent)
@@ -57,8 +74,13 @@ public void Dispose()
5774
_queue.CompleteAdding();
5875

5976
// Allow queued events to be flushed
60-
_worker.Wait();
61-
77+
_worker.Wait();
78+
79+
#if! NETSTANDARD_NO_TIMER
80+
// Only stop monitoring when we've actually completed flushing
81+
_monitorCallbackInvocationTimer?.Dispose();
82+
#endif
83+
6284
(_pipeline as IDisposable)?.Dispose();
6385
}
6486

test/Serilog.Sinks.Async.Tests/BackgroundWorkerSinkSpec.cs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System;
2+
using System.Collections.Concurrent;
23
using System.Collections.Generic;
34
using System.Diagnostics;
45
using System.Linq;
@@ -156,6 +157,30 @@ public async Task WhenQueueFull_ThenBlocks()
156157
}
157158
}
158159

160+
#if !NETSTANDARD_NO_TIMER
161+
[Fact]
162+
public void MonitorArgumentAffordsBacklogHealthMonitoringFacility()
163+
{
164+
bool logWasObservedToHaveReachedHalfFull = false;
165+
void inspectBuffer(BlockingCollection<LogEvent> queue) =>
166+
167+
logWasObservedToHaveReachedHalfFull = logWasObservedToHaveReachedHalfFull
168+
|| queue.Count * 100 / queue.BoundedCapacity >= 50;
169+
170+
var collector = new MemorySink { DelayEmit = TimeSpan.FromSeconds(3) };
171+
using (var logger = new LoggerConfiguration()
172+
.WriteTo.Async(w => w.Sink(collector), bufferSize: 2, monitorIntervalSeconds: 1, monitor: inspectBuffer)
173+
.CreateLogger())
174+
{
175+
logger.Information("Something to block the pipe");
176+
logger.Information("I'll just leave this here pending for a few seconds so I can observe it");
177+
System.Threading.Thread.Sleep(TimeSpan.FromSeconds(2));
178+
}
179+
180+
Assert.True(logWasObservedToHaveReachedHalfFull);
181+
}
182+
#endif
183+
159184
private BackgroundWorkerSink CreateSinkWithDefaultOptions()
160185
{
161186
return new BackgroundWorkerSink(_logger, 10000, false);

0 commit comments

Comments
 (0)