You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@logging.apache.org by fr...@apache.org on 2026/09/21 19:03:54 UTC

(logging-log4net) branch Feature/324-sinks-and-debug-echo created (now f17aba5b)

This is an automated email from the ASF dual-hosted git repository.

FreeAndNil pushed a change to branch Feature/324-sinks-and-debug-echo
in repository https://gitbox.apache.org/repos/asf/logging-log4net.git


      at f17aba5b truncate an oversize UDP datagram instead of losing the event #324

This branch includes the following new commits:

     new f17aba5b truncate an oversize UDP datagram instead of losing the event #324

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.



(logging-log4net) 01/01: truncate an oversize UDP datagram instead of losing the event #324

Posted by fr...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

FreeAndNil pushed a commit to branch Feature/324-sinks-and-debug-echo
in repository https://gitbox.apache.org/repos/asf/logging-log4net.git

commit f17aba5b4980dda7413ee012b6cbf08fbe7a2be3
Author: Jan Friedrich <fr...@apache.org>
AuthorDate: Mon Sep 21 21:03:24 2026 +0200

    truncate an oversize UDP datagram instead of losing the event #324
    
    - the socket rejects a payload over 65507 bytes, so the event was lost
    - new MaxDatagramSize, default 65507, configurable from 512
    - cut on a character boundary, marked with ...[truncated]
    - RemoteSyslogAppender uses the same encoder
    
    audit da18b6fd-f015
---
 .../3.5.0/324-udp-oversize-truncation.xml          |  14 +++
 .../Appender/RemoteSyslogAppenderTest.cs           |  22 +++-
 src/log4net.Tests/Appender/UdpAppenderTest.cs      | 121 +++++++++++++++++++++
 src/log4net/Appender/RemoteSyslogAppender.cs       |   2 +-
 src/log4net/Appender/UdpAppender.cs                |  89 ++++++++++++++-
 .../appenders/remotesyslogappender.adoc            |   1 +
 .../configuration/appenders/udpappender.adoc       |   8 +-
 7 files changed, 253 insertions(+), 4 deletions(-)

diff --git a/src/changelog/3.5.0/324-udp-oversize-truncation.xml b/src/changelog/3.5.0/324-udp-oversize-truncation.xml
new file mode 100644
index 00000000..948414a8
--- /dev/null
+++ b/src/changelog/3.5.0/324-udp-oversize-truncation.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<entry xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns="https://logging.apache.org/xml/ns"
+       xsi:schemaLocation="https://logging.apache.org/xml/ns https://logging.apache.org/xml/ns/log4j-changelog-0.xsd"
+       type="fixed">
+  <issue id="324" link="https://github.com/apache/logging-log4net/pull/324"/>
+  <description format="asciidoc">
+    stop `UdpAppender` and `RemoteSyslogAppender` losing an event the socket refuses to send. An
+ event over the new `MaxDatagramSize` (default 65507, configurable down to 512) is truncated on a
+ character boundary and marked `...[truncated]`, instead of being dropped with only the first loss
+ reported. The limit is the IPv4 maximum and cannot be raised, an IPv6 packet would carry 20 bytes
+ more (audit da18b6fd-f015, fixed by @FreeAndNil)
+  </description>
+</entry>
diff --git a/src/log4net.Tests/Appender/RemoteSyslogAppenderTest.cs b/src/log4net.Tests/Appender/RemoteSyslogAppenderTest.cs
index 9eb299a2..3a0118f0 100644
--- a/src/log4net.Tests/Appender/RemoteSyslogAppenderTest.cs
+++ b/src/log4net.Tests/Appender/RemoteSyslogAppenderTest.cs
@@ -67,6 +67,21 @@ public void NonAsciiContentIsEscapedAndNotDeleted()
       Is.EqualTo(@"<14>TestDomain: INFO  - Sch\u00f6nwetter \u4f60\u597d"));
   }
 
+  /// <summary>
+  /// A datagram over the limit is rejected by the socket, which used to cost the whole record.
+  /// </summary>
+  [Test]
+  public void AnOversizeRecordIsTruncatedInsteadOfLost()
+  {
+    List<byte[]> sentBytes = [];
+    LogLog.ExecuteWithoutEmittingInternalMessages(
+      () => sentBytes = ExecuteAppend(new string('a', 1000), maxDatagramSize: 512));
+
+    Assert.That(sentBytes, Has.Count.EqualTo(1));
+    Assert.That(sentBytes[0], Has.Length.EqualTo(512));
+    Assert.That(Encoding.ASCII.GetString(sentBytes[0]), Does.EndWith("...[truncated]"));
+  }
+
   /// <summary>A control character other than CR or LF was dropped as well.</summary>
   [Test]
   public void OtherControlCharactersAreEscaped()
