diff --git a/src/Serilog.Sinks.Syslog/Sinks/Formatters/Rfc5424Formatter.cs b/src/Serilog.Sinks.Syslog/Sinks/Formatters/Rfc5424Formatter.cs index 921cf17..79d9826 100644 --- a/src/Serilog.Sinks.Syslog/Sinks/Formatters/Rfc5424Formatter.cs +++ b/src/Serilog.Sinks.Syslog/Sinks/Formatters/Rfc5424Formatter.cs @@ -40,6 +40,7 @@ public class Rfc5424Formatter : SyslogFormatterBase private readonly string applicationName; private readonly string messageIdPropertyName; + private readonly string structuredDataId; internal const string DefaultMessageIdPropertyName = "SourceContext"; @@ -53,11 +54,13 @@ public class Rfc5424Formatter : SyslogFormatterBase /// Where the Id number of the message will be derived from. Defaults to the "SourceContext" property of the syslog event. Property name and value must be all printable ASCII characters with max length of 32. /// /// + /// Structured Data ID name (SD-ID) public Rfc5424Formatter(Facility facility = Facility.Local0, string applicationName = null, ITextFormatter templateFormatter = null, string messageIdPropertyName = DefaultMessageIdPropertyName, string sourceHost = null, - Func severityMapping = null) + Func severityMapping = null, + string structuredDataId = null) : base(facility, templateFormatter, sourceHost, severityMapping) { this.applicationName = applicationName ?? ProcessName; @@ -71,6 +74,10 @@ public Rfc5424Formatter(Facility facility = Facility.Local0, string applicationN this.messageIdPropertyName = (messageIdPropertyName ?? DefaultMessageIdPropertyName) .AsPrintableAscii() .WithMaxLength(32); + + this.structuredDataId = (structuredDataId ?? STRUCTURED_DATA_ID) + .AsPrintableAscii() + .WithMaxLength(32); } // NOTE: For the rsyslog daemon to correctly handle RFC5424, you need to change your /etc/rsyslog.conf to use: @@ -113,13 +120,13 @@ private string GetMessageId(LogEvent logEvent) : NILVALUE; } - private static string RenderStructuredData(LogEvent logEvent) + private string RenderStructuredData(LogEvent logEvent) { var properties = logEvent.Properties.Select(kvp => new KeyValuePair(RenderPropertyKey(kvp.Key), RenderPropertyValue(kvp.Value))); var structuredDataKvps = String.Join(" ", properties.Select(t => $@"{t.Key}=""{t.Value}""")); - var structuredData = String.IsNullOrEmpty(structuredDataKvps) ? NILVALUE : $"[{STRUCTURED_DATA_ID} {structuredDataKvps}]"; + var structuredData = String.IsNullOrEmpty(structuredDataKvps) ? NILVALUE : $"[{this.structuredDataId} {structuredDataKvps}]"; return structuredData; } diff --git a/src/Serilog.Sinks.Syslog/SyslogLoggerConfigurationExtensions.cs b/src/Serilog.Sinks.Syslog/SyslogLoggerConfigurationExtensions.cs index d27bdb6..bda4e69 100644 --- a/src/Serilog.Sinks.Syslog/SyslogLoggerConfigurationExtensions.cs +++ b/src/Serilog.Sinks.Syslog/SyslogLoggerConfigurationExtensions.cs @@ -54,12 +54,13 @@ public static class SyslogLoggerConfigurationExtensions /// The message formatter /// A switch allowing the pass-through minimum level /// to be changed at runtime. + /// The structured data ID (SD-ID). Only applicable when is . Defaults to "meta". /// public static LoggerConfiguration LocalSyslog(this LoggerSinkConfiguration loggerSinkConfig, string appName = null, Facility facility = Facility.Local0, string outputTemplate = null, LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum, Func severityMapping = null,ITextFormatter formatter = null, - LoggingLevelSwitch levelSwitch = null) + LoggingLevelSwitch levelSwitch = null, string structuredDataId = null) { if (!LocalSyslogService.IsAvailable) { @@ -68,7 +69,7 @@ public static LoggerConfiguration LocalSyslog(this LoggerSinkConfiguration logge } var messageFormatter = GetFormatter(SyslogFormat.Local, appName, facility, outputTemplate, - severityMapping: severityMapping, formatter: formatter); + severityMapping: severityMapping, formatter: formatter, structuredDataId: structuredDataId); var syslogService = new LocalSyslogService(facility, appName); syslogService.Open(); @@ -96,6 +97,7 @@ public static LoggerConfiguration LocalSyslog(this LoggerSinkConfiguration logge /// The message formatter /// A switch allowing the pass-through minimum level /// to be changed at runtime. + /// The structured data ID (SD-ID). Only applicable when is . Defaults to "meta". /// public static LoggerConfiguration UdpSyslog(this LoggerSinkConfiguration loggerSinkConfig, string host, int port = 514, string appName = null, SyslogFormat format = SyslogFormat.RFC3164, @@ -104,13 +106,14 @@ public static LoggerConfiguration UdpSyslog(this LoggerSinkConfiguration loggerS string messageIdPropertyName = Rfc5424Formatter.DefaultMessageIdPropertyName, string sourceHost = null, Func severityMapping = null, ITextFormatter formatter = null, - LoggingLevelSwitch levelSwitch = null) + LoggingLevelSwitch levelSwitch = null, + string structuredDataId = null) { if (String.IsNullOrWhiteSpace(host)) throw new ArgumentException(nameof(host)); batchConfig ??= DefaultBatchOptions; - var messageFormatter = GetFormatter(format, appName, facility, outputTemplate, messageIdPropertyName, sourceHost, severityMapping, formatter); + var messageFormatter = GetFormatter(format, appName, facility, outputTemplate, messageIdPropertyName, sourceHost, severityMapping, formatter, structuredDataId); var endpoint = ResolveIP(host, port); var syslogUdpSink = new SyslogUdpSink(endpoint, messageFormatter); @@ -172,6 +175,7 @@ public static LoggerConfiguration TcpSyslog(this LoggerSinkConfiguration loggerS /// The message formatter /// A switch allowing the pass-through minimum level /// to be changed at runtime. + /// The structured data ID (SD-ID). Only applicable when is . Defaults to "meta". /// public static LoggerConfiguration TcpSyslog(this LoggerSinkConfiguration loggerSinkConfig, string host, int port = 1468, string appName = null, FramingType framingType = FramingType.OCTET_COUNTING, @@ -184,10 +188,11 @@ public static LoggerConfiguration TcpSyslog(this LoggerSinkConfiguration loggerS PeriodicBatchingSinkOptions batchConfig = null, string sourceHost = null, Func severityMapping = null, ITextFormatter formatter = null, - LoggingLevelSwitch levelSwitch = null) + LoggingLevelSwitch levelSwitch = null, + string structuredDataId = null) { var messageFormatter = GetFormatter(format, appName, facility, outputTemplate, messageIdPropertyName, - sourceHost, severityMapping, formatter); + sourceHost, severityMapping, formatter, structuredDataId); var config = new SyslogTcpConfig { @@ -228,7 +233,9 @@ private static ISyslogFormatter GetFormatter(SyslogFormat format, string appName string outputTemplate, string messageIdPropertyName = null, string sourceHost = null, - Func severityMapping = null, ITextFormatter formatter = null) + Func severityMapping = null, + ITextFormatter formatter = null, + string structuredDataId = null) { ITextFormatter templateFormatter; @@ -246,7 +253,7 @@ private static ISyslogFormatter GetFormatter(SyslogFormat format, string appName return format switch { SyslogFormat.RFC3164 => new Rfc3164Formatter(facility, appName, templateFormatter, sourceHost, severityMapping), - SyslogFormat.RFC5424 => new Rfc5424Formatter(facility, appName, templateFormatter, messageIdPropertyName, sourceHost, severityMapping), + SyslogFormat.RFC5424 => new Rfc5424Formatter(facility, appName, templateFormatter, messageIdPropertyName, sourceHost, severityMapping, structuredDataId), SyslogFormat.Local => new LocalFormatter(facility, templateFormatter, severityMapping), _ => throw new ArgumentException($"Invalid format: {format}") }; diff --git a/test/Serilog.Sinks.Syslog.Tests/Formatters/SyslogRfc5424FormatterTests.cs b/test/Serilog.Sinks.Syslog.Tests/Formatters/SyslogRfc5424FormatterTests.cs index 03a5ed6..1385a0e 100644 --- a/test/Serilog.Sinks.Syslog.Tests/Formatters/SyslogRfc5424FormatterTests.cs +++ b/test/Serilog.Sinks.Syslog.Tests/Formatters/SyslogRfc5424FormatterTests.cs @@ -98,6 +98,7 @@ public void Should_format_message_with_structured_data() match.Groups["proc"].Value.ToInt().ShouldBeGreaterThan(0); match.Groups["msgid"].Value.ShouldBe(SOURCE_CONTEXT); match.Groups["sd"].Value.ShouldNotBe(NILVALUE); + match.Groups["sdid"].Value.ShouldBe("meta"); match.Groups["msg"].Value.ShouldBe($"This is a test message with val \"{testVal}\""); } @@ -193,5 +194,28 @@ public void Should_override_log_host_name() match.Groups["host"].Value.ShouldBe(hostname); } + + [Fact] + public void Should_format_message_with_custom_sdid() + { + const string sdId = "ourSDID@32473"; + var customFormatter = new Rfc5424Formatter(Facility.User, APP_NAME, structuredDataId: sdId); + + var properties = new List + { + new LogEventProperty("MyProperty", new ScalarValue("Lorem Ipsum")), + }; + + var template = new MessageTemplateParser().Parse("This is a test message with val {MyProperty}"); + var warnEvent = new LogEvent(this.timestamp, LogEventLevel.Warning, null, template, properties); + + var formatted = customFormatter.FormatMessage(warnEvent); + this.output.WriteLine($"RFC5424 with structured data: {formatted}"); + + var match = this.regex.Match(formatted); + match.Success.ShouldBeTrue(); + + match.Groups["sdid"].Value.ShouldBe(sdId); + } } }