Skip to content

Commit 579a0cf

Browse files
committed
Add monitor facility
1 parent f22d170 commit 579a0cf

File tree

5 files changed

+83
-7
lines changed

5 files changed

+83
-7
lines changed

README.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,22 @@ The default memory buffer feeding the worker thread is capped to 10,000 items, a
3939
.WriteTo.Async(a => a.File("logs/myapp.log"), bufferSize: 500)
4040
```
4141

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+
4258
### Blocking
4359

4460
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.

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
@@ -14,15 +14,32 @@ sealed class BackgroundWorkerSink : ILogEventSink, IDisposable
1414
readonly bool _blockWhenFull;
1515
readonly BlockingCollection<LogEvent> _queue;
1616
readonly Task _worker;
17-
18-
public BackgroundWorkerSink(ILogEventSink pipeline, int bufferCapacity, bool blockWhenFull)
17+
#if! NETSTANDARD_NO_TIMER
18+
readonly Timer _monitorCallbackInvocationTimer;
19+
#endif
20+
public BackgroundWorkerSink(
21+
ILogEventSink pipeline, int bufferCapacity,
22+
bool blockWhenFull,
23+
int monitorIntervalSeconds = 0, Action<BlockingCollection<LogEvent>> monitor = null)
1924
{
2025
if (pipeline == null) throw new ArgumentNullException(nameof(pipeline));
2126
if (bufferCapacity <= 0) throw new ArgumentOutOfRangeException(nameof(bufferCapacity));
27+
if (monitorIntervalSeconds < 0) throw new ArgumentOutOfRangeException(nameof(monitorIntervalSeconds));
2228
_pipeline = pipeline;
2329
_blockWhenFull = blockWhenFull;
2430
_queue = new BlockingCollection<LogEvent>(bufferCapacity);
2531
_worker = Task.Factory.StartNew(Pump, CancellationToken.None, TaskCreationOptions.LongRunning | TaskCreationOptions.DenyChildAttach, TaskScheduler.Default);
32+
33+
if (monitor != null)
34+
{
35+
if (monitorIntervalSeconds < 1) throw new ArgumentOutOfRangeException(nameof(monitorIntervalSeconds), "must be >=1");
36+
#if! NETSTANDARD_NO_TIMER
37+
var interval = TimeSpan.FromSeconds(monitorIntervalSeconds);
38+
_monitorCallbackInvocationTimer = new Timer(queue => monitor((BlockingCollection<LogEvent>)queue), _queue, interval, interval);
39+
#else
40+
throw new PlatformNotSupportedException($"Please use a platform supporting .netstandard1.2 or later to avail of the ${nameof(monitor)} facility.");
41+
#endif
42+
}
2643
}
2744

2845
public void Emit(LogEvent logEvent)
@@ -55,8 +72,13 @@ public void Dispose()
5572
_queue.CompleteAdding();
5673

5774
// Allow queued events to be flushed
58-
_worker.Wait();
59-
75+
_worker.Wait();
76+
77+
#if! NETSTANDARD_NO_TIMER
78+
// Only stop monitoring when we've actually completed flushing
79+
_monitorCallbackInvocationTimer?.Dispose();
80+
#endif
81+
6082
(_pipeline as IDisposable)?.Dispose();
6183
}
6284

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;
@@ -185,6 +186,30 @@ public async Task GivenConfiguredToBlock_WhenQueueFilled_ThenBlocks()
185186
}
186187
}
187188

189+
#if !NETSTANDARD_NO_TIMER
190+
[Fact]
191+
public void MonitorArgumentAffordsBacklogHealthMonitoringFacility()
192+
{
193+
bool logWasObservedToHaveReachedHalfFull = false;
194+
void inspectBuffer(BlockingCollection<LogEvent> queue) =>
195+
196+
logWasObservedToHaveReachedHalfFull = logWasObservedToHaveReachedHalfFull
197+
|| queue.Count * 100 / queue.BoundedCapacity >= 50;
198+
199+
var collector = new MemorySink { DelayEmit = TimeSpan.FromSeconds(3) };
200+
using (var logger = new LoggerConfiguration()
201+
.WriteTo.Async(w => w.Sink(collector), bufferSize: 2, monitorIntervalSeconds: 1, monitor: inspectBuffer)
202+
.CreateLogger())
203+
{
204+
logger.Information("Something to block the pipe");
205+
logger.Information("I'll just leave this here pending for a few seconds so I can observe it");
206+
System.Threading.Thread.Sleep(TimeSpan.FromSeconds(2));
207+
}
208+
209+
Assert.True(logWasObservedToHaveReachedHalfFull);
210+
}
211+
#endif
212+
188213
private BackgroundWorkerSink CreateSinkWithDefaultOptions()
189214
{
190215
return new BackgroundWorkerSink(_logger, 10000, false);

0 commit comments

Comments
 (0)