@@ -275,7 +290,8 @@ public void IdentityWithoutControlCharactersIsUnchangedAndNotReported()
 
   private static List<byte[]> ExecuteAppend(string message,
     SyslogNewLineHandling newLineHandling = default,
-    string? identity = null)
+    string? identity = null,
+    int? maxDatagramSize = null)
   {
     System.Net.IPAddress ipAddress = new([127, 0, 0, 1]);
     RemoteAppender appender = new()
@@ -285,6 +301,10 @@ public void IdentityWithoutControlCharactersIsUnchangedAndNotReported()
       NewLineHandling = newLineHandling,
       Identity = identity is null ? null : new PatternLayout(identity)
     };
+    if (maxDatagramSize is int size)
+    {
+      appender.MaxDatagramSize = size;
+    }
     appender.ActivateOptions();
     LoggingEvent loggingEvent = new(new()
     {
diff --git a/src/log4net.Tests/Appender/UdpAppenderTest.cs b/src/log4net.Tests/Appender/UdpAppenderTest.cs
new file mode 100644
index 00000000..c7512ad0
--- /dev/null
+++ b/src/log4net.Tests/Appender/UdpAppenderTest.cs
@@ -0,0 +1,121 @@
+#region Apache License
+//
+// Licensed to the Apache Software Foundation (ASF) under one or more 
+// contributor license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership. 
+// The ASF licenses this file to you 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.
+//
+#endregion
+
+using System;
+using System.Linq;
+using System.Reflection;
+using System.Text;
+using log4net.Appender;
+using log4net.Tests.Appender.Internal;
+using NUnit.Framework;
+
+namespace log4net.Tests.Appender;
+
+/// <summary>
+/// Tests for <see cref="UdpAppender"/>
+/// </summary>
+[TestFixture]
+public sealed class UdpAppenderTest
+{
+  private const string Marker = "...[truncated]";
+
+  /// <summary>Calls the internal encoder the way <c>Append</c> does.</summary>
+  private static byte[] Encode(UdpAppender appender, string message)
+    => (byte[])typeof(UdpAppender)
+      .GetMethod("GetDatagramBytes", BindingFlags.Instance | BindingFlags.NonPublic)!
+      .Invoke(appender, [message])!;
+
+  /// <summary>The maximum UDP payload of an IPv4 packet, measured as the first size the socket rejects minus one.</summary>
+  [Test]
+  public void MaxDatagramSizeDefaultsTo65507()
+    => Assert.That(new UdpAppender().MaxDatagramSize, Is.EqualTo(65507));
+
+  /// <summary>Below 512 nothing useful fits, above 65507 an IPv4 socket rejects the datagram.</summary>
+  [TestCase(511)]
+  [TestCase(65508)]
+  public void MaxDatagramSizeRejectsValuesOutsideTheRange(int value)
+    => Assert.That(() => new UdpAppender().MaxDatagramSize = value,
+      Throws.TypeOf<ArgumentOutOfRangeException>());
+
+  /// <summary>An event that fits is sent exactly as the layout rendered it.</summary>
+  [Test]
+  public void AMessageThatFitsIsEncodedUnchanged()
+  {
+    UdpAppender appender = new() { Encoding = Encoding.UTF8 };
+
+    Assert.That(Encode(appender, "hello"), Is.EqualTo(Encoding.UTF8.GetBytes("hello")));
+  }
+
+  /// <summary>An event of exactly the limit still fits, so nothing is cut and nothing reported.</summary>
+  [Test]
+  public void AMessageOfExactlyTheLimitIsEncodedUnchanged()
+  {
+    UdpAppender appender = new() { Encoding = Encoding.ASCII, MaxDatagramSize = 512 };
+    RecordingErrorHandler errorHandler = new();
+    appender.ErrorHandler = errorHandler;
+
+    byte[] datagram = Encode(appender, new string('a', 512));
+
+    Assert.That(datagram, Is.EqualTo(Encoding.ASCII.GetBytes(new string('a', 512))));
+    Assert.That(errorHandler.Messages, Is.Empty);
+  }
+
+  /// <summary>
+  /// An oversize datagram used to be rejected by the socket, which cost the whole event.
+  /// </summary>
+  [Test]
+  public void AnOversizeMessageIsTruncatedInsteadOfLost()
+  {
+    UdpAppender appender = new() { Encoding = Encoding.ASCII, MaxDatagramSize = 512 };
+    RecordingErrorHandler errorHandler = new();
+    appender.ErrorHandler = errorHandler;
+
+    byte[] datagram = Encode(appender, new string('a', 1000));
+
+    Assert.That(datagram, Has.Length.EqualTo(512));
+    Assert.That(Encoding.ASCII.GetString(datagram), Does.EndWith(Marker));
+    Assert.That(errorHandler.Messages, Has.Count.EqualTo(1));
+  }
+
+  /// <summary>A cut inside a character would encode as the replacement character.</summary>
+  [Test]
+  public void TruncationDoesNotSplitAMultiByteCharacter()
+  {
+    UdpAppender appender = new() { Encoding = Encoding.UTF8, MaxDatagramSize = 513 };
+    appender.ErrorHandler = new RecordingErrorHandler();
+
+    string decoded = Encoding.UTF8.GetString(Encode(appender, new string('\u00e4', 1000)));
+
+    // 513 bytes hold the 14 byte marker and 249 of the two byte characters.
+    Assert.That(decoded, Is.EqualTo(new string('\u00e4', 249) + Marker));
+  }
+
+  /// <summary>The same for a cut between the two halves of a surrogate pair.</summary>
+  [Test]
+  public void TruncationDoesNotSplitASurrogatePair()
+  {
+    UdpAppender appender = new() { Encoding = Encoding.UTF8, MaxDatagramSize = 515 };
+    appender.ErrorHandler = new RecordingErrorHandler();
+
+    string decoded = Encoding.UTF8.GetString(Encode(appender, string.Concat(Enumerable.Repeat("\U0001f600", 1000))));
+
+    Assert.That(decoded, Does.EndWith(Marker));
+    Assert.That(decoded, !Contains.Substring("\ufffd").Using(StringComparison.Ordinal));
+  }
+}
diff --git a/src/log4net/Appender/RemoteSyslogAppender.cs b/src/log4net/Appender/RemoteSyslogAppender.cs
index 00a55dab..128b9127 100644
--- a/src/log4net/Appender/RemoteSyslogAppender.cs
+++ b/src/log4net/Appender/RemoteSyslogAppender.cs
@@ -367,7 +367,7 @@ protected override void Append(LoggingEvent loggingEvent)
         AppendMessage(message, ref i, builder);
 
         // Grab as a byte array
-        byte[] buffer = Encoding.GetBytes(builder.ToString());
+        byte[] buffer = GetDatagramBytes(builder.ToString());
 
         // A full queue is reported by the sender itself.
         _sender?.TryEnqueue(buffer, EnqueueTimeoutMillis);
diff --git a/src/log4net/Appender/UdpAppender.cs b/src/log4net/Appender/UdpAppender.cs
index 2e0dc957..d0e75577 100644
--- a/src/log4net/Appender/UdpAppender.cs
+++ b/src/log4net/Appender/UdpAppender.cs
@@ -257,6 +257,30 @@ public int LocalPort
   /// </remarks>
   public Encoding Encoding { get; set; } = Encoding.Default;
 
+  /// <summary>
+  /// Gets or sets the largest datagram this appender sends, in bytes.
+  /// </summary>
+  /// <value>A value from 512 to 65507, the maximum UDP payload of an IPv4 packet. Default 65507.</value>
+  /// <remarks>
+  /// The socket rejects a larger datagram, so a longer event is truncated to fit instead of lost.
+  /// Lower the value to match a receiver that accepts less. IPv6 would carry 20 bytes more, which
+  /// is not worth a second limit.
+  /// </remarks>
+  /// <exception cref="ArgumentOutOfRangeException">The value specified is outside 512 to 65507.</exception>
+  public int MaxDatagramSize
+  {
+    get => _maxDatagramSize;
+    set
+    {
+      if (value is < MinDatagramSize or > MaxIPv4DatagramSize)
+      {
+        throw SystemInfo.CreateArgumentOutOfRangeException(nameof(value), value,
+          $"The value specified is less than {MinDatagramSize} or greater than {MaxIPv4DatagramSize}.");
+      }
+      _maxDatagramSize = value;
+    }
+  }
+
   /// <summary>
   /// Gets or sets the underlying <see cref="UdpClient" />.
   /// </summary>
@@ -348,7 +372,7 @@ protected override void Append(LoggingEvent loggingEvent)
 
     try
     {
-      byte[] buffer = Encoding.GetBytes(RenderLoggingEvent(loggingEvent).ToCharArray());
+      byte[] buffer = GetDatagramBytes(RenderLoggingEvent(loggingEvent));
       Client.Send(buffer, buffer.Length, RemoteEndPoint);
     }
     catch (Exception e) when (!e.IsFatal())
@@ -359,6 +383,49 @@ protected override void Append(LoggingEvent loggingEvent)
     }
   }
 
