blob: 3d30c5262f40d7ce9735144bd3d46951b3f09e95 [file]
#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.Collections.Concurrent;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using log4net.Appender.Internal;
using log4net.Core;
using log4net.Layout;
using log4net.Util;
namespace log4net.Appender;
/// <summary>
/// Logs events to a remote syslog daemon.
/// </summary>
/// <remarks>
/// <para>
/// The BSD syslog protocol is used to remotely log to
/// a syslog daemon. The syslogd listens for messages
/// on UDP port 514.
/// </para>
/// <para>
/// The syslog UDP protocol is not authenticated. Most syslog daemons
/// do not accept remote log messages because of the security implications.
/// You may be able to use the LocalSyslogAppender to talk to a local
/// syslog service.
/// </para>
/// <para>
/// There is an RFC 3164 that claims to document the BSD Syslog Protocol.
/// This RFC can be seen here: http://www.faqs.org/rfcs/rfc3164.html.
/// This appender generates what the RFC calls an "Original Device Message",
/// i.e. does not include the TIMESTAMP or HOSTNAME fields. By observation
/// this format of message will be accepted by all current syslog daemon
/// implementations. The daemon will attach the current time and the source
/// hostname or IP address to any messages received.
/// </para>
/// <para>
/// Syslog messages must have a facility and a severity. The severity
/// is derived from the Level of the logging event.
/// The facility must be chosen from the set of defined syslog
/// <see cref="SyslogFacility"/> values. The facilities list is predefined
/// and cannot be extended.
/// </para>
/// <para>
/// An identifier is specified with each log message. This can be specified
/// by setting the <see cref="Identity"/> property. The identity (also known
/// as the tag) must not contain white space. The default value for the
/// identity is the application name (from <see cref="LoggingEvent.Domain"/>).
/// </para>
/// </remarks>
/// <author>Rob Lyon</author>
/// <author>Nicko Cadell</author>
public class RemoteSyslogAppender : UdpAppender
{
/// <summary>
/// Syslog port 514
/// </summary>
private const int DefaultSyslogPort = 514;
/// <summary>
/// syslog severities
/// </summary>
/// <remarks>
/// <para>
/// The syslog severities.
/// </para>
/// </remarks>
public enum SyslogSeverity
{
/// <summary>
/// system is unusable
/// </summary>
Emergency = 0,
/// <summary>
/// action must be taken immediately
/// </summary>
Alert = 1,
/// <summary>
/// critical conditions
/// </summary>
Critical = 2,
/// <summary>
/// error conditions
/// </summary>
Error = 3,
/// <summary>
/// warning conditions
/// </summary>
Warning = 4,
/// <summary>
/// normal but significant condition
/// </summary>
Notice = 5,
/// <summary>
/// informational
/// </summary>
Informational = 6,
/// <summary>
/// debug-level messages
/// </summary>
Debug = 7
};
/// <summary>
/// syslog facilities
/// </summary>
/// <remarks>
/// <para>
/// The syslog facilities
/// </para>
/// </remarks>
public enum SyslogFacility
{
/// <summary>
/// kernel messages
/// </summary>
Kernel = 0,
/// <summary>
/// random user-level messages
/// </summary>
User = 1,
/// <summary>
/// mail system
/// </summary>
Mail = 2,
/// <summary>
/// system daemons
/// </summary>
Daemons = 3,
/// <summary>
/// security/authorization messages
/// </summary>
Authorization = 4,
/// <summary>
/// messages generated internally by syslogd
/// </summary>
Syslog = 5,
/// <summary>
/// line printer subsystem
/// </summary>
Printer = 6,
/// <summary>
/// network news subsystem
/// </summary>
News = 7,
/// <summary>
/// UUCP subsystem
/// </summary>
Uucp = 8,
/// <summary>
/// clock (cron/at) daemon
/// </summary>
Clock = 9,
/// <summary>
/// security/authorization messages (private)
/// </summary>
Authorization2 = 10,
/// <summary>
/// ftp daemon
/// </summary>
Ftp = 11,
/// <summary>
/// NTP subsystem
/// </summary>
Ntp = 12,
/// <summary>
/// log audit
/// </summary>
Audit = 13,
/// <summary>
/// log alert
/// </summary>
Alert = 14,
/// <summary>
/// clock daemon
/// </summary>
Clock2 = 15,
/// <summary>
/// reserved for local use
/// </summary>
Local0 = 16,
/// <summary>
/// reserved for local use
/// </summary>
Local1 = 17,
/// <summary>
/// reserved for local use
/// </summary>
Local2 = 18,
/// <summary>
/// reserved for local use
/// </summary>
Local3 = 19,
/// <summary>
/// reserved for local use
/// </summary>
Local4 = 20,
/// <summary>
/// reserved for local use
/// </summary>
Local5 = 21,
/// <summary>
/// reserved for local use
/// </summary>
Local6 = 22,
/// <summary>
/// reserved for local use
/// </summary>
Local7 = 23
}
/// <summary>
/// Options for handling newlines (\r or \n) in <see ref="AppendMessage" />
/// </summary>
public enum SyslogNewLineHandling
{
/// <summary>
/// escape the newlines (\\r for \r and \\n for \n)
/// </summary>
Escape,
/// <summary>
/// split the message at new lines
/// </summary>
Split,
/// <summary>
/// keep newlines as is (many syslog servers can handle newlines in the message part)
/// </summary>
Keep
}
private readonly BlockingCollection<byte[]> _sendQueue = new();
private CancellationTokenSource? _cancellationTokenSource;
private Task? _pumpTask;
/// <summary>
/// Initializes a new instance of the <see cref="RemoteSyslogAppender" /> class.
/// </summary>
/// <remarks>
/// This instance of the <see cref="RemoteSyslogAppender" /> class is set up to write
/// to a remote syslog daemon.
/// </remarks>
public RemoteSyslogAppender()
{
// syslog udp defaults
RemotePort = DefaultSyslogPort;
RemoteAddress = System.Net.IPAddress.Parse("127.0.0.1");
Encoding = Encoding.ASCII;
}
/// <summary>
/// Message identity
/// </summary>
/// <remarks>
/// <para>
/// An identifier is specified with each log message. This can be specified
/// by setting the <see cref="Identity"/> property. The identity (also known
/// as the tag) must not contain white space. The default value for the
/// identity is the application name (from <see cref="LoggingEvent.Domain"/>).
/// </para>
/// </remarks>
public PatternLayout? Identity { get; set; }
/// <summary>
/// Syslog facility
/// </summary>
/// <remarks>
/// Set to one of the <see cref="SyslogFacility"/> values. The list of
/// facilities is predefined and cannot be extended. The default value
/// is <see cref="SyslogFacility.User"/>.
/// </remarks>
public SyslogFacility Facility { get; set; } = SyslogFacility.User;
/// <summary>
/// NewLine handling
/// </summary>
/// <remarks>The default value is <see cref="SyslogNewLineHandling.Escape"/>.</remarks>
public SyslogNewLineHandling NewLineHandling { get; set; }
/// <summary>
/// Gets or sets the delegate used to create instances of <see cref="IUdpConnection"/>.
/// </summary>
protected virtual IUdpConnection CreateUdpConnection() => new UdpConnection();
/// <summary>
/// Add a mapping of level to severity
/// </summary>
/// <param name="mapping">The mapping to add</param>
/// <remarks>
/// <para>
/// Add a <see cref="LevelSeverity"/> mapping to this appender.
/// </para>
/// </remarks>
public void AddMapping(LevelSeverity mapping) => _levelMapping.Add(mapping);
/// <summary>
/// Writes the event to a remote syslog daemon.
/// </summary>
/// <param name="loggingEvent">The event to log.</param>
/// <remarks>
/// <para>
/// This method is called by the <see cref="AppenderSkeleton.DoAppend(LoggingEvent)"/> method.
/// </para>
/// <para>
/// The format of the output will depend on the appender's layout.
/// </para>
/// </remarks>
protected override void Append(LoggingEvent loggingEvent)
{
loggingEvent.EnsureNotNull();
try
{
// Priority
int priority = GeneratePriority(Facility, GetSeverity(loggingEvent.Level));
// Identity
string? identity = ValidateIdentity(Identity?.Format(loggingEvent) ?? loggingEvent.Domain);
// Message. The message goes after the tag/identity
string message = RenderLoggingEvent(loggingEvent);
int i = 0;
StringBuilder builder = new();
while (i < message.Length)
{
// Clear StringBuilder
builder.Length = 0;
// Write priority
builder.Append('<').Append(priority).Append('>');
// Write identity
builder.Append(identity).Append(": ");
AppendMessage(message, ref i, builder);
// Grab as a byte array
byte[] buffer = Encoding.GetBytes(builder.ToString());
_sendQueue.Add(buffer);
}
}
catch (Exception e) when (!e.IsFatal())
{
ErrorHandler.Error(
$"Unable to enqueue logging event to remote syslog {RemoteAddress} on port {RemotePort}.",
e, ErrorCode.WriteFailure);
}
}
/// <summary>
/// Checks that <paramref name="identity"/> is usable as the TAG part of a syslog record, and
/// reports it through the <see cref="AppenderSkeleton.ErrorHandler"/> when it is not.
/// </summary>
/// <param name="identity">The formatted <see cref="Identity"/>.</param>
/// <returns>
/// The identity, with any control character removed.
/// </returns>
/// <remarks>
/// <para>
/// The TAG is a structural identifier and therefore expected to be a constant chosen by the
/// developer or operator, not something derived from a logging event. A malformed one is a
/// configuration error rather than untrusted input, so it is reported instead of being altered
/// silently: a carriage return or line feed in the TAG would split the record and let the text
/// after it be read as a second record with its own facility and severity.
/// </para>
/// <para>
/// The offending characters are removed rather than the event being dropped, so that an
/// <see cref="Identity"/> pattern which does contain event data cannot be used to suppress
/// records.
/// </para>
/// </remarks>
private string? ValidateIdentity(string? identity)
{
if (identity is null)
{
return null;
}
StringBuilder? sanitized = null;
for (int i = 0; i < identity.Length; i++)
{
// Control characters only. A carriage return or line feed ends the record, so the text
// after it is read as a record of its own. Printable characters are left alone, including
// the space that an application friendly name may contain, because they cannot break the
// record apart.
if (identity[i] is >= ' ' and not (char)127)
{
sanitized?.Append(identity[i]);
}
else
{
sanitized ??= new StringBuilder(identity.Length).Append(identity, 0, i);
}
}
if (sanitized is null)
{
return identity;
}
ErrorHandler.Error(
$"The Identity of appender [{Name}] rendered control characters, which would have split the syslog record, and they were removed. "
+ "Identity is a structural identifier and is expected to be a constant rather than a pattern that renders logging event data.");
return sanitized.ToString();
}
/// <summary>
/// Appends the rendered message to the buffer
/// </summary>
/// <param name="message">rendered message</param>
/// <param name="characterIndex">index of the current character in the message</param>
/// <param name="builder">buffer</param>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1045:Do not pass types by reference")]
protected virtual void AppendMessage(string message, ref int characterIndex, StringBuilder builder)
{
message.EnsureNotNull();
builder.EnsureNotNull();
for (; characterIndex < message.Length; characterIndex++)
{
char c = message[characterIndex];
// Accept only visible ASCII characters and space. See RFC 3164 section 4.1.3
if (c is >= ' ' and <= (char)126)
{
builder.Append(c);
}
// If character is newline
else if (c is '\r' or '\n')
{
if (NewLineHandling == SyslogNewLineHandling.Escape)
{
// escape
builder.Append(c == '\r' ? "\\r" : "\\n");
}
else if (NewLineHandling == SyslogNewLineHandling.Keep)
{
// keep
builder.Append(c);
}
else if (NewLineHandling == SyslogNewLineHandling.Split)
{
// break and send the current line
// Check the next character to handle \r\n or \n\r
if ((message.Length > characterIndex + 1)
&& ((message[characterIndex + 1] == '\r') || (message[characterIndex + 1] == '\n')))
{
characterIndex++;
}
characterIndex++;
break;
}
}
}
}
/// <summary>
/// Initialize the options for this appender
/// </summary>
/// <remarks>
/// <para>
/// Initialize the level to syslog severity mappings set on this appender.
/// </para>
/// </remarks>
public override void ActivateOptions()
{
base.ActivateOptions();
if (NewLineHandling is not (SyslogNewLineHandling.Escape or SyslogNewLineHandling.Keep or SyslogNewLineHandling.Split))
{
throw SystemInfo.CreateArgumentOutOfRangeException(nameof(NewLineHandling), NewLineHandling,
$"The NewLineHandling is not {SyslogNewLineHandling.Escape} or {SyslogNewLineHandling.Keep} or {SyslogNewLineHandling.Split}.");
}
_levelMapping.ActivateOptions();
// Start the background pump
_cancellationTokenSource = new();
_pumpTask = Task.Factory.StartNew(() => ProcessQueueAsync(_cancellationTokenSource.Token), CancellationToken.None,
TaskCreationOptions.LongRunning, TaskScheduler.Default);
}
/// <summary>
/// Translates a log4net level to a syslog severity.
/// </summary>
/// <param name="level">A log4net level.</param>
/// <returns>A syslog severity.</returns>
protected virtual SyslogSeverity GetSeverity(Level? level)
{
if (_levelMapping.Lookup(level) is LevelSeverity levelSeverity)
{
return levelSeverity.Severity;
}
//
// Fallback to sensible default values
//
if (level is null)
{
// Default setting
return SyslogSeverity.Debug;
}
if (level >= Level.Alert)
{
return SyslogSeverity.Alert;
}
else if (level >= Level.Critical)
{
return SyslogSeverity.Critical;
}
else if (level >= Level.Error)
{
return SyslogSeverity.Error;
}
else if (level >= Level.Warn)
{
return SyslogSeverity.Warning;
}
else if (level >= Level.Notice)
{
return SyslogSeverity.Notice;
}
else if (level >= Level.Info)
{
return SyslogSeverity.Informational;
}
// Default setting
return SyslogSeverity.Debug;
}
/// <summary>
/// Generate a syslog priority.
/// </summary>
/// <param name="facility">The syslog facility.</param>
/// <param name="severity">The syslog severity.</param>
/// <returns>A syslog priority.</returns>
/// <remarks>
/// <para>
/// Generate a syslog priority.
/// </para>
/// </remarks>
public static int GeneratePriority(SyslogFacility facility, SyslogSeverity severity)
{
if (facility is < SyslogFacility.Kernel or > SyslogFacility.Local7)
{
throw new ArgumentException($"{nameof(SyslogFacility)} out of range", nameof(facility));
}
if (severity is < SyslogSeverity.Emergency or > SyslogSeverity.Debug)
{
throw new ArgumentException($"{nameof(SyslogSeverity)} out of range", nameof(severity));
}
unchecked
{
return ((int)facility * 8) + (int)severity;
}
}
/// <summary>
/// Mapping from level object to syslog severity
/// </summary>
private readonly LevelMapping _levelMapping = new();
/// <summary>
/// A class to act as a mapping between the level that a logging call is made at and
/// the syslog severity that it should be logged at.
/// </summary>
/// <remarks>
/// <para>
/// A class to act as a mapping between the level that a logging call is made at and
/// the syslog severity that it should be logged at.
/// </para>
/// </remarks>
public class LevelSeverity : LevelMappingEntry
{
/// <summary>
/// The mapped syslog severity for the specified level
/// </summary>
/// <remarks>
/// <para>
/// Required property.
/// </para>
/// </remarks>
public SyslogSeverity Severity { get; set; }
}
/// <inheritdoc/>
protected override void OnClose()
{
// Signal shutdown and wait for the pump to drain
_cancellationTokenSource?.Cancel();
_pumpTask?.Wait(TimeSpan.FromSeconds(5));
base.OnClose();
}
private async Task ProcessQueueAsync(CancellationToken token)
{
// We create our own UdpClient here, so that client lifetime is tied to this task
using IUdpConnection udpClient = CreateUdpConnection();
udpClient.Connect(LocalPort, RemoteAddress.EnsureNotNull(), RemotePort);
try
{
while (!token.IsCancellationRequested)
{
// Take next message or throw when canceled
byte[] datagram = _sendQueue.Take(token);
try
{
await udpClient.SendAsync(datagram, datagram.Length).ConfigureAwait(false);
}
catch (Exception ex) when (!ex.IsFatal())
{
ErrorHandler.Error("RemoteSyslogAppender: send failed", ex, ErrorCode.WriteFailure);
}
}
}
catch (OperationCanceledException)
{
// Clean shutdown: drain remaining items if desired
while (_sendQueue.TryTake(out byte[]? leftover))
{
try
{
await udpClient.SendAsync(leftover, leftover.Length).ConfigureAwait(false);
}
catch (Exception ex) when (!ex.IsFatal())
{
ErrorHandler.Error("RemoteSyslogAppender: send failed during shutdown", ex, ErrorCode.FlushFailure);
}
}
}
}
}