Skip to content

Commit

Permalink
Merge pull request #34 from AlexeyRokhin/master
Browse files Browse the repository at this point in the history
Rfc 5424 support added
  • Loading branch information
Jesper Hess Nielsen committed Feb 19, 2016
2 parents bfec403 + e7715de commit c16cadb
Show file tree
Hide file tree
Showing 7 changed files with 229 additions and 90 deletions.
3 changes: 0 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.

# VS 2015 dot files
.vs/

# User-specific files
*.suo
*.user
Expand Down
25 changes: 19 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,29 +40,42 @@ Optionally, your configuration can override them using attributes on
* `port`: Port of syslog listener (default: `514`)
* `protocol`: `udp` or `tcp` (default: `udp`)
* `ssl`: `false` or `true`; TCP only (default: `false`)
* `rfc`: Rfc compatibility for syslog message `Rfc3164` or `Rfc5424` (default: `Rfc3164`)

#### Syslog packet elements

Messages are sent using the format (framing) called syslog, which is
defined in [RFC 3164](http://www.ietf.org/rfc/rfc3164.txt). In addition
defined in [RFC 3164](http://www.ietf.org/rfc/rfc3164.txt) or
[RFC 5424](http://tools.ietf.org/html/rfc5424). In addition
to a timestamp and the log message, RFC 3164 syslog messages include
other elements: sending device name (such as the machine's hostname),
sending app/component name (called "tag" in the RFC), facility, and
severity.

The following syslog elements can be overridden:
The following syslog elements can be overridden for RFC 3164:

* `machinename`: name of sending system or entity (default: machine
[hostname](http://msdn.microsoft.com/en-us/library/system.net.dns.gethostname(v=vs.110).aspx))
* `sender`: name of sending component or application (default:
[calling method](http://msdn.microsoft.com/en-us/library/system.reflection.assembly.getcallingassembly(v=vs.110).aspx))
* `machinename` ([Layout](https://github.com/NLog/NLog/wiki/Layouts)): name of sending system or entity (default: machine
[hostname](http://msdn.microsoft.com/en-us/library/system.net.dns.gethostname(v=vs.110).aspx)).
For example, ${machinename}
* `sender` ([Layout](https://github.com/NLog/NLog/wiki/Layouts)): name of sending component or application (default:
[calling method](http://msdn.microsoft.com/en-us/library/system.reflection.assembly.getcallingassembly(v=vs.110).aspx)).
For example, ${logger}
* `facility`: facility name (default: `Local1`)

For example, to make logs from multiple systems use the same device
identifier (rather than each system's hostname), one could set
`machinename` to `app-cloud`. The logs from different systems would
all appear to be from the same single entity called `app-cloud`.

The following additional syslog elements can be overridden for [RFC 5424](http://tools.ietf.org/html/rfc5424):

* `procid` ([Layout](https://github.com/NLog/NLog/wiki/Layouts)): [identifier](http://tools.ietf.org/html/rfc5424#section-6.2.6) (numeric or alphanumeric) of sending entity
(default: -). For example, ${processid} or ${processname}
* `msgid` ([Layout](https://github.com/NLog/NLog/wiki/Layouts)): [message type identifier](http://tools.ietf.org/html/rfc5424#section-6.2.7) (numeric or alphanumeric) of sending entity
(default: -). For example, ${callsite}
* `structureddata` ([Layout](https://github.com/NLog/NLog/wiki/Layouts)): [additional data](http://tools.ietf.org/html/rfc5424#section-6.3) of sending entity (default: -).
For example, [thread@12345 id="${threadid}" name="${threadname}"][mydata2@12345 num="1" code="mycode"]

#### Log message body

This target supports the standard NLog
Expand Down
258 changes: 178 additions & 80 deletions src/NLog.Targets.Syslog/NLog.Targets.Syslog.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,10 @@
using System.Net.Security;
using System.Net.Sockets;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Text;
using NLog.Common;
using NLog.Layouts;

// ReSharper disable UnusedMember.Global
// ReSharper disable MemberCanBePrivate.Global
Expand All @@ -38,20 +41,25 @@ namespace NLog.Targets
[Target("Syslog")]
public class Syslog : TargetWithLayout
{
private const string NilValue = "-";
private static readonly CultureInfo _usCulture = new CultureInfo("en-US");
private static readonly byte[] _bom = { 0xEF, 0xBB, 0xBF };
private static readonly char[] _lineSeps = { '\r', '\n' };

/// <summary>Gets or sets the IP Address or Host name of your Syslog server</summary>
public string SyslogServer { get; set; }

/// <summary>Gets or sets the port number syslog is running on (usually 514)</summary>
public int Port { get; set; }

/// <summary>Gets or sets the name of the application that will show up in the syslog log</summary>
public string Sender { get; set; }
public Layout Sender { get; set; }

/// <summary>Gets or sets the timestamp format</summary>
public string TimestampFormat { get; set; }

/// <summary>Gets or sets the machine name hosting syslog</summary>
public string MachineName { get; set; }
public Layout MachineName { get; set; }

/// <summary>Gets or sets the syslog facility name to transmit message from (e.g. local0 or local7)</summary>
public SyslogFacility Facility { get; set; }
Expand All @@ -65,6 +73,25 @@ public class Syslog : TargetWithLayout
/// <summary>If set, split message by newlines and send as separate messages</summary>
public bool SplitNewlines { get; set; }

/// <summary>RFC number for syslog protocol</summary>
public RfcNumber Rfc { get; set; }

#region RFC 5424 members

/// <summary>Syslog protocol version for RFC 5424</summary>
private byte ProtocolVersion { get; }

/// <summary>Layout for PROCID protocol field</summary>
public Layout ProcId { get; set; }

/// <summary>Layout for MSGID protocol field</summary>
public Layout MsgId { get; set; }

/// <summary>Layout for STRUCTURED-DATA protocol field</summary>
public Layout StructuredData { get; set; }

#endregion

/// <summary>Initializes a new instance of the Syslog class</summary>
public Syslog()
{
Expand All @@ -76,114 +103,185 @@ public Syslog()
TimestampFormat = "MMM dd HH:mm:ss";
MachineName = Dns.GetHostName();
SplitNewlines = true;
}
Rfc = RfcNumber.Rfc3164;

/// <summary>This is where we hook into NLog, by overriding the Write method</summary>
/// <param name="logEvent">The NLog.LogEventInfo</param>
protected override void Write(LogEventInfo logEvent)
{
FormatMessageLines(logEvent)
.Select(line => BuildSyslogMessage(Facility, (SyslogSeverity)logEvent.Level, DateTime.Now, Sender, line))
.ToList()
.ForEach(message => SendMessage(SyslogServer, Port, message, Protocol, Ssl));
//Defaults for rfc 5424
ProtocolVersion = 1;
ProcId = NilValue;
MsgId = NilValue;
StructuredData = NilValue;
}

/// <summary>Renders message lines</summary>
/// <param name="logEvent">The NLog.LogEventInfo</param>
private IEnumerable<string> FormatMessageLines(LogEventInfo logEvent)
/// <summary>
/// Writes single event.
/// No need to override sync version of Write(LogEventInfo) because it is called only from async version.
/// </summary>
/// <param name="logEvent">The NLog.AsyncLogEventInfo</param>
protected override void Write(AsyncLogEventInfo logEvent)
{
var msg = Layout.Render(logEvent);
return SplitNewlines ? msg.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries) : new[] { msg };
SendEventsBatch(logEvent);
}

/// <summary>Builds a syslog-compatible message using the information we have available</summary>
/// <param name="facility">Syslog facility to transmit message from</param>
/// <param name="severity">Syslog severity level</param>
/// <param name="dateTime">Timestamp for log message</param>
/// <param name="sender">Name of the subsystem sending the message</param>
/// <param name="body">Message text</param>
/// <returns>Byte array containing formatted syslog message</returns>
private byte[] BuildSyslogMessage(SyslogFacility facility, SyslogSeverity severity, DateTime dateTime, string sender, string body)
/// <summary>Writes array of events</summary>
/// <param name="logEvents">The array of NLog.AsyncLogEventInfo</param>
protected override void Write(AsyncLogEventInfo[] logEvents)
{
var prival = CalculatePriorityValue(facility, severity).ToString(CultureInfo.InvariantCulture);
var timestamp = dateTime.ToString(TimestampFormat, CultureInfo.GetCultureInfo("en-US"));

return Encoding.ASCII.GetBytes($"<{prival}>{timestamp} {MachineName} {sender}: {body}{Environment.NewLine}");
SendEventsBatch(logEvents);
}

/// <summary>Calculates syslog PRIVAL</summary>
/// <param name="facility">Syslog facility to transmit message from</param>
/// <param name="severity">Syslog severity level</param>
/// <returns>Byte array containing formatted syslog message</returns>
private static int CalculatePriorityValue(SyslogFacility facility, SyslogSeverity severity)
/// <summary>Sends array of events to syslog server</summary>
/// <param name="logEvents">The array of NLog.AsyncLogEventInfo</param>
private void SendEventsBatch(params AsyncLogEventInfo[] logEvents)
{
return (int)facility * 8 + (int)severity;
}

/// <summary>Performs the actual network part of sending a message</summary>
/// <param name="logServer">The syslog server's host name or IP address</param>
/// <param name="port">The UDP port that syslog is running on</param>
/// <param name="msg">The syslog formatted message ready to transmit</param>
/// <param name="protocol">The syslog server protocol (TCP/UDP)</param>
/// <param name="useSsl">Specify if SSL should be used</param>
private static void SendMessage(string logServer, int port, byte[] msg, ProtocolType protocol, bool useSsl = false)
{
var logServerIp = Dns.GetHostAddresses(logServer).FirstOrDefault();
var logServerIp = Dns.GetHostAddresses(SyslogServer).FirstOrDefault();
if (logServerIp == null)
{
return;

}
var ipAddress = logServerIp.ToString();
switch (protocol)
switch (Protocol)
{
case ProtocolType.Udp:
SendUdpMessage(port, msg, ipAddress);
using (var udp = new UdpClient(ipAddress, Port))
{
ProcessAndSendEvents(logEvents, messageData => udp.Send(messageData, messageData.Length));
}
break;
case ProtocolType.Tcp:
SendTcpMessage(logServer, port, msg, useSsl, ipAddress);
using (var tcp = new TcpClient(ipAddress, Port))
{
// disposition of tcp also disposes stream
var stream = tcp.GetStream();
if (Ssl)
{
// leave stream open so that we don't double dispose
using (var sslStream = new SslStream(stream, true))
{
sslStream.AuthenticateAsClient(SyslogServer);
ProcessAndSendEvents(logEvents, messageData => sslStream.Write(messageData, 0, messageData.Length));
}
}
else
{
ProcessAndSendEvents(logEvents, messageData => stream.Write(messageData, 0, messageData.Length));
}
}
break;
default:
throw new NLogConfigurationException($"Protocol '{protocol}' is not supported");
throw new NLogConfigurationException($"Protocol '{Protocol}' is not supported.");
}
}

/// <summary>Performs the actual network part of sending a message with the UDP protocol</summary>
/// <param name="port">The TCP port that syslog is running on</param>
/// <param name="msg">The syslog formatted message ready to transmit</param>
/// <param name="ipAddress">The syslog server's IP address</param>
private static void SendUdpMessage(int port, byte[] msg, string ipAddress)
/// <summary>Processes array of events and sends messages bytes using action</summary>
/// <param name="logEvents">The array of NLog.AsyncLogEventInfo</param>
/// <param name="messageSendAction">Implementation of send data method</param>
void ProcessAndSendEvents(AsyncLogEventInfo[] logEvents, Action<byte[]> messageSendAction)
{
using (var udp = new UdpClient(ipAddress, port))
foreach (var asyncLogEvent in logEvents)
{
udp.Send(msg, msg.Length);
var logEvent = asyncLogEvent.LogEvent;
var formattedMessageLines = FormatMessageLines(logEvent);
var severity = (SyslogSeverity)logEvent.Level;
foreach (var formattedMessageLine in formattedMessageLines)
{
var message = BuildSyslogMessage(logEvent, Facility, severity, formattedMessageLine);
messageSendAction(message);
}
}
}

/// <summary>Performs the actual network part of sending a message with the TCP protocol</summary>
/// <param name="logServer">The syslog server's host name or IP address</param>
/// <param name="port">The UDP port that syslog is running on</param>
/// <param name="msg">The syslog formatted message ready to transmit</param>
/// <param name="useSsl">Specify if SSL should be used</param>
/// <param name="ipAddress">The syslog server's IP address</param>
private static void SendTcpMessage(string logServer, int port, byte[] msg, bool useSsl, string ipAddress)
/// <summary>Builds a syslog-compatible message using the information we have available</summary>
/// <param name="logEvent">The NLog.LogEventInfo</param>
/// <param name="facility">Syslog Facility to transmit message from</param>
/// <param name="priority">Syslog severity level</param>
/// <param name="body">Message text</param>
/// <returns>Byte array containing formatted syslog message</returns>
private byte[] BuildSyslogMessage(LogEventInfo logEvent, SyslogFacility facility, SyslogSeverity priority, string body)
{
using (var tcp = new TcpClient(ipAddress, port))
switch (Rfc)
{
// Disposition of tcp also disposes stream
var stream = tcp.GetStream();
if (useSsl)
{
// Leave stream open so that we don't double dispose
using (var sslStream = new SslStream(stream, true))
{
sslStream.AuthenticateAsClient(logServer);
sslStream.Write(msg, 0, msg.Length);
}
}
else
{
stream.Write(msg, 0, msg.Length);
}
case RfcNumber.Rfc5424:
return BuildSyslogMessage5424(logEvent, facility, priority, body);
default:
return BuildSyslogMessage3164(logEvent, facility, priority, body);
}
}

/// <summary>Builds rfc-3164 compatible message</summary>
/// <param name="logEvent">The NLog.LogEventInfo</param>
/// <param name="facility">Syslog Facility to transmit message from</param>
/// <param name="severity">Syslog severity level</param>
/// <param name="body">Message text</param>
/// <returns>Byte array containing formatted syslog message</returns>
private byte[] BuildSyslogMessage3164(LogEventInfo logEvent, SyslogFacility facility, SyslogSeverity severity, string body)
{
// Calculate PRI field
var priority = CalculatePriorityValue(facility, severity).ToString(CultureInfo.InvariantCulture);
var time = logEvent.TimeStamp.ToLocalTime().ToString(TimestampFormat, _usCulture);
// Get sender machine name
var machine = MachineName.Render(logEvent);
// Get sender
var sender = Sender.Render(logEvent);

return Encoding.ASCII.GetBytes($"<{priority}>{time} {machine} {sender}: {body}{Environment.NewLine}");
}

/// <summary>Builds rfc-5424 compatible message</summary>
/// <param name="logEvent">The NLog.LogEventInfo</param>
/// <param name="facility">Syslog Facility to transmit message from</param>
/// <param name="severity">Syslog severity level</param>
/// <param name="body">Message text</param>
/// <returns>Byte array containing formatted syslog message</returns>
private byte[] BuildSyslogMessage5424(LogEventInfo logEvent, SyslogFacility facility, SyslogSeverity severity, string body)
{
// Calculate PRI field
var priority = CalculatePriorityValue(facility, severity).ToString(CultureInfo.InvariantCulture);
var version = ProtocolVersion.ToString(CultureInfo.InvariantCulture);
var time = logEvent.TimeStamp.ToString("o");
// Get sender machine name
var machine = Left(MachineName.Render(logEvent), 255);
var sender = Left(Sender.Render(logEvent), 48);
var procId = Left(ProcId.Render(logEvent), 128);
var msgId = Left(MsgId.Render(logEvent), 32);

var headerData = Encoding.ASCII.GetBytes($"<{priority}>{version} {time} {machine} {sender} {procId} {msgId} ");
var structuredData = Encoding.UTF8.GetBytes(StructuredData.Render(logEvent) + " ");
var messageData = Encoding.UTF8.GetBytes(body);

var allData = new List<byte>(headerData.Length + structuredData.Length + _bom.Length + messageData.Length);
allData.AddRange(headerData);
allData.AddRange(structuredData);
allData.AddRange(_bom);
allData.AddRange(messageData);
return allData.ToArray();
}

/// <summary>Gets at most length first symbols</summary>
/// <param name="value">Source string</param>
/// <param name="length">Maximum symbols count</param>
/// <returns>String that contains at most length symbols</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static string Left(string value, int length)
{
return value.Length <= length ? value : value.Substring(0, length);
}

/// <summary>Renders message lines</summary>
/// <param name="logEvent">The NLog.LogEventInfo</param>
private IEnumerable<string> FormatMessageLines(LogEventInfo logEvent)
{
var msg = Layout.Render(logEvent);
return SplitNewlines ? msg.Split(_lineSeps, StringSplitOptions.RemoveEmptyEntries) : new[] { msg };
}

/// <summary>Calculates syslog PRIVAL</summary>
/// <param name="facility">Syslog facility to transmit message from</param>
/// <param name="severity">Syslog severity level</param>
/// <returns>Byte array containing formatted syslog message</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int CalculatePriorityValue(SyslogFacility facility, SyslogSeverity severity)
{
return (int)facility * 8 + (int)severity;
}
}
}
Loading

0 comments on commit c16cadb

Please sign in to comment.