+  /// <summary>
+  /// Encodes a rendered event for one datagram, truncated to <see cref="MaxDatagramSize"/>.
+  /// </summary>
+  /// <param name="message">The rendered event.</param>
+  /// <returns>The bytes to send, ending in <c>...[truncated]</c> when the event did not fit.</returns>
+  /// <remarks>
+  /// An oversize datagram is rejected by the socket, which used to cost the whole event.
+  /// </remarks>
+  private protected byte[] GetDatagramBytes(string message)
+  {
+    byte[] datagram = Encoding.GetBytes(message.EnsureNotNull());
+    if (datagram.Length <= MaxDatagramSize)
+    {
+      return datagram;
+    }
+
+    byte[] result = GetTruncatedBytes(message, datagram);
+
+    ErrorHandler.Error(
+      $"Truncated a logging event from {datagram.Length} to {result.Length} bytes for {RemoteAddress} on port {RemotePort}.");
+    return result;
+  }
+
+  /// <summary>Encodes as much of <paramref name="message"/> as fits, ending in the marker.</summary>
+  /// <param name="message">The rendered event.</param>
+  /// <param name="datagram">The whole encoded event, reused as the encoder's output.</param>
+  /// <returns>The bytes to send, of at most <see cref="MaxDatagramSize"/> length.</returns>
+  private byte[] GetTruncatedBytes(string message, byte[] datagram)
+  {
+    int markerLength = Encoding.GetByteCount(TruncationMarker);
+
+    // Convert fills the budget without splitting a character, and unflushed it holds back a
+    // trailing high surrogate rather than encoding it as the replacement character. It writes
+    // into the oversize encoding, which is the one buffer already long enough.
+    Encoding.GetEncoder().Convert(message.ToCharArray(), 0, message.Length,
+      datagram, 0, MaxDatagramSize - markerLength, flush: false, out _, out int written, out _);
+
+    byte[] result = new byte[written + markerLength];
+    Buffer.BlockCopy(datagram, 0, result, 0, written);
+    Encoding.GetBytes(TruncationMarker, 0, TruncationMarker.Length, result, written);
+    return result;
+  }
+
   /// <summary>
   /// This appender requires a <see cref="Layout"/> to be set.
   /// </summary>
