Skip to content

Commit f5efd75

Browse files
committed
send syslog datagrams through BackgroundSender #314
The appender's own pump held an unbounded queue, so a syslog server that stopped accepting datagrams grew it until the process ran out of memory. Shutdown waited five seconds and then abandoned a drain that had no limit of its own. It now holds SendQueueSize datagrams (500), a logging call waits at most EnqueueTimeoutMillis (5000) for room, losses are counted, and Flush honours its timeout: this appender sends asynchronously, so unlike a buffering one the timeout means something here.
1 parent 0463e2e commit f5efd75

4 files changed

Lines changed: 145 additions & 75 deletions

File tree

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<entry xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
3+
xmlns="https://logging.apache.org/xml/ns"
4+
xsi:schemaLocation="https://logging.apache.org/xml/ns https://logging.apache.org/xml/ns/log4j-changelog-0.xsd"
5+
type="fixed">
6+
<issue id="314" link="https://github.com/apache/logging-log4net/pull/314"/>
7+
<description format="asciidoc">
8+
bound the queue `RemoteSyslogAppender` sends from. It was unbounded, so a syslog server that
9+
stopped accepting datagrams grew it until the process ran out of memory, and shutdown waited five
10+
seconds and then abandoned a drain that had no limit of its own. It now holds `sendQueueSize`
11+
datagrams (500), a logging call waits at most `enqueueTimeoutMillis` (5000) for room, losses are
12+
counted and reported, and `Flush` honours its timeout (implemented by @FreeAndNil)
13+
</description>
14+
</entry>

‎src/log4net.Tests/Appender/RemoteSyslogAppenderTest.cs‎

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121
using System.Collections.Generic;
2222
using System.Linq;
2323
using System.Text;
24-
using System.Threading;
2524
using log4net.Appender;
2625
using log4net.Appender.Internal;
2726
using log4net.Core;
@@ -68,6 +67,31 @@ private sealed class RecordingErrorHandler : IErrorHandler
6867
public void Error(string message) => Messages.Add(message);
6968
}
7069

70+
private const int FlushTimeoutMillis = 30_000;
71+
72+
/// <summary>Bounded, so an unreachable server cannot grow the queue without limit.</summary>
73+
[Test]
74+
public void SendQueueSizeDefaultsTo500()
75+
=> Assert.That(new RemoteSyslogAppender().SendQueueSize, Is.EqualTo(500));
76+
77+
/// <summary>The longest a logging call waits when the queue is full.</summary>
78+
[Test]
79+
public void EnqueueTimeoutMillisDefaultsTo5000()
80+
=> Assert.That(new RemoteSyslogAppender().EnqueueTimeoutMillis, Is.EqualTo(5_000));
81+
82+
/// <summary>A queue with no room for anything cannot work.</summary>
83+
[TestCase(0)]
84+
[TestCase(-1)]
85+
public void SendQueueSizeRejectsValuesThatAreNotPositive(int value)
86+
=> Assert.That(() => new RemoteSyslogAppender().SendQueueSize = value,
87+
Throws.TypeOf<ArgumentOutOfRangeException>());
88+
89+
/// <summary>0 is allowed and never waits, a negative wait is meaningless.</summary>
90+
[Test]
91+
public void EnqueueTimeoutMillisRejectsANegativeValue()
92+
=> Assert.That(() => new RemoteSyslogAppender().EnqueueTimeoutMillis = -1,
93+
Throws.TypeOf<ArgumentOutOfRangeException>());
94+
7195
/// <summary>
7296
/// This appender sends through the connection its pump owns, so the inherited one would be a
7397
/// second socket, holding a second binding of LocalPort, that nothing ever uses.
@@ -262,13 +286,7 @@ private static List<byte[]> ExecuteAppend(string message,
262286
Domain = "TestDomain",
263287
});
264288
appender.DoAppend(loggingEvent);
265-
for (int i = 0; i < 20; i++)
266-
{
267-
if (appender.Mock.Sent.Count == 0)
268-
{
269-
Thread.Sleep(10);
270-
}
271-
}
289+
Assert.That(appender.Flush(FlushTimeoutMillis), Is.True);
272290
appender.Close();
273291
Assert.That(appender.Mock.ConnectedTo, Is.EqualTo((0, ipAddress, 514)));
274292
Assert.That(appender.Mock.WasDisposed, Is.True);

‎src/log4net/Appender/RemoteSyslogAppender.cs‎

