Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
5549775
chore: use current go-syslog version for replacements as well
x1unix Dec 12, 2025
d10570f
feat: add raw syslog format option
x1unix Dec 12, 2025
8a59b17
feat: implement raw parser
x1unix Dec 16, 2025
6c765a4
feat: add raw read test
x1unix Dec 16, 2025
e7c8c39
fix: fix stream parser
x1unix Dec 16, 2025
ad24b24
fix: octet counting
x1unix Dec 16, 2025
719b5a3
feat: add octetcount single line case
x1unix Dec 16, 2025
86bc6e5
fix: remove unused ReadLineRaw
x1unix Dec 16, 2025
d66f315
feat: add raw support for TCP/UDP streams
x1unix Dec 16, 2025
8aa0196
fix: TCP conn close logging
x1unix Dec 16, 2025
c0bd46b
fix: handle CEF logs
x1unix Dec 17, 2025
7feaeae
fix: use helper to prepopulate facility and severity from priority
x1unix Dec 18, 2025
b385c48
feat: handle raw messages
x1unix Dec 18, 2025
9158834
fix: handle empty vals
x1unix Dec 18, 2025
36a9e26
fix: use more meaningful name for raw parse opts
x1unix Dec 18, 2025
f65d1c6
feat: add raw message parse option
x1unix Dec 18, 2025
a9c7022
fix: yaml attribute name
x1unix Dec 18, 2025
21b7e45
feat: map alloy config to raw options
x1unix Dec 18, 2025
cbbe412
feat: update promtail yaml mapper
x1unix Dec 18, 2025
e526342
fix: component name
x1unix Dec 18, 2025
064f17e
feat: update promtailconvert tests
x1unix Dec 18, 2025
f7290fb
feat: update component docs
x1unix Dec 18, 2025
91837dc
fix: use strconv.Atoi instead of ParseInt
x1unix Dec 18, 2025
b275b9d
fix: Apply suggestions from docs review
x1unix Dec 18, 2025
0589768
fix: linter
x1unix Dec 18, 2025
5bddfb7
fix: deadlock
x1unix Dec 19, 2025
0025d2b
fix: CEF test
x1unix Dec 19, 2025
1ab7ab5
fix: review
x1unix Dec 19, 2025
894e324
fix: add missing raw_format_options block in blocks list
x1unix Dec 19, 2025
3ed603c
Apply suggestions from code review
x1unix Dec 23, 2025
2ba0e7e
fix: typo
x1unix Dec 23, 2025
786d411
feat: make raw format experimental
x1unix Dec 23, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion dependency-replacements.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ replaces:

- comment: Use forked syslog implementation by leodido for continued support
dependency: github.com/influxdata/go-syslog/v3
replacement: github.com/leodido/go-syslog/v4 v4.2.0
replacement: github.com/leodido/go-syslog/v4 v4.3.0

- comment: Replace thanos-io/objstore with Grafana fork
dependency: github.com/thanos-io/objstore
Expand Down
64 changes: 58 additions & 6 deletions docs/sources/reference/components/loki/loki.source.syslog.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,16 @@ title: loki.source.syslog

