diff --git a/docs/sources/reference/components/loki/loki.source.syslog.md b/docs/sources/reference/components/loki/loki.source.syslog.md index ca3e483777c..2bf5214e3a1 100644 --- a/docs/sources/reference/components/loki/loki.source.syslog.md +++ b/docs/sources/reference/components/loki/loki.source.syslog.md @@ -112,12 +112,14 @@ The `listener` block defines the listen address and protocol where the listener The following arguments can be used to configure a `listener`. Only the `address` field is required and any omitted fields take their default values. -| Name | Type | Description | Default | Required | -|-----------------------------------|---------------|----------------------------------------------------------------------------------------|-------------|----------| -| `address` | `string` | The `` address to listen to for syslog messages. | | yes | -| `idle_timeout` | `duration` | The idle timeout for TCP connections. | `"120s"` | no | -| `label_structured_data` | `bool` | Whether to translate syslog structured data to Loki labels. | `false` | no | -| `labels` | `map(string)` | The labels to associate with each received syslog record. | `{}` | no | +| Name | Type | Description | Default | Required | +|---------------------------|---------------|------------------------------------------------------------------------------------------------------------------------------------------------------|----------|----------| +| `address` | `string` | The `` address to listen to for syslog messages. | | yes | +| `allow_empty_rfc5424_msg` | `bool` | Whether to forward RFC5424 messages with empty MSG content. When `false`, such messages are dropped. Only applies when `syslog_format` is `rfc5424`. | `false` | no | +| `idle_timeout` | `duration` | The idle timeout for TCP connections. | `"120s"` | no | +| `label_structured_data` | `bool` | Whether to translate syslog structured data to Loki labels. | `false` | no | +| `labels` | `map(string)` | The labels to associate with each received syslog record. | `{}` | no | + | `max_message_length` | `int` | The maximum limit to the length of syslog messages. | `8192` | no | | `protocol` | `string` | The protocol to listen to for syslog messages. Must be either `tcp` or `udp`. | `"tcp"` | no | | `rfc3164_default_to_current_year` | `bool` | Whether to default the incoming timestamp of an `rfc3164` message to the current year. | `false` | no | @@ -146,13 +148,18 @@ The `rfc3164_default_to_current_year`, `use_incoming_timestamp` and `use_rfc5424 * **`rfc3164`** A legacy syslog format, also known as BSD syslog. - Example: `<34>Oct 11 22:14:15 my-server-01 sshd[1234]: Failed password for root from 192.168.1.10 port 22 ssh2` + Example: `<34>Oct 11 22:14:15 my-server-01 sshd[1234]: Failed password for root from 192.168.1.10 port 22 ssh2`. + `loki.source.syslog` drops messages with empty MSG content and increments the `loki_source_syslog_empty_messages_total` counter. * **`rfc5424`** A modern, structured syslog format. Uses ISO 8601 for timestamps. Example: `<165>1 2025-12-18T00:33:00Z web01 nginx - - [audit@123 id="456"] Login failed`. + `loki.source.syslog` drops messages with empty MSG content by default. + Set `rfc5424_allow_empty_msg` to `true` to forward them. + `loki.source.syslog` increments the `loki_source_syslog_empty_messages_total` counter in both cases for debugging. * **`raw`** Disables log line parsing. This format allows receiving non-RFC5424 compliant logs, such as [CEF][cef]. Raw logs can be forwarded to [`loki.process`](./loki.process.md) component for parsing. + `loki.source.syslog` drops messages with nil or empty body and increments the `loki_source_syslog_empty_messages_total` counter. [cef]: https://www.splunk.com/en_us/blog/learn/common-event-format-cef.html diff --git a/internal/component/loki/source/syslog/config/config.go b/internal/component/loki/source/syslog/config/config.go index d0203c81dc3..009502570e9 100644 --- a/internal/component/loki/source/syslog/config/config.go +++ b/internal/component/loki/source/syslog/config/config.go @@ -126,6 +126,9 @@ type SyslogTargetConfig struct { // When false, the year will default to 0. RFC3164DefaultToCurrentYear bool `yaml:"rfc3164_default_to_current_year"` + // RFC5424AllowEmptyMsg when true, forwards RFC5424 messages with empty MSG content. Default false. + RFC5424AllowEmptyMsg bool `yaml:"rfc5424_allow_empty_msg"` + // RFC3164CiscoComponents enables and configures Cisco IOS syslog parsing. RFC3164CiscoComponents *RFC3164CiscoComponents `yaml:"rfc3164_cisco_components"` } diff --git a/internal/component/loki/source/syslog/internal/syslogtarget/syslogtarget.go b/internal/component/loki/source/syslog/internal/syslogtarget/syslogtarget.go index b1bea63c47b..1e96dde08ad 100644 --- a/internal/component/loki/source/syslog/internal/syslogtarget/syslogtarget.go +++ b/internal/component/loki/source/syslog/internal/syslogtarget/syslogtarget.go @@ -138,7 +138,10 @@ func (t *SyslogTarget) handleMessageError(err error) { func (t *SyslogTarget) handleMessageRFC5424(connLabels labels.Labels, msg *rfc5424.SyslogMessage) { if msg.Message == nil { t.metrics.syslogEmptyMessages.Inc() - return + + if !t.config.RFC5424AllowEmptyMsg { + return + } } lb := labels.NewBuilder(connLabels) @@ -197,7 +200,10 @@ func (t *SyslogTarget) handleMessageRFC5424(connLabels labels.Labels, msg *rfc54 timestamp = time.Now() } - m := *msg.Message + m := "" + if msg.Message != nil { + m = *msg.Message + } if t.config.UseRFC5424Message { fullMsg, err := msg.String() if err != nil { @@ -220,6 +226,7 @@ func (t *SyslogTarget) handleMessageRFC5424(connLabels labels.Labels, msg *rfc54 func (t *SyslogTarget) handleMessageRFC3164(connLabels labels.Labels, msg *rfc3164.SyslogMessage) { if msg.Message == nil { + // Drop RFC3164 messages with an empty Message t.metrics.syslogEmptyMessages.Inc() return } @@ -283,6 +290,7 @@ func (t *SyslogTarget) handleMessageRFC3164(connLabels labels.Labels, msg *rfc31 } func (t *SyslogTarget) handleMessageRaw(connLabels labels.Labels, msg *syslog.Base) { + // Drop raw logs with nil or empty message - this would mean the line itself is empty (e.g. "\n") if msg.Message == nil || *msg.Message == "" { t.metrics.syslogEmptyMessages.Inc() return diff --git a/internal/component/loki/source/syslog/internal/syslogtarget/syslogtarget_test.go b/internal/component/loki/source/syslog/internal/syslogtarget/syslogtarget_test.go index 781944f06f8..bd61a01ba9a 100644 --- a/internal/component/loki/source/syslog/internal/syslogtarget/syslogtarget_test.go +++ b/internal/component/loki/source/syslog/internal/syslogtarget/syslogtarget_test.go @@ -567,6 +567,93 @@ func TestSyslogTarget_RFC5424Messages(t *testing.T) { } } +func TestSyslogTarget_RFC5424MessageEmptyMSGWhenAllowed(t *testing.T) { + msg := `<14>1 2026-02-19T14:57:17.097Z secfw-a RT_FLOW - RT_FLOW_SESSION_DENY [junos@2636.1.1.1.2.129 application="UNKNOWN"]` + + w := log.NewSyncWriter(os.Stderr) + logger := log.NewLogfmtLogger(w) + handler := loki.NewCollectingHandler() + defer handler.Stop() + + metrics := NewMetrics(nil) + tgt, err := NewSyslogTarget(TargetParams{ + Metrics: metrics, + Logger: logger, + Handler: handler, + Relabel: []*relabel.Config{}, + Config: &scrapeconfig.SyslogTargetConfig{ + ListenAddress: "127.0.0.1:0", + ListenProtocol: ProtocolUDP, + RFC5424AllowEmptyMsg: true, + Labels: model.LabelSet{ + "test": "syslog_target", + }, + }, + }) + require.NoError(t, err) + require.Eventually(t, tgt.Ready, time.Second, 10*time.Millisecond) + defer func() { + require.NoError(t, tgt.Stop()) + }() + + addr := tgt.ListenAddress().String() + c, err := net.Dial(ProtocolUDP, addr) + require.NoError(t, err) + + err = writeMessagesToStream(c, []string{msg}, fmtNewline) + require.NoError(t, err) + require.NoError(t, c.Close()) + + require.Eventuallyf(t, func() bool { + return len(handler.Received()) == 1 + }, time.Second, time.Millisecond, "Expected to receive 1 message, got %d.", len(handler.Received())) + + entry := handler.Received()[0] + require.Equal(t, "", entry.Line, "empty MSG yields empty line when using MSG-only format") + require.Equal(t, model.LabelSet{"test": "syslog_target"}, entry.Labels) +} + +func TestSyslogTarget_RFC5424MessageEmptyMSGWhenNotAllowed(t *testing.T) { + // RFC5424 message with empty MSG part (no content after structured data) + msg := `<14>1 2026-02-19T14:57:17.097Z secfw-a RT_FLOW - RT_FLOW_SESSION_DENY [junos@2636.1.1.1.2.129 application="UNKNOWN"]` + + w := log.NewSyncWriter(os.Stderr) + logger := log.NewLogfmtLogger(w) + handler := loki.NewCollectingHandler() + defer handler.Stop() + + metrics := NewMetrics(nil) + tgt, err := NewSyslogTarget(TargetParams{ + Metrics: metrics, + Logger: logger, + Handler: handler, + Relabel: []*relabel.Config{}, + Config: &scrapeconfig.SyslogTargetConfig{ + ListenAddress: "127.0.0.1:0", + ListenProtocol: ProtocolUDP, + RFC5424AllowEmptyMsg: false, + }, + }) + require.NoError(t, err) + require.Eventually(t, tgt.Ready, time.Second, 10*time.Millisecond) + defer func() { + require.NoError(t, tgt.Stop()) + }() + + addr := tgt.ListenAddress().String() + c, err := net.Dial(ProtocolUDP, addr) + require.NoError(t, err) + + err = writeMessagesToStream(c, []string{msg}, fmtNewline) + require.NoError(t, err) + require.NoError(t, c.Close()) + + start := time.Now() + require.Eventually(t, func() bool { + return time.Since(start) >= 100*time.Millisecond && len(handler.Received()) == 0 + }, 200*time.Millisecond, 10*time.Millisecond, "RFC5424 log with nil Message should be dropped when RFC5424AllowEmptyMsg is false") +} + const layout = "Jan 02 15:04:05" var reCefDate = regexp.MustCompile(`(Dec \d{2} \d{2}:\d{2}:\d{2})`) @@ -803,6 +890,89 @@ func TestSyslogTarget_CEFRawMessages(t *testing.T) { require.Equal(t, wantLines, gotLines, "log lines did not match") } +func TestSyslogTarget_RawMessageEmptyDropped(t *testing.T) { + w := log.NewSyncWriter(os.Stderr) + logger := log.NewLogfmtLogger(w) + handler := loki.NewCollectingHandler() + defer handler.Stop() + + metrics := NewMetrics(nil) + tgt, err := NewSyslogTarget(TargetParams{ + Metrics: metrics, + Logger: logger, + Handler: handler, + Relabel: []*relabel.Config{}, + Config: &scrapeconfig.SyslogTargetConfig{ + ListenAddress: "127.0.0.1:0", + ListenProtocol: "udp", + SyslogFormat: scrapeconfig.SyslogFormatRaw, + RawFormatOptions: scrapeconfig.RawFormatOptions{ + UseNullTerminatorDelimiter: false, + }, + }, + }) + require.NoError(t, err) + require.Eventually(t, tgt.Ready, time.Second, 10*time.Millisecond) + + addr := tgt.ListenAddress().String() + c, err := net.Dial("udp", addr) + require.NoError(t, err) + + // "<13>" has PRI only, no message content — raw parser produces empty Message + messages := []string{ + "<13>", + "<34> ", // PRI + whitespace only + } + err = writeMessagesToStream(c, messages, fmtNewline) + require.NoError(t, err) + require.NoError(t, c.Close()) + + time.Sleep(100 * time.Millisecond) + require.NoError(t, tgt.Stop()) + + require.Empty(t, handler.Received(), "raw log with nil Message should be dropped") +} + +func TestSyslogTarget_RFC3164MessageEmptyDropped(t *testing.T) { + // PRI+timestamp only — no hostname, tag, or message. Parser produces nil Message. + logLine := "<13>Dec 1 00:00:00" + + w := log.NewSyncWriter(os.Stderr) + logger := log.NewLogfmtLogger(w) + handler := loki.NewCollectingHandler() + defer handler.Stop() + + metrics := NewMetrics(nil) + tgt, err := NewSyslogTarget(TargetParams{ + Metrics: metrics, + Logger: logger, + Handler: handler, + Relabel: []*relabel.Config{}, + Config: &scrapeconfig.SyslogTargetConfig{ + ListenAddress: "127.0.0.1:0", + ListenProtocol: ProtocolUDP, + SyslogFormat: scrapeconfig.SyslogFormatRFC3164, + }, + }) + require.NoError(t, err) + require.Eventually(t, tgt.Ready, time.Second, 10*time.Millisecond) + defer func() { + require.NoError(t, tgt.Stop()) + }() + + addr := tgt.ListenAddress().String() + c, err := net.Dial(ProtocolUDP, addr) + require.NoError(t, err) + + err = writeMessagesToStream(c, []string{logLine}, fmtNewline) + require.NoError(t, err) + require.NoError(t, c.Close()) + + time.Sleep(100 * time.Millisecond) + + require.Empty(t, handler.Received(), "RFC3164 log with nil Message should be dropped") +} + func TestSyslogTarget_RFC3164YearSetting(t *testing.T) { for _, tt := range []struct { name string diff --git a/internal/component/loki/source/syslog/types.go b/internal/component/loki/source/syslog/types.go index 15d98311e0f..cc4ab659af5 100644 --- a/internal/component/loki/source/syslog/types.go +++ b/internal/component/loki/source/syslog/types.go @@ -22,6 +22,7 @@ type ListenerConfig struct { UseIncomingTimestamp bool `alloy:"use_incoming_timestamp,attr,optional"` UseRFC5424Message bool `alloy:"use_rfc5424_message,attr,optional"` RFC3164DefaultToCurrentYear bool `alloy:"rfc3164_default_to_current_year,attr,optional"` + RFC5424AllowEmptyMsg bool `alloy:"rfc5424_allow_empty_msg,attr,optional"` MaxMessageLength int `alloy:"max_message_length,attr,optional"` TLSConfig config.TLSConfig `alloy:"tls_config,block,optional"` SyslogFormat scrapeconfig.SyslogFormat `alloy:"syslog_format,attr,optional"` @@ -89,6 +90,12 @@ func (sc *ListenerConfig) Validate() error { } } + if sc.SyslogFormat != scrapeconfig.SyslogFormatRFC5424 { + if sc.RFC5424AllowEmptyMsg { + return fmt.Errorf(`"rfc5424_allow_empty_msg" has no effect when syslog format is set to %q`, sc.SyslogFormat) + } + } + if sc.SyslogFormat == scrapeconfig.SyslogFormatRaw { // mention fields that have no effect for better UX if sc.UseRFC5424Message { @@ -129,6 +136,7 @@ func (sc ListenerConfig) Convert() (*scrapeconfig.SyslogTargetConfig, error) { UseIncomingTimestamp: sc.UseIncomingTimestamp, UseRFC5424Message: sc.UseRFC5424Message, RFC3164DefaultToCurrentYear: sc.RFC3164DefaultToCurrentYear, + RFC5424AllowEmptyMsg: sc.RFC5424AllowEmptyMsg, MaxMessageLength: sc.MaxMessageLength, TLSConfig: *sc.TLSConfig.Convert(), SyslogFormat: sc.SyslogFormat, diff --git a/internal/component/loki/source/syslog/types_test.go b/internal/component/loki/source/syslog/types_test.go index c0a859a2d9d..4465b32c347 100644 --- a/internal/component/loki/source/syslog/types_test.go +++ b/internal/component/loki/source/syslog/types_test.go @@ -128,6 +128,7 @@ func TestValidateRawOnlyOpts(t *testing.T) { } mappings := map[string]*bool{ + "rfc5424_allow_empty_msg": &sc.RFC5424AllowEmptyMsg, "use_rfc5424_message": &sc.UseRFC5424Message, "rfc3164_default_to_current_year": &sc.RFC3164DefaultToCurrentYear, "use_incoming_timestamp": &sc.UseIncomingTimestamp,