Lines changed: 98 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,8 @@
1818
#endregion
1919

2020
using System;
21-
using System.Collections.Concurrent;
2221
using System.Text;
2322
using System.Threading;
24-
using System.Threading.Tasks;
2523
using log4net.Appender.Internal;
2624
using log4net.Core;
2725
using log4net.Layout;
@@ -279,9 +277,11 @@ public enum SyslogNewLineHandling
279277
Keep
280278
}
281279

282-
private readonly BlockingCollection<byte[]> _sendQueue = new();
283-
private CancellationTokenSource? _cancellationTokenSource;
284-
private Task? _pumpTask;
280+
private const int CloseTimeoutMillis = 5_000;
281+
private IUdpConnection? _connection;
282+
private BackgroundSender<byte[]>? _sender;
283+
private int _sendQueueSize = 500;
284+
private int _enqueueTimeoutMillis = 5_000;
285285

286286
/// <summary>
287287
/// Initializes a new instance of the <see cref="RemoteSyslogAppender" /> class.
@@ -389,7 +389,8 @@ protected override void Append(LoggingEvent loggingEvent)
389389
// Grab as a byte array
390390
byte[] buffer = Encoding.GetBytes(builder.ToString());
391391

392-
_sendQueue.Add(buffer);
392+
// A full queue is reported by the sender itself.
393+
_sender?.TryEnqueue(buffer, EnqueueTimeoutMillis);
393394
}
394395
}
395396
catch (Exception e) when (!e.IsFatal())
@@ -524,10 +525,89 @@ public override void ActivateOptions()
524525
$"The NewLineHandling is not {SyslogNewLineHandling.Escape} or {SyslogNewLineHandling.Keep} or {SyslogNewLineHandling.Split}.");
525526
}
526527
_levelMapping.ActivateOptions();
527-
// Start the background pump
528-
_cancellationTokenSource = new();
529-
_pumpTask = Task.Factory.StartNew(() => ProcessQueueAsync(_cancellationTokenSource.Token), CancellationToken.None,
530-
TaskCreationOptions.LongRunning, TaskScheduler.Default);
528+
StopSending();
529+
530+
IUdpConnection connection = CreateUdpConnection();
531+
try
532+
{
533+
connection.Connect(LocalPort, RemoteAddress.EnsureNotNull(), RemotePort);
534+
}
535+
catch (Exception e) when (!e.IsFatal())
536+
{
537+
ErrorHandler.Error(
538+
$"Unable to connect to remote syslog {RemoteAddress} on port {RemotePort} from local port {LocalPort}. "
539+
+ "No logging event will be sent.",
540+
e, ErrorCode.GenericFailure);
541+
connection.Dispose();
542+
return;
543+
}
544+
545+
_connection = connection;
546+
_sender = new(nameof(RemoteSyslogAppender), SendQueueSize, Send, Report);
547+
}
548+
549+
/// <summary>
550+
/// How many datagrams may wait to be sent. Defaults to 500.
551+
/// </summary>
552+
/// <exception cref="ArgumentOutOfRangeException">The value specified is not positive.</exception>
553+
public int SendQueueSize
554+
{
555+
get => _sendQueueSize;
556+
set
557+
{
558+
if (value <= 0)
559+
{
560+
throw SystemInfo.CreateArgumentOutOfRangeException(nameof(value), value,
561+
"The value specified for SendQueueSize is not positive.");
562+
}
563+
_sendQueueSize = value;
564+
}
565+
}
566+
567+
/// <summary>
568+
/// How long a logging call waits for room in a full queue. Defaults to 5000, 0 never waits.
569+
/// </summary>
570+
/// <exception cref="ArgumentOutOfRangeException">The value specified is negative.</exception>
571+
public int EnqueueTimeoutMillis
572+
{
573+
get => _enqueueTimeoutMillis;
574+
set
575+
{
576+
if (value < 0)
577+
{
578+
throw SystemInfo.CreateArgumentOutOfRangeException(nameof(value), value,
579+
"The value specified for EnqueueTimeoutMillis is negative.");
580+
}
581+
_enqueueTimeoutMillis = value;
582+
}
583+
}
584+
585+
private void Send(byte[] datagram, CancellationToken cancellationToken)
586+
=> _connection?.SendAsync(datagram, datagram.Length).GetAwaiter().GetResult();
587+
588+
private void Report(string message, Exception? exception)
589+
{
590+
if (exception is null)
591+
{
592+
ErrorHandler.Error(message);
593+
}
594+
else
595+
{
596+
ErrorHandler.Error(message, exception);
597+
}
598+
}
599+
600+
private void StopSending()
601+
{
602+
if (_sender is BackgroundSender<byte[]> sender)
603+
{
604+
_sender = null;
605+
sender.Close(CloseTimeoutMillis);
606+
sender.Dispose();
607+
}
608+
609+
_connection?.Dispose();
610+
_connection = null;
531611
}
532612

