Skip to content
Merged
21 changes: 14 additions & 7 deletions docs/sources/reference/components/loki/loki.source.syslog.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<host:port>` 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 `<host:port>` 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 |
Expand Down Expand Up @@ -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

Expand Down
3 changes: 3 additions & 0 deletions internal/component/loki/source/syslog/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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 {
Expand All @@ -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
}
Expand Down Expand Up @@ -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
Comment thread
blewis12 marked this conversation as resolved.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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})`)
Expand Down Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions internal/component/loki/source/syslog/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions internal/component/loki/source/syslog/types_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading