Skip to content

Commit

Permalink
pkg/promtail: IETF Syslog (RFC5424) Support (#1275)
Browse files Browse the repository at this point in the history
* IETF syslog (rfc5254) support

* Upgrade go-syslog to correctly released v2.0.1

* Incorporate feedback from new linter

* Update documentation with relevant RFC sections.

* Incorporate feedback from review

* Use context to shutdown server.
* Use util.Backoff from cortex
* Do not embed mutex into TestLabeledClient
* Use strings.HasPrefix
* Better naming of connectionsClosed -> openConnections

* Incorporate further feedback from review

* Callback instead of channel in ParseStream.
  Removes one running Goroutine per connection.
* Improve parse error log level.
* Move mutex above field it protects.
* Use backoff.Ongoing()

* Switch to "nontransparent" parser

* Further improvements to the documentation
  • Loading branch information
bastjan authored and cyriltovena committed Dec 13, 2019
1 parent 9420fb1 commit f0f6f24
Show file tree
Hide file tree
Showing 47 changed files with 25,350 additions and 0 deletions.
5 changes: 5 additions & 0 deletions docs/clients/promtail/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ Just like Prometheus, `promtail` is configured using a `scrape_configs` stanza.
drop, and the final metadata to attach to the log line. Refer to the docs for
[configuring Promtail](configuration.md) for more details.

## Receiving Logs From Syslog

When the [Syslog Target](./scraping.md#syslog-target) is being used, logs
can be written with the syslog protocol to the configured port.

## Labeling and Parsing

During service discovery, metadata is determined (pod name, filename, etc.) that
Expand Down
82 changes: 82 additions & 0 deletions docs/clients/promtail/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,15 @@ and how to scrape logs from files.
* [metric_histogram](#metric_histogram)
* [tenant_stage](#tenant_stage)
* [journal_config](#journal_config)
* [syslog_config](#syslog_config)
* [relabel_config](#relabel_config)
* [static_config](#static_config)
* [file_sd_config](#file_sd_config)
* [kubernetes_sd_config](#kubernetes_sd_config)
* [target_config](#target_config)
* [Example Docker Config](#example-docker-config)
* [Example Journal Config](#example-journal-config)
* [Example Syslog Config](#example-syslog-config)

## Configuration File Reference

Expand Down Expand Up @@ -244,6 +246,9 @@ job_name: <string>
# Describes how to scrape logs from the journal.
[journal: <journal_config>]
# Describes how to receive logs from syslog.
[syslog: <syslog_config>]
# Describes how to relabel targets to determine if they should
# be processed.
relabel_configs:
Expand Down Expand Up @@ -580,6 +585,59 @@ labels:
[path: <string>]
```

### syslog_config

The `syslog_config` block configures a syslog listener allowing users to push
logs to promtail with the syslog protocol.
Currently supported is [IETF Syslog (RFC5424)](https://tools.ietf.org/html/rfc5424)
with and without octet counting.

The recommended deployment is to have a dedicated syslog forwarder like **syslog-ng** or **rsyslog**
in front of promtail. The forwarder can take care of the various specifications
and transports that exist (UDP, BSD syslog, ...).

[Octet counting](https://tools.ietf.org/html/rfc6587#section-3.4.1) is recommended as the
message framing method. In a stream with [non-transparent framing](https://tools.ietf.org/html/rfc6587#section-3.4.2),
promtail needs to wait for the next message to catch multi-line messages,
therefore delays between messages can occur.

See recommended output configurations for
[syslog-ng](scraping.md#syslog-ng-output-configuration) and
[rsyslog](scraping.md#rsyslog-output-configuration). Both configurations enable
IETF Syslog with octet-counting.

You may need to increase the open files limit for the promtail process
if many clients are connected. (`ulimit -Sn`)

```yaml
# TCP address to listen on. Has the format of "host:port".
listen_address: <string>
# The idle timeout for tcp syslog connections, default is 120 seconds.
idle_timeout: <duration>
# Whether to convert syslog structured data to labels.
# A structured data entry of [example@99999 test="yes"] would become
# the label "__syslog_message_sd_example_99999_test" with the value "yes".
label_structured_data: <bool>
# Label map to add to every log message.
labels:
[ <labelname>: <labelvalue> ... ]
```

#### Available Labels

* `__syslog_connection_ip_address`: The remote IP address.
* `__syslog_connection_hostname`: The remote hostname.
* `__syslog_message_severity`: The [syslog severity](https://tools.ietf.org/html/rfc5424#section-6.2.1) parsed from the message. Symbolic name as per [syslog_message.go](https://github.com/influxdata/go-syslog/blob/v2.0.1/rfc5424/syslog_message.go#L184).
* `__syslog_message_facility`: The [syslog facility](https://tools.ietf.org/html/rfc5424#section-6.2.1) parsed from the message. Symbolic name as per [syslog_message.go](https://github.com/influxdata/go-syslog/blob/v2.0.1/rfc5424/syslog_message.go#L235) and `syslog(3)`.
* `__syslog_message_hostname`: The [hostname](https://tools.ietf.org/html/rfc5424#section-6.2.4) parsed from the message.
* `__syslog_message_app_name`: The [app-name field](https://tools.ietf.org/html/rfc5424#section-6.2.5) parsed from the message.
* `__syslog_message_proc_id`: The [procid field](https://tools.ietf.org/html/rfc5424#section-6.2.6) parsed from the message.
* `__syslog_message_msg_id`: The [msgid field](https://tools.ietf.org/html/rfc5424#section-6.2.7) parsed from the message.
* `__syslog_message_sd_<sd_id>[_<iana_enterprise_id>]_<sd_name>`: The [structured-data field](https://tools.ietf.org/html/rfc5424#section-6.3) parsed from the message. The data field `[custom@99770 example="1"]` becomes `__syslog_message_sd_custom_99770_example`.

### relabel_config

Relabeling is a powerful tool to dynamically rewrite the label set of a target
Expand Down Expand Up @@ -970,3 +1028,27 @@ scrape_configs:
- source_labels: ['__journal__systemd_unit']
target_label: 'unit'
```

## Example Syslog Config

```yaml
server:
http_listen_port: 9080
grpc_listen_port: 0
positions:
filename: /tmp/positions.yaml
clients:
- url: http://loki_addr:3100/loki/api/v1/push
scrape_configs:
- job_name: syslog
syslog:
listen_address: 0.0.0.0:1514
labels:
job: "syslog"
relabel_configs:
- source_labels: ['__syslog_message_hostname']
target_label: 'host'
```
56 changes: 56 additions & 0 deletions docs/clients/promtail/scraping.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,62 @@ When Promtail reads from the journal, it brings in all fields prefixed with
field from the journal was transformed into a label called `unit` through
`relabel_configs`. See [Relabeling](#relabeling) for more information.

## Syslog Receiver

Promtail supports receiving [IETF Syslog (RFC5424)](https://tools.ietf.org/html/rfc5424)
messages from a tcp stream. Receiving syslog messages is defined in a `syslog`
stanza:

```yaml
scrape_configs:
- job_name: syslog
syslog:
listen_address: 0.0.0.0:1514
idle_timeout: 60s
label_structured_data: yes
labels:
job: "syslog"
relabel_configs:
- source_labels: ['__syslog_message_hostname']
target_label: 'host'
```

The only required field in the syslog section is the `listen_address` field,
where a valid network address should be provided. The `idle_timeout` can help
with cleaning up stale syslog connections. If `label_structured_data` is set,
[structured data](https://tools.ietf.org/html/rfc5424#section-6.3) in the
syslog header will be translated to internal labels in the form of
`__syslog_message_sd_<ID>_<KEY>`.
The labels map defines a constant list of labels to add to every journal entry
that Promtail reads.

Note that it is recommended to deploy a dedicated syslog forwarder
like **syslog-ng** or **rsyslog** in front of Promtail.
The forwarder can take care of the various specifications
and transports that exist (UDP, BSD syslog, ...). See recommended output
configurations for [syslog-ng](#syslog-ng-output-configuration) and
[rsyslog](#rsyslog-output-configuration).

When Promtail receives syslog messages, it brings in all header fields,
parsed from the received message, prefixed with `__syslog_` as internal labels.
Like in the example above, the `__syslog_message_hostname`
field from the journal was transformed into a label called `host` through
`relabel_configs`. See [Relabeling](#relabeling) for more information.

### Syslog-NG Output Configuration

```
destination d_loki {
syslog("localhost" transport("tcp") port(<promtail_port>));
};
```
### Rsyslog Output Configuration
```
action(type="omfwd" protocol="tcp" port="<promtail_port>" Template="RSYSLOG_SyslogProtocol23Format" TCP_Framing="octet-counted")
```
## Relabeling
Each `scrape_configs` entry can contain a `relabel_configs` stanza.
Expand Down
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,14 @@ require (
github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645
github.com/hashicorp/golang-lru v0.5.3
github.com/hpcloud/tail v1.0.0
github.com/influxdata/go-syslog/v2 v2.0.1
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af
github.com/json-iterator/go v1.1.7
github.com/klauspost/compress v1.7.4
github.com/klauspost/cpuid v1.2.1 // indirect
github.com/mitchellh/mapstructure v1.1.2
github.com/morikuni/aec v0.0.0-20170113033406-39771216ff4c // indirect
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f
github.com/opencontainers/go-digest v1.0.0-rc1 // indirect
github.com/opencontainers/image-spec v1.0.1 // indirect
github.com/opentracing/opentracing-go v1.1.0
Expand Down
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,8 @@ github.com/hashicorp/serf v0.8.3 h1:MWYcmct5EtKz0efYooPcL0yNkem+7kWxqXDi/UIh+8k=
github.com/hashicorp/serf v0.8.3/go.mod h1:UpNcs7fFbpKIyZaUuSW6EPiH+eZC7OuyFD+wc1oal+k=
github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/influxdata/go-syslog/v2 v2.0.1 h1:l44S4l4Q8MhGQcoOxJpbo+QQYxJqp0vdgIVHh4+DO0s=
github.com/influxdata/go-syslog/v2 v2.0.1/go.mod h1:hjvie1UTaD5E1fTnDmxaCw8RRDrT4Ve+XHr5O2dKSCo=
github.com/influxdata/influxdb v1.7.7/go.mod h1:qZna6X/4elxqT3yI9iZYdZrWWdeFOOprn86kgg4+IzY=
github.com/jessevdk/go-flags v0.0.0-20180331124232-1c38ed7ad0cc/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5imkbgOkpRUYLnmbU7UEFbjtDA2hxJ1ichM=
Expand Down Expand Up @@ -413,6 +415,8 @@ github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LE
github.com/lann/builder v0.0.0-20150808151131-f22ce00fd939/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o=
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw=
github.com/leanovate/gopter v0.2.4/go.mod h1:gNcbPWNEWRe4lm+bycKqxUYoH5uoVje5SkOJ3uoLer8=
github.com/leodido/ragel-machinery v0.0.0-20181214104525-299bdde78165 h1:bCiVCRCs1Heq84lurVinUPy19keqGEe4jh5vtK37jcg=
github.com/leodido/ragel-machinery v0.0.0-20181214104525-299bdde78165/go.mod h1:WZxr2/6a/Ar9bMDc2rN/LJrE/hF6bXE4LPyDSIxwAfg=
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lovoo/gcloud-opentracing v0.3.0/go.mod h1:ZFqk2y38kMDDikZPAK7ynTTGuyt17nSPdS3K5e+ZTBY=
github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
Expand Down
19 changes: 19 additions & 0 deletions pkg/promtail/scrape/scrape.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package scrape
import (
"fmt"
"reflect"
"time"

"github.com/prometheus/common/model"

Expand All @@ -19,6 +20,7 @@ type Config struct {
EntryParser api.EntryParser `yaml:"entry_parser"`
PipelineStages stages.PipelineStages `yaml:"pipeline_stages,omitempty"`
JournalConfig *JournalTargetConfig `yaml:"journal,omitempty"`
SyslogConfig *SyslogTargetConfig `yaml:"syslog,omitempty"`
RelabelConfigs []*relabel.Config `yaml:"relabel_configs,omitempty"`
ServiceDiscoveryConfig sd_config.ServiceDiscoveryConfig `yaml:",inline"`
}
Expand All @@ -42,6 +44,23 @@ type JournalTargetConfig struct {
Path string `yaml:"path"`
}

// 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.
ListenAddress string `yaml:"listen_address"`

// IdleTimeout is the idle timeout for tcp connections.
IdleTimeout time.Duration `yaml:"idle_timeout"`

// LabelStructuredData sets if the structured data part of a syslog message
// is translated to a label.
// [example@99999 test="yes"] => {__syslog_message_sd_example_99999_test="yes"}
LabelStructuredData bool `yaml:"label_structured_data"`

// Labels optionally holds labels to associate with each record read from syslog.
Labels model.LabelSet `yaml:"labels"`
}

// DefaultScrapeConfig is the default Config.
var DefaultScrapeConfig = Config{
EntryParser: api.Docker,
Expand Down
14 changes: 14 additions & 0 deletions pkg/promtail/targets/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ func NewTargetManagers(
var targetManagers []targetManager
var fileScrapeConfigs []scrape.Config
var journalScrapeConfigs []scrape.Config
var syslogScrapeConfigs []scrape.Config

for _, cfg := range scrapeConfigs {
if cfg.HasServiceDiscoveryConfig() {
Expand Down Expand Up @@ -70,6 +71,19 @@ func NewTargetManagers(
targetManagers = append(targetManagers, journalTargetManager)
}

for _, cfg := range scrapeConfigs {
if cfg.SyslogConfig != nil {
syslogScrapeConfigs = append(syslogScrapeConfigs, cfg)
}
}
if len(syslogScrapeConfigs) > 0 {
syslogTargetManager, err := NewSyslogTargetManager(logger, client, syslogScrapeConfigs)
if err != nil {
return nil, errors.Wrap(err, "failed to make syslog target manager")
}
targetManagers = append(targetManagers, syslogTargetManager)
}

return &TargetManagers{targetManagers: targetManagers}, nil

}
Expand Down
35 changes: 35 additions & 0 deletions pkg/promtail/targets/syslogparser/syslogparser.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package syslogparser

import (
"bufio"
"fmt"
"io"

"github.com/influxdata/go-syslog/v2"
"github.com/influxdata/go-syslog/v2/nontransparent"
"github.com/influxdata/go-syslog/v2/octetcounting"
)

// ParseStream parses a rfc5424 syslog stream from the given Reader, calling
// the callback function with the parsed messages. The parser automatically
// detects octet counting.
// The function returns on EOF or unrecoverable errors.
func ParseStream(r io.Reader, callback func(res *syslog.Result)) error {
buf := bufio.NewReader(r)

firstByte, err := buf.Peek(1)
if err != nil {
return err
}

b := firstByte[0]
if b == '<' {
nontransparent.NewParser(syslog.WithListener(callback)).Parse(buf)
} else if b >= '0' && b <= '9' {
octetcounting.NewParser(syslog.WithListener(callback)).Parse(buf)
} else {
return fmt.Errorf("invalid or unsupported framing. first byte: '%s'", firstByte)
}

return nil
}
61 changes: 61 additions & 0 deletions pkg/promtail/targets/syslogparser/syslogparser_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package syslogparser_test

import (
"io"
"strings"
"testing"

"github.com/grafana/loki/pkg/promtail/targets/syslogparser"
"github.com/influxdata/go-syslog/v2"
"github.com/stretchr/testify/require"
)

func TestParseStream_OctetCounting(t *testing.T) {
r := strings.NewReader("23 <13>1 - - - - - - First24 <13>1 - - - - - - Second")

results := make([]*syslog.Result, 0)
cb := func(res *syslog.Result) {
results = append(results, res)
}

err := syslogparser.ParseStream(r, cb)
require.NoError(t, err)

require.Equal(t, 2, len(results))
require.NoError(t, results[0].Error)
require.Equal(t, "First", *results[0].Message.Message())
require.NoError(t, results[1].Error)
require.Equal(t, "Second", *results[1].Message.Message())
}

func TestParseStream_NewlineSeparated(t *testing.T) {
r := strings.NewReader("<13>1 - - - - - - First\n<13>1 - - - - - - Second\n")

results := make([]*syslog.Result, 0)
cb := func(res *syslog.Result) {
results = append(results, res)
}

err := syslogparser.ParseStream(r, cb)
require.NoError(t, err)

require.Equal(t, 2, len(results))
require.NoError(t, results[0].Error)
require.Equal(t, "First", *results[0].Message.Message())
require.NoError(t, results[1].Error)
require.Equal(t, "Second", *results[1].Message.Message())
}

func TestParseStream_InvalidStream(t *testing.T) {
r := strings.NewReader("invalid")

err := syslogparser.ParseStream(r, func(res *syslog.Result) {})
require.EqualError(t, err, "invalid or unsupported framing. first byte: 'i'")
}

func TestParseStream_EmptyStream(t *testing.T) {
r := strings.NewReader("")

err := syslogparser.ParseStream(r, func(res *syslog.Result) {})
require.Equal(t, err, io.EOF)
}
Loading

0 comments on commit f0f6f24

Please sign in to comment.