`loki.source.syslog` listens for syslog messages over TCP or UDP connections and forwards them to other `loki.*` components.
The messages must be compliant with the [RFC5424](https://www.rfc-editor.org/rfc/rfc5424) syslog protocol or the [RFC3164](https://datatracker.ietf.org/doc/html/rfc3164) BSD syslog protocol.
If your messages aren't RFC5424 compliant, you can use syslog-ng or rsyslog to convert the messages to a compliant format.
For a detailed example, refer to the [Monitor RFC5424-compliant syslog messages with Grafana Alloy](https://grafana.com/docs/alloy/latest/monitor/monitor-syslog-messages/) scenario.

{{< admonition type="note" >}}
If your messages aren't RFC5424 compliant, you can use `raw` syslog format in combination with the [`loki.process`](./loki.process.md) component.

Please note, that the `raw` syslog format is an [experimental][] feature.
{{< /admonition >}}

[experimental]: https://grafana.com/docs/release-life-cycle/

The component starts a new syslog listener for each of the given `config` blocks and fans out incoming entries to the list of receivers in `forward_to`.

You can specify multiple `loki.source.syslog` components by giving them different labels.
Expand Down Expand Up @@ -81,16 +88,18 @@ loki.relabel "syslog" {

You can use the following blocks with `loki.source.syslog`:

| Name | Description | Required |
|-----------------------------------------|-----------------------------------------------------------------------------|----------|
| [`listener`][listener] | Configures a listener for Syslog messages. | no |
| `listener` > [`tls_config`][tls_config] | Configures TLS settings for connecting to the endpoint for TCP connections. | no |
| Name | Description | Required |
|---------------------------------------------------------|-----------------------------------------------------------------------------|----------|
| [`listener`][listener] | Configures a listener for Syslog messages. | no |
| `listener` > [`raw_format_options`][raw_format_options] | Configures `raw` syslog format behavior. | no |
| `listener` > [`tls_config`][tls_config] | Configures TLS settings for connecting to the endpoint for TCP connections. | no |

The > symbol indicates deeper levels of nesting.
For example, `listener` > `tls_config` refers to a `tls_config` block defined inside a `listener` block.

[listener]: #listener
[tls_config]: #tls_config
[raw_format_options]: #raw_format_options

### `listener`

Expand All @@ -108,7 +117,7 @@ Only the `address` field is required and any omitted fields take their default v
| `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 |
| `syslog_format` | `string` | The format for incoming messages. Must be either `rfc5424` or `rfc3164`. | `"rfc5424"` | no |
| `syslog_format` | `string` | The format for incoming messages. See [supported formats](#supported-formats). | `"rfc5424"` | no |
| `use_incoming_timestamp` | `bool` | Whether to set the timestamp to the incoming syslog record timestamp. | `false` | no |
| `use_rfc5424_message` | `bool` | Whether to forward the full RFC5424-formatted syslog message. | `false` | no |

Expand All @@ -125,6 +134,49 @@ The `rfc3164_default_to_current_year` argument is only relevant when `use_incomi
`rfc3164` message timestamps don't contain a year, and this component's default behavior is to mimic Promtail behavior and leave the year as 0.
Setting `rfc3164_default_to_current_year` to `true` sets the year of the incoming timestamp to the current year using the local time of the {{< param "PRODUCT_NAME" >}} instance.

{{< admonition type="note" >}}
The `rfc3164_default_to_current_year`, `use_incoming_timestamp` and `use_rfc5424_message` fields cannot be used when `syslog_format` is set to `raw`.
{{< /admonition >}}

#### Supported formats

* **`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`
* **`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`.
* **`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.

{{< admonition type="note" >}}
The `raw` format is an [experimental][] feature.
Experimental features are subject to frequent breaking changes, and may be removed with no equivalent replacement.
To enable and use an experimental feature, you must set the `stability.level` [flag][] to `experimental`.
{{< /admonition >}}

[flag]: https://grafana.com/docs/alloy/<ALLOY_VERSION>/reference/cli/run/
[experimental]: https://grafana.com/docs/release-life-cycle/

[cef]: https://www.splunk.com/en_us/blog/learn/common-event-format-cef.html

### `raw_format_options`

{{< docs/shared lookup="stability/experimental_feature.md" source="alloy" version="<ALLOY_VERSION>" >}}

The `raw_format_options` block configures the `raw` syslog format behavior.

{{< admonition type="note" >}}
This block can only be used when you set `syslog_format` to `raw`.
{{< /admonition >}}

The following argument is supported:

| Name | Type | Description | Default | Required |
|---------------------------------|--------|-----------------------------------------------------------------------------|---------|----------|
| `use_null_terminator_delimiter` | `bool` | Use null-terminator (`\0`) instead of line break (`\n`) to split log lines. | `false` | no |

### `tls_config`

{{< docs/shared lookup="reference/components/tls-config-block.md" source="alloy" version="<ALLOY_VERSION>" >}}
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -1037,7 +1037,7 @@ replace github.com/grafana/regexp => github.com/grafana/regexp v0.0.0-2024051813
replace github.com/hashicorp/memberlist => github.com/grafana/memberlist v0.3.1-0.20220714140823-09ffed8adbbe

// Use forked syslog implementation by leodido for continued support
replace github.com/influxdata/go-syslog/v3 => github.com/leodido/go-syslog/v4 v4.2.0
replace github.com/influxdata/go-syslog/v3 => github.com/leodido/go-syslog/v4 v4.3.0

// Replace thanos-io/objstore with Grafana fork
replace github.com/thanos-io/objstore => github.com/grafana/objstore v0.0.0-20250210100727-533688b5600d
Expand Down
64 changes: 62 additions & 2 deletions internal/component/loki/source/syslog/config/config.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package config

import (
"fmt"
"time"

promconfig "github.com/prometheus/common/config"
Expand All @@ -10,12 +11,66 @@ import (
type SyslogFormat string

const (
// A modern Syslog RFC
// SyslogFormatRFC5424 is a modern Syslog RFC format.
SyslogFormatRFC5424 = "rfc5424"
// A legacy Syslog RFC also known as BSD-syslog
// SyslogFormatRFC3164 is a legacy Syslog RFC format, also known as BSD-syslog.
SyslogFormatRFC3164 = "rfc3164"

// SyslogFormatRaw is a raw format.
//
// Using this format, skips log label parsing.
SyslogFormatRaw = "raw"
)

// MarshalText implements encoding.TextMarshaler
func (s SyslogFormat) MarshalText() (text []byte, err error) {
return []byte(s), nil
}

// UnmarshalText implements encoding.TextUnmarshaler
func (s *SyslogFormat) UnmarshalText(text []byte) error {
str := SyslogFormat(text)
switch str {
case "rfc5424":
*s = SyslogFormatRFC5424
case "rfc3164":
*s = SyslogFormatRFC3164
case "raw":
*s = SyslogFormatRaw
default:
return fmt.Errorf("unknown syslog format: %s", str)
}

return nil
}

func (s SyslogFormat) Validate() error {
switch s {
case SyslogFormatRFC5424,
SyslogFormatRFC3164,
SyslogFormatRaw:
return nil
}

return fmt.Errorf("unknown syslog format: %q", s)
}

// RawFormatOptions are options for raw syslog format processing.
type RawFormatOptions struct {
// UseNullTerminatorDelimiter sets null terminator ('\0') as a log line delimiter for non-transparent framed messages.
//
// When set to false, new line character ('\n') is used instead.
UseNullTerminatorDelimiter bool `yaml:"use_null_terminator_delimiter"`
}

func (opts RawFormatOptions) Delimiter() byte {
if opts.UseNullTerminatorDelimiter {
return 0
}

return '\n'
}

// SyslogTargetConfig describes a scrape config that listens for log lines over syslog.
type SyslogTargetConfig struct {
// ListenAddress is the address to listen on for syslog messages.
Expand Down Expand Up @@ -48,6 +103,11 @@ type SyslogTargetConfig struct {
// Default is rfc5424.
SyslogFormat SyslogFormat `yaml:"syslog_format"`

// RawFormatOptions are options for processing syslog messages in raw mode.
//
// Takes effect only if "syslog_format" is set to "raw".
RawFormatOptions RawFormatOptions `yaml:"raw_format_options"`

// MaxMessageLength sets the maximum limit to the length of syslog messages
MaxMessageLength int `yaml:"max_message_length"`

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
package syslogparser

import (
"bufio"
"bytes"
"errors"
"fmt"
"io"
"iter"
"strconv"
"unicode"

"github.com/leodido/go-syslog/v4"
)

// IterStreamRaw returns an iterator to read syslog lines from a stream without contents parsing.
//
// Delimiter argument is used to determine line end for non-transparent framing.
func IterStreamRaw(r io.Reader, delimiter byte) iter.Seq2[*syslog.Base, error] {
return func(yield func(*syslog.Base, error) bool) {
buf := bufio.NewReaderSize(r, 1<<10)
for {
r, err := parseLineRaw(buf, delimiter)
if err != nil {
if !errors.Is(err, io.EOF) {
yield(nil, err)
}

return
}

// skip empty lines
if r == nil {
continue
}

if !yield(r, nil) {
return
}
}
}
}

func parseLineRaw(buf *bufio.Reader, delimiter byte) (*syslog.Base, error) {
b, err := buf.ReadByte()
if err != nil {
return nil, err
}

// TODO: use bytebufferpool?
_ = buf.UnreadByte()
ftype := framingTypeFromFirstByte(b)
if ftype == framingTypeOctetCounting {
contentLength, err := readFrameLength(buf)
if err != nil {
return nil, fmt.Errorf("failed to read octet length header: %w", err)
}

buff := make([]byte, contentLength)
n, err := buf.Read(buff)
if err != nil {
return nil, fmt.Errorf("cannot read message: %w (length: %d)", err, contentLength)
}

if n == 0 {
return nil, fmt.Errorf("empty buffer returned (expected: %d)", contentLength)
}

buff = buff[:n]
return readLogLine(buff), nil
}

// NOTE: CEF logs don't have log priority prefix and will be detected as [framingTypeUnknown], but logic still the same.
buff, err := buf.ReadBytes(delimiter)
if err != nil {
// Ignore io.EOF if some data was returned
if !errors.Is(err, io.EOF) || len(buff) == 0 {
return nil, err
}
}

if len(buff) == 0 {
return nil, nil
}

// trim potential newline leftovers if called sequentially inside TCP conn.
buff = bytes.TrimFunc(buff, unicode.IsSpace)
if len(buff) == 0 {
return nil, nil
}

return readLogLine(buff), nil
}

func readLogLine(line []byte) *syslog.Base {
out := &syslog.Base{}
line = readSeverity(line, out)

msg := string(bytes.TrimSpace(line))
out.Message = &msg
return out
}

func readSeverity(line []byte, dst *syslog.Base) (next []byte) {
// priority has to be in format '<0-9+>'
if len(line) < 3 || line[0] != '<' {
return line
}

buff := line[1:]
priority := uint(0)
for i, v := range buff {
if v == '>' {
if i == 0 || priority > 255 {
return line
}

dst.ComputeFromPriority(uint8(priority))
buff = buff[i+1:]
return buff
}

if !isDigit(v) {
return line
}

priority *= 10
priority += uint(v - '0')
}

return line
}

func readFrameLength(r *bufio.Reader) (flen int, err error) {
// log lines with octet counted framing start with length.
// Example: `114 <34>1 2025-01-03T14:07:15.003Z message...`
part, err := r.ReadString(' ')
if err != nil {
return 0, fmt.Errorf("%w (read: %q)", err, part)
}

if len(part) == 0 {
return 0, errors.New("missing octet length")
}

// ReadString returns value with its delimiter
part = part[:len(part)-1]
c, err := strconv.Atoi(part)
if err != nil {
return 0, fmt.Errorf("failed to parse octet length from %q: %w", part, err)
}

return c, nil
}
Loading
Loading