533613
/// <summary>
@@ -636,12 +716,17 @@ public class LevelSeverity : LevelMappingEntry
636716
public SyslogSeverity Severity { get; set; }
637717
}
638718

719+
/// <summary>
720+
/// Waits until everything queued so far has been sent.
721+
/// </summary>
722+
/// <param name="millisecondsTimeout">The maximum time to wait.</param>
723+
/// <returns><see langword="false"/> on timeout, or when the sender is no longer running.</returns>
724+
public override bool Flush(int millisecondsTimeout) => _sender?.Flush(millisecondsTimeout) ?? true;
725+
639726
/// <inheritdoc/>
640727
protected override void OnClose()
641728
{
642-
// Signal shutdown and wait for the pump to drain
643-
_cancellationTokenSource?.Cancel();
644-
_pumpTask?.Wait(TimeSpan.FromSeconds(5));
729+
StopSending();
645730
base.OnClose();
646731
}
647732

@@ -653,58 +738,4 @@ protected override void OnClose()
653738
protected override void InitializeClientConnection()
654739
{
655740
}
656-
657-
private async Task ProcessQueueAsync(CancellationToken token)
658-
{
659-
// We create our own UdpClient here, so that client lifetime is tied to this task
660-
IUdpConnection udpClient;
661-
try
662-
{
663-
udpClient = CreateUdpConnection();
664-
udpClient.Connect(LocalPort, RemoteAddress.EnsureNotNull(), RemotePort);
665-
}
666-
catch (Exception e) when (!e.IsFatal())
667-
{
668-
// Outside the loop below, so this would otherwise fault the pump unobserved and leave
669-
// every later event queued behind a sender that is no longer running.
670-
ErrorHandler.Error(
671-
$"Unable to connect to remote syslog {RemoteAddress} on port {RemotePort} from local port {LocalPort}. "
672-
+ "No logging event will be sent.",
673-
e, ErrorCode.GenericFailure);
674-
return;
675-
}
676-
677-
using IUdpConnection connection = udpClient;
678-
try
679-
{
680-
while (!token.IsCancellationRequested)
681-
{
682-
// Take next message or throw when canceled
683-
byte[] datagram = _sendQueue.Take(token);
684-
try
685-
{
686-
await udpClient.SendAsync(datagram, datagram.Length).ConfigureAwait(false);
687-
}
688-
catch (Exception ex) when (!ex.IsFatal())
689-
{
690-
ErrorHandler.Error("RemoteSyslogAppender: send failed", ex, ErrorCode.WriteFailure);
691-
}
692-
}
693-
}
694-
catch (OperationCanceledException)
695-
{
696-
// Clean shutdown: drain remaining items if desired
697-
while (_sendQueue.TryTake(out byte[]? leftover))
698-
{
699-
try
700-
{
701-
await udpClient.SendAsync(leftover, leftover.Length).ConfigureAwait(false);
702-
}
703-
catch (Exception ex) when (!ex.IsFatal())
704-
{
705-
ErrorHandler.Error("RemoteSyslogAppender: send failed during shutdown", ex, ErrorCode.FlushFailure);
706-
}
707-
}
708-
}
709-
}
710741
}

‎src/site/antora/modules/ROOT/pages/manual/configuration/appenders/remotesyslogappender.adoc‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,13 @@ You can also specify:
4242
4343
* Facility (default: user)
4444
* Identity (default: application name)
45+
* SendQueueSize (default: 500), how many datagrams may wait to be sent
46+
* EnqueueTimeoutMillis (default: 5000), how long a logging call waits for room in a full queue
47+
48+
Datagrams are handed to a background thread, so a slow or unreachable syslog server does not hold
49+
up logging. Once the queue is full, a logging call waits `EnqueueTimeoutMillis` for room and the
50+
datagram is then discarded and counted, rather than growing the queue without limit. `Flush` waits
51+
for the queue to drain.
4552
4653
[source,xml]
4754
----

0 commit comments

Comments
 (0)