@@ -419,4 +486,24 @@ protected virtual void InitializeClientConnection()
   /// The TCP port number from which the <see cref="UdpClient" /> will communicate.
   /// </summary>
   private int _localPort;
+
+  /// <summary>
+  /// The largest datagram this appender sends.
+  /// </summary>
+  private int _maxDatagramSize = MaxIPv4DatagramSize;
+
+  /// <summary>
+  /// The maximum UDP payload of an IPv4 packet.
+  /// </summary>
+  private const int MaxIPv4DatagramSize = 65507;
+
+  /// <summary>
+  /// The smallest datagram size an operator may configure.
+  /// </summary>
+  private const int MinDatagramSize = 512;
+
+  /// <summary>
+  /// Marks a datagram that could not carry the whole event.
+  /// </summary>
+  private const string TruncationMarker = "...[truncated]";
 }
\ No newline at end of file
diff --git a/src/site/antora/modules/ROOT/pages/manual/configuration/appenders/remotesyslogappender.adoc b/src/site/antora/modules/ROOT/pages/manual/configuration/appenders/remotesyslogappender.adoc
index 9fef8942..b35d314b 100644
--- a/src/site/antora/modules/ROOT/pages/manual/configuration/appenders/remotesyslogappender.adoc
+++ b/src/site/antora/modules/ROOT/pages/manual/configuration/appenders/remotesyslogappender.adoc
@@ -44,6 +44,7 @@ You can also specify:
 * Identity (default: application name)
 * SendQueueSize (default: 500), how many datagrams may wait to be sent
 * EnqueueTimeoutMillis (default: 5000), how long a logging call waits for room in a full queue
+* MaxDatagramSize (default: 65507), the largest datagram sent, inherited from the xref:./udpappender.adoc[]
 
 RFC 3164 allows only the visible ASCII characters and space in the message, so anything else is
 written as a `\uXXXX` escape rather than dropped: a message in a non-Latin script reaches the
diff --git a/src/site/antora/modules/ROOT/pages/manual/configuration/appenders/udpappender.adoc b/src/site/antora/modules/ROOT/pages/manual/configuration/appenders/udpappender.adoc
index 0cac823d..d19a0dca 100644
--- a/src/site/antora/modules/ROOT/pages/manual/configuration/appenders/udpappender.adoc
+++ b/src/site/antora/modules/ROOT/pages/manual/configuration/appenders/udpappender.adoc
@@ -33,4 +33,10 @@ The following example shows how to configure the `UdpAppender` to send events to
     <conversionPattern value="%-5level %logger - %message%newline" />
   </layout>
 </appender>
-----
\ No newline at end of file
+----
+
+An event that does not fit one datagram is truncated and marked with `...[truncated]` rather than
+dropped: the socket rejects a payload above `MaxDatagramSize` (default 65507, the maximum UDP
+payload of an IPv4 packet), and that used to cost the whole event. Lower `MaxDatagramSize`, down to
+512, to match a receiver that accepts less. It cannot be raised: an IPv6 packet would carry 20 bytes
+more, which is not worth a second limit.