diff --git a/internal/generator/vector/output/syslog/rfc3164_with_defaults.toml b/internal/generator/vector/output/syslog/rfc3164_with_defaults.toml index a05afc1f6..958d5b4c6 100644 --- a/internal/generator/vector/output/syslog/rfc3164_with_defaults.toml +++ b/internal/generator/vector/output/syslog/rfc3164_with_defaults.toml @@ -23,11 +23,11 @@ if .log_source == "container" { } if .log_type == "audit" { ._syslog.app_name = .log_source - ._syslog.severity = "informational" + ._syslog.severity = "info" ._syslog.facility = "security" } -._syslog.proc_id = to_string!(._syslog.proc_id || "") -if exists(._syslog.proc_id) && is_empty(strip_whitespace(string!(._syslog.proc_id))) { del(._syslog.proc_id) } +._syslog.proc_id = to_string(._syslog.proc_id) +if is_empty(strip_whitespace(._syslog.proc_id)) { del(._syslog.proc_id) } ._syslog.app_name = to_string!(._syslog.app_name || "") # try to convert syslog code to the facility, severity names (e.g. 4 -> "warning", "4" -> "warning" ) @@ -51,6 +51,11 @@ if exists(._syslog.facility) && !is_null(._syslog.facility) { } if exists(._syslog.severity) && !is_null(._syslog.severity) { + if is_string(._syslog.severity) { + ._syslog.severity = downcase(._syslog.severity) ?? ._syslog.severity + if ._syslog.severity == "emergency" { ._syslog.severity = "emerg" } else if ._syslog.severity == "critical" { ._syslog.severity = "crit" } else if ._syslog.severity == "informational" { ._syslog.severity = "info" } + } + _, err = to_syslog_severity(._syslog.severity) if err != null { # Field is not a valid name — try treating it as a code (int or string int) diff --git a/internal/generator/vector/output/syslog/syslog.go b/internal/generator/vector/output/syslog/syslog.go index 2bb81f50d..882863c54 100644 --- a/internal/generator/vector/output/syslog/syslog.go +++ b/internal/generator/vector/output/syslog/syslog.go @@ -29,8 +29,8 @@ const ( vrlKeySyslogAppName = "._syslog.app_name" vrlKeySyslogMsgID = "._syslog.msg_id" - defProcIdRFC3164 = `to_string!(._syslog.proc_id || "") -if exists(._syslog.proc_id) && is_empty(strip_whitespace(string!(._syslog.proc_id))) { del(._syslog.proc_id) } + defProcIdRFC3164 = `to_string(._syslog.proc_id) +if is_empty(strip_whitespace(._syslog.proc_id)) { del(._syslog.proc_id) } ` defAppNameRFC3164 = `to_string!(._syslog.app_name || "")` @@ -63,7 +63,7 @@ if err != null { // Default values for Syslog fields for audit logType auditAppName = `._syslog.app_name = .log_source` - auditSeverity = `._syslog.severity = "informational"` + auditSeverity = `._syslog.severity = "info"` auditFacility = `._syslog.facility = "security"` auditProcId = `._syslog.proc_id = to_string!(.auditID || "-")` @@ -74,7 +74,7 @@ if err != null { isContainerLogCond = `.log_source == "container"` isAuditLogCond = `.log_type == "audit"` - facilitySeverityConversionVRL = ` + facilityConversionVRL = ` # try to convert syslog code to the facility, severity names (e.g. 4 -> "warning", "4" -> "warning" ) if exists(._syslog.facility) && !is_null(._syslog.facility) { _, err = to_syslog_facility_code(._syslog.facility) @@ -93,9 +93,23 @@ if exists(._syslog.facility) && !is_null(._syslog.facility) { } } # else: already a valid name, leave it as-is -} +}` + + // severityNormalizationInfallibleVRL is used when VRL already knows ._syslog.severity is a string (dynamic severity with field references). + severityNormalizationInfallibleVRL = ` + ._syslog.severity = downcase(._syslog.severity) + if ._syslog.severity == "emergency" { ._syslog.severity = "emerg" } else if ._syslog.severity == "critical" { ._syslog.severity = "crit" } else if ._syslog.severity == "informational" { ._syslog.severity = "info" }` + // severityNormalizationFallibleVRL is used when ._syslog.severity type is unknown (empty/default severity). + severityNormalizationFallibleVRL = ` + if is_string(._syslog.severity) { + ._syslog.severity = downcase(._syslog.severity) ?? ._syslog.severity + if ._syslog.severity == "emergency" { ._syslog.severity = "emerg" } else if ._syslog.severity == "critical" { ._syslog.severity = "crit" } else if ._syslog.severity == "informational" { ._syslog.severity = "info" } + }` + + severityValidationVRL = ` if exists(._syslog.severity) && !is_null(._syslog.severity) { +%s _, err = to_syslog_severity(._syslog.severity) if err != null { # Field is not a valid name — try treating it as a code (int or string int) @@ -181,6 +195,20 @@ func New(id string, o *adapters.Output, inputs []string, secrets observability.S return id, sink, tfs } +func normalizeSeverity(s string) string { + lower := strings.ToLower(s) + switch lower { + case "emergency": + return "emerg" + case "critical": + return "crit" + case "informational": + return "info" + default: + return lower + } +} + func socketMode(urlScheme string) sinks.SocketMode { switch strings.ToLower(urlScheme) { case TCP, TLS: @@ -225,7 +253,13 @@ func parseEncoding(inputs []string, o *obs.Syslog) types.Transform { } appendField(vrlKeySyslogFacility, o.Facility, "") - appendField(vrlKeySyslogSeverity, o.Severity, "") + + severity := o.Severity + isDynamicSeverity := strings.Contains(severity, "{") + if !isDynamicSeverity && severity != "" { + severity = normalizeSeverity(severity) + } + appendField(vrlKeySyslogSeverity, severity, "") if o.RFC == obs.SyslogRFC3164 { appendField(vrlKeySyslogProcID, o.ProcId, defProcIdRFC3164) @@ -236,7 +270,15 @@ func parseEncoding(inputs []string, o *obs.Syslog) types.Transform { appendField(vrlKeySyslogMsgID, o.MsgId, "") } - vrls = append(vrls, facilitySeverityConversionVRL) + vrls = append(vrls, facilityConversionVRL) + + normVRL := "" + if isDynamicSeverity { + normVRL = severityNormalizationInfallibleVRL + } else if severity == "" { + normVRL = severityNormalizationFallibleVRL + } + vrls = append(vrls, fmt.Sprintf(severityValidationVRL, normVRL)) if key := PayloadKey(o.PayloadKey); key != "" { vrls = append(vrls, fmt.Sprintf(payloadKeyConfiguredVRL, key)) diff --git a/internal/generator/vector/output/syslog/tcp_with_defaults.toml b/internal/generator/vector/output/syslog/tcp_with_defaults.toml index 9eba0839b..e1c078131 100644 --- a/internal/generator/vector/output/syslog/tcp_with_defaults.toml +++ b/internal/generator/vector/output/syslog/tcp_with_defaults.toml @@ -22,7 +22,7 @@ if .log_source == "container" { if .log_type == "audit" { ._syslog.app_name = .log_source ._syslog.proc_id = to_string!(.auditID || "-") - ._syslog.severity = "informational" + ._syslog.severity = "info" ._syslog.facility = "security" } @@ -47,6 +47,11 @@ if exists(._syslog.facility) && !is_null(._syslog.facility) { } if exists(._syslog.severity) && !is_null(._syslog.severity) { + if is_string(._syslog.severity) { + ._syslog.severity = downcase(._syslog.severity) ?? ._syslog.severity + if ._syslog.severity == "emergency" { ._syslog.severity = "emerg" } else if ._syslog.severity == "critical" { ._syslog.severity = "crit" } else if ._syslog.severity == "informational" { ._syslog.severity = "info" } + } + _, err = to_syslog_severity(._syslog.severity) if err != null { # Field is not a valid name — try treating it as a code (int or string int) diff --git a/internal/generator/vector/output/syslog/tcp_with_kubernetes_minimal_enrichment.toml b/internal/generator/vector/output/syslog/tcp_with_kubernetes_minimal_enrichment.toml index 1be85ea5c..f93201287 100644 --- a/internal/generator/vector/output/syslog/tcp_with_kubernetes_minimal_enrichment.toml +++ b/internal/generator/vector/output/syslog/tcp_with_kubernetes_minimal_enrichment.toml @@ -22,7 +22,7 @@ if .log_source == "container" { if .log_type == "audit" { ._syslog.app_name = .log_source ._syslog.proc_id = to_string!(.auditID || "-") - ._syslog.severity = "informational" + ._syslog.severity = "info" ._syslog.facility = "security" } @@ -47,6 +47,11 @@ if exists(._syslog.facility) && !is_null(._syslog.facility) { } if exists(._syslog.severity) && !is_null(._syslog.severity) { + if is_string(._syslog.severity) { + ._syslog.severity = downcase(._syslog.severity) ?? ._syslog.severity + if ._syslog.severity == "emergency" { ._syslog.severity = "emerg" } else if ._syslog.severity == "critical" { ._syslog.severity = "crit" } else if ._syslog.severity == "informational" { ._syslog.severity = "info" } + } + _, err = to_syslog_severity(._syslog.severity) if err != null { # Field is not a valid name — try treating it as a code (int or string int) diff --git a/internal/generator/vector/output/syslog/tcp_with_tuning.toml b/internal/generator/vector/output/syslog/tcp_with_tuning.toml index c25dcc4a5..60c5dc30b 100644 --- a/internal/generator/vector/output/syslog/tcp_with_tuning.toml +++ b/internal/generator/vector/output/syslog/tcp_with_tuning.toml @@ -23,7 +23,7 @@ if .log_source == "container" { if .log_type == "audit" { ._syslog.app_name = .log_source ._syslog.proc_id = to_string!(.auditID || "-") - ._syslog.severity = "informational" + ._syslog.severity = "info" ._syslog.facility = "security" } @@ -48,6 +48,11 @@ if exists(._syslog.facility) && !is_null(._syslog.facility) { } if exists(._syslog.severity) && !is_null(._syslog.severity) { + if is_string(._syslog.severity) { + ._syslog.severity = downcase(._syslog.severity) ?? ._syslog.severity + if ._syslog.severity == "emergency" { ._syslog.severity = "emerg" } else if ._syslog.severity == "critical" { ._syslog.severity = "crit" } else if ._syslog.severity == "informational" { ._syslog.severity = "info" } + } + _, err = to_syslog_severity(._syslog.severity) if err != null { # Field is not a valid name — try treating it as a code (int or string int) diff --git a/internal/generator/vector/output/syslog/tls_with_field_references.toml b/internal/generator/vector/output/syslog/tls_with_field_references.toml index 005996599..6951fa125 100644 --- a/internal/generator/vector/output/syslog/tls_with_field_references.toml +++ b/internal/generator/vector/output/syslog/tls_with_field_references.toml @@ -31,6 +31,9 @@ if exists(._syslog.facility) && !is_null(._syslog.facility) { } if exists(._syslog.severity) && !is_null(._syslog.severity) { + ._syslog.severity = downcase(._syslog.severity) + if ._syslog.severity == "emergency" { ._syslog.severity = "emerg" } else if ._syslog.severity == "critical" { ._syslog.severity = "crit" } else if ._syslog.severity == "informational" { ._syslog.severity = "info" } + _, err = to_syslog_severity(._syslog.severity) if err != null { # Field is not a valid name — try treating it as a code (int or string int) diff --git a/internal/generator/vector/output/syslog/udp_with_every_setting.toml b/internal/generator/vector/output/syslog/udp_with_every_setting.toml index e9071a1ab..04953374e 100644 --- a/internal/generator/vector/output/syslog/udp_with_every_setting.toml +++ b/internal/generator/vector/output/syslog/udp_with_every_setting.toml @@ -5,7 +5,7 @@ source = ''' ._syslog = {} ._syslog.facility = "kern" -._syslog.severity = "critical" +._syslog.severity = "crit" ._syslog.proc_id = "procID" ._syslog.app_name = "appName" @@ -30,6 +30,7 @@ if exists(._syslog.facility) && !is_null(._syslog.facility) { } if exists(._syslog.severity) && !is_null(._syslog.severity) { + _, err = to_syslog_severity(._syslog.severity) if err != null { # Field is not a valid name — try treating it as a code (int or string int) diff --git a/test/functional/outputs/syslog/rfc3164_test.go b/test/functional/outputs/syslog/rfc3164_test.go index 092368ece..fd80d0343 100644 --- a/test/functional/outputs/syslog/rfc3164_test.go +++ b/test/functional/outputs/syslog/rfc3164_test.go @@ -53,9 +53,48 @@ var _ = Describe("[Functional][Outputs][Syslog] RFC3164 tests", func() { // 134 = Facility(local0/16)*8 + Severity(Informational/6) expectedPriority := "<134>" Expect(outputlogs[0]).To(MatchRegexp(expectedPriority), "Exp to find tag in received message") + + collectorLogs, err := framework.ReadCollectorLogs() + Expect(err).To(BeNil()) + Expect(collectorLogs).ToNot(ContainSubstring("VRL compilation warning"), "Expected no VRL compilation warnings in collector logs") }) }) + DescribeTable("should correctly encode severity keywords in all forms", func(severity string, expectedPriority string) { + obstestruntime.NewClusterLogForwarderBuilder(framework.Forwarder). + FromInput(obs.InputTypeApplication). + ToSyslogOutput(obs.SyslogRFC3164, func(spec *obs.OutputSpec) { + spec.Syslog.Facility = "user" + spec.Syslog.Severity = severity + }) + Expect(framework.Deploy()).To(BeNil()) + + crioMessage := functional.NewFullCRIOLogMessage(functional.CRIOTime(time.Now()), `{"index":1}`) + Expect(framework.WriteMessagesToApplicationLog(crioMessage, 1)).To(BeNil()) + + outputlogs, err := framework.ReadRawApplicationLogsFrom(string(obs.OutputTypeSyslog)) + Expect(err).To(BeNil(), "Expected no errors reading the logs") + Expect(outputlogs).To(HaveLen(1), "Expected the receiver to receive the message") + Expect(outputlogs[0]).To(HavePrefix(expectedPriority), "Expected priority to match facility(user/1)*8 + severity") + + collectorLogs, err := framework.ReadCollectorLogs() + Expect(err).To(BeNil()) + Expect(collectorLogs).ToNot(ContainSubstring("VRL compilation warning"), "Expected no VRL compilation warnings in collector logs") + Expect(collectorLogs).ToNot(ContainSubstring("Invalid syslog severity"), "Expected no invalid severity warnings in collector logs") + }, + // facility "user" = 1, priority = 1*8 + severity_code + // full-form keywords (RFC 5424) + Entry("full-form: critical", "critical", "<10>"), // 8 + 2 + Entry("full-form: emergency", "emergency", "<8>"), // 8 + 0 + Entry("full-form: error", "error", "<11>"), // 8 + 3 + Entry("full-form: informational", "informational", "<14>"), // 8 + 6 + Entry("full-form: warning", "warning", "<12>"), // 8 + 4 + // capitalized keywords (as documented in oc explain) + Entry("capitalized: Critical", "Critical", "<10>"), // 8 + 2 + Entry("capitalized: Emergency", "Emergency", "<8>"), // 8 + 0 + Entry("capitalized: Informational", "Informational", "<14>"), // 8 + 6 + ) + DescribeTable("should enrich logs based upon the enrichment type", func(source obs.InputType, enrichment obs.EnrichmentType) { obstestruntime.NewClusterLogForwarderBuilder(framework.Forwarder). FromInput(source). diff --git a/test/functional/outputs/syslog/rfc5424_test.go b/test/functional/outputs/syslog/rfc5424_test.go index 686c26ddb..0c045d876 100644 --- a/test/functional/outputs/syslog/rfc5424_test.go +++ b/test/functional/outputs/syslog/rfc5424_test.go @@ -138,6 +138,10 @@ var _ = Describe("[Functional][Outputs][Syslog] RFC5424 tests", func() { // The 1 after <134> is version, which is always set to 1 expectedPriority := "<134>1 " Expect(outputlogs[0]).To(MatchRegexp(expectedPriority), "Exp to find tag in received message") + + collectorLogs, err := framework.ReadCollectorLogs() + Expect(err).To(BeNil()) + Expect(collectorLogs).ToNot(ContainSubstring("VRL compilation warning"), "Expected no VRL compilation warnings in collector logs") }) It("should use numeric value", func() { @@ -164,7 +168,7 @@ var _ = Describe("[Functional][Outputs][Syslog] RFC5424 tests", func() { }) }) - DescribeTable("should correctly encode short-form severity keywords", func(severity string, expectedPriority string) { + DescribeTable("should correctly encode severity keywords in all forms", func(severity string, expectedPriority string) { obstestruntime.NewClusterLogForwarderBuilder(framework.Forwarder). FromInput(obs.InputTypeApplication). ToSyslogOutput(obs.SyslogRFC5424, func(spec *obs.OutputSpec) { @@ -180,16 +184,32 @@ var _ = Describe("[Functional][Outputs][Syslog] RFC5424 tests", func() { Expect(err).To(BeNil(), "Expected no errors reading the logs") Expect(outputlogs).To(HaveLen(1), "Expected the receiver to receive the message") Expect(outputlogs[0]).To(HavePrefix(expectedPriority), "Expected priority to match facility(user/1)*8 + severity") + + collectorLogs, err := framework.ReadCollectorLogs() + Expect(err).To(BeNil()) + Expect(collectorLogs).ToNot(ContainSubstring("VRL compilation warning"), "Expected no VRL compilation warnings in collector logs") + Expect(collectorLogs).ToNot(ContainSubstring("Invalid syslog severity"), "Expected no invalid severity warnings in collector logs") }, // facility "user" = 1, priority = 1*8 + severity_code - Entry("crit", "crit", "<10>1 "), // 8 + 2 - Entry("emerg", "emerg", "<8>1 "), // 8 + 0 - Entry("err", "err", "<11>1 "), // 8 + 3 - Entry("info", "info", "<14>1 "), // 8 + 6 - Entry("warn", "warn", "<12>1 "), // 8 + 4 - Entry("debug", "debug", "<15>1 "), // 8 + 7 - Entry("notice", "notice", "<13>1 "), // 8 + 5 - Entry("alert", "alert", "<9>1 "), // 8 + 1 + // full-form keywords (RFC 5424) + Entry("full-form: critical", "critical", "<10>1 "), // 8 + 2 + Entry("full-form: emergency", "emergency", "<8>1 "), // 8 + 0 + Entry("full-form: error", "error", "<11>1 "), // 8 + 3 + Entry("full-form: informational", "informational", "<14>1 "), // 8 + 6 + Entry("full-form: warning", "warning", "<12>1 "), // 8 + 4 + Entry("full-form: debug", "debug", "<15>1 "), // 8 + 7 + Entry("full-form: notice", "notice", "<13>1 "), // 8 + 5 + Entry("full-form: alert", "alert", "<9>1 "), // 8 + 1 + // short-form keywords (VRL to_syslog_level output) + Entry("short-form: crit", "crit", "<10>1 "), // 8 + 2 + Entry("short-form: emerg", "emerg", "<8>1 "), // 8 + 0 + Entry("short-form: err", "err", "<11>1 "), // 8 + 3 + Entry("short-form: info", "info", "<14>1 "), // 8 + 6 + Entry("short-form: warn", "warn", "<12>1 "), // 8 + 4 + // capitalized keywords (as documented in oc explain) + Entry("capitalized: Critical", "Critical", "<10>1 "), // 8 + 2 + Entry("capitalized: Emergency", "Emergency", "<8>1 "), // 8 + 0 + Entry("capitalized: Informational", "Informational", "<14>1 "), // 8 + 6 ) It("should be able to send a large payload", func() {