From 1f9ef907e107faa2838abfd810c47ba3a2b5cfb3 Mon Sep 17 00:00:00 2001 From: Alexey Rokhin Date: Mon, 11 Jan 2016 17:42:53 +0300 Subject: [PATCH 1/3] Added support for rfc5424 --- README.md | 25 ++- .../NLog.Targets.Syslog.cs | 143 +++++++++++++++--- .../NLog.Targets.Syslog.csproj | 1 + src/NLog.Targets.Syslog/RfcNumber.cs | 8 + src/TestApp/NLog.config | 19 ++- 5 files changed, 168 insertions(+), 28 deletions(-) create mode 100644 src/NLog.Targets.Syslog/RfcNumber.cs diff --git a/README.md b/README.md index fa10e890..89936ac3 100644 --- a/README.md +++ b/README.md @@ -40,22 +40,26 @@ 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 @@ -63,6 +67,15 @@ 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 diff --git a/src/NLog.Targets.Syslog/NLog.Targets.Syslog.cs b/src/NLog.Targets.Syslog/NLog.Targets.Syslog.cs index 80186f3e..7803818d 100644 --- a/src/NLog.Targets.Syslog/NLog.Targets.Syslog.cs +++ b/src/NLog.Targets.Syslog/NLog.Targets.Syslog.cs @@ -24,19 +24,23 @@ namespace NLog.Targets using System.Linq; using System.Text; using System.Reflection; - using System.Threading; using System.Net; using System.Net.Sockets; using System.Globalization; using System.Net.Security; using System.Collections.Generic; - + using Layouts; /// /// This class enables logging to a unix-style syslog server using NLog. /// [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 }; + + /// /// Gets or sets the IP Address or Host name of your Syslog server /// @@ -50,12 +54,12 @@ public class Syslog : TargetWithLayout /// /// Gets or sets the name of the application that will show up in the syslog log /// - public string Sender { get; set; } + public Layout Sender { get; set; } /// /// Gets or sets the machine name hosting syslog /// - public string MachineName { get; set; } + public Layout MachineName { get; set; } /// /// Gets or sets the syslog facility name to send messages as (for example, local0 or local7) @@ -77,6 +81,35 @@ public class Syslog : TargetWithLayout /// public bool SplitNewlines { get; set; } + /// + /// RFC number for syslog protocol + /// + public RfcNumber Rfc { get; set; } + + #region RFC 5424 members + + /// + /// Syslog protocol version for RFC 5424 + /// + private byte ProtocolVersion { get; set; } + + /// + /// Layout for PROCID protocol field + /// + public Layout ProcId { get; set; } + + /// + /// Layout for MSGID protocol field + /// + public Layout MsgId { get; set; } + + /// + /// Layout for STRUCTURED-DATA protocol field + /// + public Layout StructuredData { get; set; } + + #endregion + /// /// Initializes a new instance of the Syslog class /// @@ -90,6 +123,13 @@ public Syslog() this.Protocol = ProtocolType.Udp; this.MachineName = Dns.GetHostName(); this.SplitNewlines = true; + this.Rfc = RfcNumber.Rfc3164; + + //Defaults for rfc 5424 + this.ProtocolVersion = 1; + this.ProcId = NilValue; + this.MsgId = NilValue; + this.StructuredData = NilValue; } /// @@ -98,21 +138,13 @@ public Syslog() /// The NLog.LogEventInfo protected override void Write(LogEventInfo logEvent) { - // Store the current UI culture - var currentCulture = Thread.CurrentThread.CurrentCulture; - // Set the current Locale to "en-US" for proper date formatting - Thread.CurrentThread.CurrentCulture = new CultureInfo("en-US"); - var formattedMessageLines = this.GetFormattedMessageLines(logEvent); var severity = GetSyslogSeverity(logEvent.Level); foreach (var formattedMessageLine in formattedMessageLines) { - var message = this.BuildSyslogMessage(this.Facility, severity, DateTime.Now, this.Sender, formattedMessageLine); + var message = this.BuildSyslogMessage(logEvent, this.Facility, severity, formattedMessageLine); SendMessage(this.SyslogServer, this.Port, message, this.Protocol, this.Ssl); } - - // Restore the original culture - Thread.CurrentThread.CurrentCulture = currentCulture; } private IEnumerable GetFormattedMessageLines(LogEventInfo logEvent) @@ -210,26 +242,95 @@ private static SyslogSeverity GetSyslogSeverity(LogLevel logLevel) /// /// Builds a syslog-compatible message using the information we have available. /// + /// The NLog.LogEventInfo /// Syslog Facility to transmit message from /// Syslog severity level - /// Time stamp for log message - /// Name of the subsystem sending the message /// Message text /// Byte array containing formatted syslog message - private byte[] BuildSyslogMessage(SyslogFacility facility, SyslogSeverity priority, DateTime time, string sender, string body) + private byte[] BuildSyslogMessage(LogEventInfo logEvent, SyslogFacility facility, SyslogSeverity priority, string body) { - // Get sender machine name - var machine = this.MachineName + " "; + switch (Rfc) + { + case RfcNumber.Rfc5424: + return this.BuildSyslogMessage5424(logEvent, facility, priority, body); + default: + return this.BuildSyslogMessage3164(logEvent, facility, priority, body); + } + } + /// + /// Builds rfc-3164 compatible message + /// + /// The NLog.LogEventInfo + /// Syslog Facility to transmit message from/param> + /// Syslog severity level + /// Message text/param> + /// Byte array containing formatted syslog message + private byte[] BuildSyslogMessage3164(LogEventInfo logEvent, SyslogFacility facility, SyslogSeverity priority, string body) + { // Calculate PRI field var calculatedPriority = (int)facility * 8 + (int)priority; var pri = "<" + calculatedPriority.ToString(CultureInfo.InvariantCulture) + ">"; - var timeToString = time.ToString("MMM dd HH:mm:ss "); - sender = sender + ": "; + var time = logEvent.TimeStamp.ToLocalTime().ToString("MMM dd HH:mm:ss ", _usCulture); - string[] strParams = { pri, timeToString, machine, sender, body, Environment.NewLine }; + // Get sender machine name + var machine = this.MachineName.Render(logEvent) + " "; + + var sender = this.Sender.Render(logEvent) + ": "; + + string[] strParams = { pri, time, machine, sender, body, Environment.NewLine }; return Encoding.ASCII.GetBytes(string.Concat(strParams)); } + + /// + /// Builds rfc-5424 compatible message + /// + /// The NLog.LogEventInfo + /// Syslog Facility to transmit message from/param> + /// Syslog severity level + /// Message text/param> + /// Byte array containing formatted syslog message + private byte[] BuildSyslogMessage5424(LogEventInfo logEvent, SyslogFacility facility, SyslogSeverity priority, string body) + { + // Calculate PRI field + var calculatedPriority = (int)facility * 8 + (int)priority; + var pri = "<" + calculatedPriority.ToString(CultureInfo.InvariantCulture) + ">"; + var version = this.ProtocolVersion.ToString(CultureInfo.InvariantCulture); + var time = logEvent.TimeStamp.ToString("o"); + // Get sender machine name + var machine = this.MachineName.Render(logEvent); + if (machine.Length > 255) + { + machine = machine.Substring(0, 255); + } + var sender = this.Sender.Render(logEvent); + if (sender.Length > 48) + { + sender = sender.Substring(0, 48); + } + var procId = this.ProcId.Render(logEvent); + if (procId.Length > 128) + { + procId = procId.Substring(0, 128); + } + var msgId = this.MsgId.Render(logEvent); + if (msgId.Length > 32) + { + msgId = msgId.Substring(0, 32); + } + + var headerData = Encoding.ASCII.GetBytes(string.Concat(pri, version, " ", time, " ", machine, " ", sender, " ", procId, " ", msgId, " ")); + var structuredData = Encoding.UTF8.GetBytes(this.StructuredData.Render(logEvent) + " "); + var messageData = Encoding.UTF8.GetBytes(body); + + var allData = new List(); + allData.AddRange(headerData); + allData.AddRange(structuredData); + allData.AddRange(_bom); + allData.AddRange(messageData); + + return allData.ToArray(); + } } } diff --git a/src/NLog.Targets.Syslog/NLog.Targets.Syslog.csproj b/src/NLog.Targets.Syslog/NLog.Targets.Syslog.csproj index 0432e8ab..14e7640f 100644 --- a/src/NLog.Targets.Syslog/NLog.Targets.Syslog.csproj +++ b/src/NLog.Targets.Syslog/NLog.Targets.Syslog.csproj @@ -52,6 +52,7 @@ + diff --git a/src/NLog.Targets.Syslog/RfcNumber.cs b/src/NLog.Targets.Syslog/RfcNumber.cs new file mode 100644 index 00000000..8da05745 --- /dev/null +++ b/src/NLog.Targets.Syslog/RfcNumber.cs @@ -0,0 +1,8 @@ +namespace NLog.Targets +{ + public enum RfcNumber + { + Rfc3164 = 3164, + Rfc5424 = 5424 + } +} diff --git a/src/TestApp/NLog.config b/src/TestApp/NLog.config index 7db1c772..06a626eb 100644 --- a/src/TestApp/NLog.config +++ b/src/TestApp/NLog.config @@ -10,6 +10,8 @@ + + + + - + + From 946dd0442095c2cf27d107e895a8d50f698c9c40 Mon Sep 17 00:00:00 2001 From: Alexey Rokhin Date: Tue, 12 Jan 2016 11:59:05 +0300 Subject: [PATCH 2/3] Added events batch sending. --- .../NLog.Targets.Syslog.cs | 87 ++++++++++++------- src/TestApp/NLog.config | 33 +++---- 2 files changed, 73 insertions(+), 47 deletions(-) diff --git a/src/NLog.Targets.Syslog/NLog.Targets.Syslog.cs b/src/NLog.Targets.Syslog/NLog.Targets.Syslog.cs index 7803818d..63f64a65 100644 --- a/src/NLog.Targets.Syslog/NLog.Targets.Syslog.cs +++ b/src/NLog.Targets.Syslog/NLog.Targets.Syslog.cs @@ -30,6 +30,8 @@ namespace NLog.Targets using System.Net.Security; using System.Collections.Generic; using Layouts; + using Common; + /// /// This class enables logging to a unix-style syslog server using NLog. /// @@ -133,77 +135,98 @@ public Syslog() } /// - /// This is where we hook into NLog, by overriding the Write method. + /// Writes single event. + /// No need to override sync version of Write(LogEventInfo) because it is called only from async version. /// - /// The NLog.LogEventInfo - protected override void Write(LogEventInfo logEvent) + /// The NLog.AsyncLogEventInfo + protected override void Write(AsyncLogEventInfo logEvent) { - var formattedMessageLines = this.GetFormattedMessageLines(logEvent); - var severity = GetSyslogSeverity(logEvent.Level); - foreach (var formattedMessageLine in formattedMessageLines) - { - var message = this.BuildSyslogMessage(logEvent, this.Facility, severity, formattedMessageLine); - SendMessage(this.SyslogServer, this.Port, message, this.Protocol, this.Ssl); - } + SendEventsBatch(new[] { logEvent }); } - private IEnumerable GetFormattedMessageLines(LogEventInfo logEvent) + /// + /// Writes array of events + /// + /// The array of NLog.AsyncLogEventInfo + protected override void Write(AsyncLogEventInfo[] logEvents) { - var msg = this.Layout.Render(logEvent); - return this.SplitNewlines ? msg.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries) : new[] { msg }; + var sw = System.Diagnostics.Stopwatch.StartNew(); + SendEventsBatch(logEvents); + sw.Stop(); + System.Diagnostics.Debug.WriteLine("Elapsed {0} ms", sw.Elapsed.TotalMilliseconds); } /// - /// Performs the actual network part of sending a message + /// Sends array of events to syslog server /// - /// The syslog server's host name or IP address - /// The UDP port that syslog is running on - /// The syslog formatted message ready to transmit - /// The syslog server protocol (tcp/udp) - /// Specify if SSL should be used - private static void SendMessage(string logServer, int port, byte[] msg, ProtocolType protocol, bool useSsl = false) + /// The array of NLog.AsyncLogEventInfo + private void SendEventsBatch(AsyncLogEventInfo[] logEvents) { - 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: - using (var udp = new UdpClient(ipAddress, port)) + using (var udp = new UdpClient(ipAddress, Port)) { - udp.Send(msg, msg.Length); + ProcessAndSendEvents(logEvents, messageData => udp.Send(messageData, messageData.Length)); } break; case ProtocolType.Tcp: - using (var tcp = new TcpClient(ipAddress, port)) + using (var tcp = new TcpClient(ipAddress, Port)) { // disposition of tcp also disposes stream var stream = tcp.GetStream(); - if (useSsl) + if (Ssl) { // 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); + sslStream.AuthenticateAsClient(SyslogServer); + ProcessAndSendEvents(logEvents, messageData => sslStream.Write(messageData, 0, messageData.Length)); } } else { - stream.Write(msg, 0, msg.Length); + 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."); } } + /// + /// Processes array of events and sends messages bytes using + /// + /// The array of NLog.AsyncLogEventInfo + /// Implementation of send data method + void ProcessAndSendEvents(AsyncLogEventInfo[] logEvents, Action messageSendAction) + { + foreach (var asyncLogEvent in logEvents) + { + var logEvent = asyncLogEvent.LogEvent; + var formattedMessageLines = this.GetFormattedMessageLines(logEvent); + var severity = GetSyslogSeverity(logEvent.Level); + foreach (var formattedMessageLine in formattedMessageLines) + { + var message = this.BuildSyslogMessage(logEvent, this.Facility, severity, formattedMessageLine); + messageSendAction(message); + } + } + } + + private IEnumerable GetFormattedMessageLines(LogEventInfo logEvent) + { + var msg = this.Layout.Render(logEvent); + return this.SplitNewlines ? msg.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries) : new[] { msg }; + } + /// /// Mapping between NLog levels and syslog severity levels as they are not exactly one to one. /// diff --git a/src/TestApp/NLog.config b/src/TestApp/NLog.config index 06a626eb..977aa99c 100644 --- a/src/TestApp/NLog.config +++ b/src/TestApp/NLog.config @@ -21,25 +21,28 @@ facility="Local7" sender="MyProgram" layout="[CustomPrefix] ${machinename} ${message} ${callsite} ${exception:format=ToString,StackTrace}" /> - - + + + + - + + From e7715dec2baf30a53a28b251ae1cb04d9624c8e4 Mon Sep 17 00:00:00 2001 From: Alexey Rokhin Date: Fri, 19 Feb 2016 01:04:47 +0300 Subject: [PATCH 3/3] ReSharper disable CheckNamespace --- src/NLog.Targets.Syslog/RfcNumber.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/NLog.Targets.Syslog/RfcNumber.cs b/src/NLog.Targets.Syslog/RfcNumber.cs index 8da05745..400302ca 100644 --- a/src/NLog.Targets.Syslog/RfcNumber.cs +++ b/src/NLog.Targets.Syslog/RfcNumber.cs @@ -1,4 +1,6 @@ -namespace NLog.Targets +// ReSharper disable CheckNamespace +namespace NLog.Targets +// ReSharper restore CheckNamespace { public enum RfcNumber {