Adds a Docker log source.
When Postfix is running in a Docker container, it's most useful to use the built-in Docker logging (as with Systemd). Setting `--docker.enable` allows that. The log source is using `client.NewEnvClient`, which reads environment variables to determine which Docker to connect to, and how TLS is handled.
This commit is contained in:
parent
82fa993361
commit
f1d5d0ad4d
18
README.md
18
README.md
@ -5,7 +5,7 @@ This exporter provides histogram metrics for the size and age of messages stored
|
||||
the mail queue. It extracts these metrics from Postfix by connecting to
|
||||
a UNIX socket under `/var/spool`. It also counts events by parsing Postfix's
|
||||
log entries, using regular expression matching. The log entries are retrieved from
|
||||
the systemd journal or from a log file.
|
||||
the systemd journal, the Docker logs, or from a log file.
|
||||
|
||||
## Options
|
||||
|
||||
@ -18,11 +18,25 @@ These options can be used when starting the `postfix_exporter`
|
||||
| `--postfix.showq_path` | Path at which Postfix places its showq socket | `/var/spool/postfix/public/showq` |
|
||||
| `--postfix.logfile_path` | Path where Postfix writes log entries | `/var/log/maillog` |
|
||||
| `--log.unsupported` | Log all unsupported lines | `false` |
|
||||
| `--systemd.enable` | Read from the systemd journal instead of log | `false` |
|
||||
| `--docker.enable` | Read from the Docker logs instead of a file | `false` |
|
||||
| `--docker.container.id` | The container to read Docker logs from | `postfix` |
|
||||
| `--systemd.enable` | Read from the systemd journal instead of file | `false` |
|
||||
| `--systemd.unit` | Name of the Postfix systemd unit | `postfix.service` |
|
||||
| `--systemd.slice` | Name of the Postfix systemd slice. | `""` |
|
||||
| `--systemd.journal_path` | Path to the systemd journal | `""` |
|
||||
|
||||
## Events from Docker
|
||||
|
||||
Postfix servers running in a [Docker](https://www.docker.com/)
|
||||
container can be monitored using the `--docker.enable` flag. The
|
||||
default container ID is `postfix`, but can be customized with the
|
||||
`--docker.container.id` flag.
|
||||
|
||||
The default is to connect to the local Docker, but this can be
|
||||
customized using [the `DOCKER_HOST` and
|
||||
similar](https://pkg.go.dev/github.com/docker/docker/client?tab=doc#NewEnvClient)
|
||||
environment variables.
|
||||
|
||||
## Events from log file
|
||||
|
||||
The log file is tailed when processed. Rotating the log files while the exporter
|
||||
|
7
go.mod
7
go.mod
@ -5,8 +5,15 @@ go 1.13
|
||||
require (
|
||||
github.com/alecthomas/kingpin v2.2.6+incompatible
|
||||
github.com/coreos/go-systemd/v22 v22.0.0
|
||||
github.com/docker/distribution v2.7.1+incompatible // indirect
|
||||
github.com/docker/docker v1.13.1
|
||||
github.com/docker/go-connections v0.4.0 // indirect
|
||||
github.com/docker/go-units v0.4.0 // indirect
|
||||
github.com/fsnotify/fsnotify v1.4.7 // indirect
|
||||
github.com/hpcloud/tail v1.0.0
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.1 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/prometheus/client_golang v1.4.1
|
||||
github.com/prometheus/client_model v0.2.0
|
||||
github.com/stretchr/testify v1.4.0
|
||||
|
12
go.sum
12
go.sum
@ -17,6 +17,14 @@ github.com/coreos/go-systemd/v22 v22.0.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/docker/distribution v2.7.1+incompatible h1:a5mlkVzth6W5A4fOsS3D2EO5BUmsJpcB+cRlLU7cSug=
|
||||
github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
|
||||
github.com/docker/docker v1.13.1 h1:IkZjBSIc8hBjLpqeAbeE5mca5mNgeatLHBy3GO78BWo=
|
||||
github.com/docker/docker v1.13.1/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=
|
||||
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
|
||||
github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw=
|
||||
github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
@ -53,7 +61,10 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJ
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
@ -83,6 +94,7 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81P
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980 h1:dfGZHvZk057jK2MCeWus/TowKpJ8y4AmooUzdBSR9GU=
|
||||
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
|
@ -1,6 +1,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
@ -17,7 +18,7 @@ type LogSourceFactory interface {
|
||||
// New attempts to create a new log source. This is called after
|
||||
// flags have been parsed. Returning `nil, nil`, means the user
|
||||
// didn't want this log source.
|
||||
New() (LogSourceCloser, error)
|
||||
New(context.Context) (LogSourceCloser, error)
|
||||
}
|
||||
|
||||
type LogSourceCloser interface {
|
||||
@ -48,9 +49,9 @@ func InitLogSourceFactories(app *kingpin.Application) {
|
||||
// NewLogSourceFromFactories iterates through the factories and
|
||||
// attempts to instantiate a log source. The first factory to return
|
||||
// success wins.
|
||||
func NewLogSourceFromFactories() (LogSourceCloser, error) {
|
||||
func NewLogSourceFromFactories(ctx context.Context) (LogSourceCloser, error) {
|
||||
for _, f := range logSourceFactories {
|
||||
src, err := f.New()
|
||||
src, err := f.New(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
96
logsource_docker.go
Normal file
96
logsource_docker.go
Normal file
@ -0,0 +1,96 @@
|
||||
// +build !nodocker
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"io"
|
||||
"log"
|
||||
"strings"
|
||||
|
||||
"github.com/alecthomas/kingpin"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/client"
|
||||
)
|
||||
|
||||
// A DockerLogSource reads log records from the given Docker
|
||||
// journal.
|
||||
type DockerLogSource struct {
|
||||
client DockerClient
|
||||
containerID string
|
||||
reader *bufio.Reader
|
||||
}
|
||||
|
||||
// A DockerClient is the client interface that client.Client
|
||||
// provides. See https://pkg.go.dev/github.com/docker/docker/client
|
||||
type DockerClient interface {
|
||||
io.Closer
|
||||
ContainerLogs(context.Context, string, types.ContainerLogsOptions) (io.ReadCloser, error)
|
||||
}
|
||||
|
||||
// NewDockerLogSource returns a log source for reading Docker logs.
|
||||
func NewDockerLogSource(ctx context.Context, c DockerClient, containerID string) (*DockerLogSource, error) {
|
||||
r, err := c.ContainerLogs(ctx, containerID, types.ContainerLogsOptions{
|
||||
ShowStdout: true,
|
||||
ShowStderr: true,
|
||||
Follow: true,
|
||||
Tail: "0",
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
logSrc := &DockerLogSource{
|
||||
client: c,
|
||||
containerID: containerID,
|
||||
reader: bufio.NewReader(r),
|
||||
}
|
||||
|
||||
return logSrc, nil
|
||||
}
|
||||
|
||||
func (s *DockerLogSource) Close() error {
|
||||
return s.client.Close()
|
||||
}
|
||||
|
||||
func (s *DockerLogSource) Path() string {
|
||||
return "docker:" + s.containerID
|
||||
}
|
||||
|
||||
func (s *DockerLogSource) Read(ctx context.Context) (string, error) {
|
||||
line, err := s.reader.ReadString('\n')
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return strings.TrimSpace(line), nil
|
||||
}
|
||||
|
||||
// A dockerLogSourceFactory is a factory that can create
|
||||
// DockerLogSources from command line flags.
|
||||
type dockerLogSourceFactory struct {
|
||||
enable bool
|
||||
containerID string
|
||||
}
|
||||
|
||||
func (f *dockerLogSourceFactory) Init(app *kingpin.Application) {
|
||||
app.Flag("docker.enable", "Read from Docker logs. Environment variable DOCKER_HOST can be used to change the address. See https://pkg.go.dev/github.com/docker/docker/client?tab=doc#NewEnvClient for more information.").Default("false").BoolVar(&f.enable)
|
||||
app.Flag("docker.container.id", "ID/name of the Postfix Docker container.").Default("postfix").StringVar(&f.containerID)
|
||||
}
|
||||
|
||||
func (f *dockerLogSourceFactory) New(ctx context.Context) (LogSourceCloser, error) {
|
||||
if !f.enable {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
log.Println("Reading log events from Docker")
|
||||
c, err := client.NewEnvClient()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return NewDockerLogSource(ctx, c, f.containerID)
|
||||
}
|
||||
|
||||
func init() {
|
||||
RegisterLogSourceFactory(&dockerLogSourceFactory{})
|
||||
}
|
79
logsource_docker_test.go
Normal file
79
logsource_docker_test.go
Normal file
@ -0,0 +1,79 @@
|
||||
// +build !nodocker
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestNewDockerLogSource(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
c := &fakeDockerClient{}
|
||||
src, err := NewDockerLogSource(ctx, c, "acontainer")
|
||||
if err != nil {
|
||||
t.Fatalf("NewDockerLogSource failed: %v", err)
|
||||
}
|
||||
|
||||
assert.Equal(t, []string{"acontainer"}, c.containerLogsCalls, "A call to ContainerLogs should be made.")
|
||||
|
||||
if err := src.Close(); err != nil {
|
||||
t.Fatalf("Close failed: %v", err)
|
||||
}
|
||||
|
||||
assert.Equal(t, 1, c.closeCalls, "A call to Close should be made.")
|
||||
}
|
||||
|
||||
func TestDockerLogSource_Path(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
c := &fakeDockerClient{}
|
||||
src, err := NewDockerLogSource(ctx, c, "acontainer")
|
||||
if err != nil {
|
||||
t.Fatalf("NewDockerLogSource failed: %v", err)
|
||||
}
|
||||
defer src.Close()
|
||||
|
||||
assert.Equal(t, "docker:acontainer", src.Path(), "Path should be set by New.")
|
||||
}
|
||||
|
||||
func TestDockerLogSource_Read(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
c := &fakeDockerClient{
|
||||
logsReader: ioutil.NopCloser(strings.NewReader("Feb 13 23:31:30 ahost anid[123]: aline\n")),
|
||||
}
|
||||
src, err := NewDockerLogSource(ctx, c, "acontainer")
|
||||
if err != nil {
|
||||
t.Fatalf("NewDockerLogSource failed: %v", err)
|
||||
}
|
||||
defer src.Close()
|
||||
|
||||
s, err := src.Read(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("Read failed: %v", err)
|
||||
}
|
||||
assert.Equal(t, "Feb 13 23:31:30 ahost anid[123]: aline", s, "Read should get data from the journal entry.")
|
||||
}
|
||||
|
||||
type fakeDockerClient struct {
|
||||
logsReader io.ReadCloser
|
||||
|
||||
containerLogsCalls []string
|
||||
closeCalls int
|
||||
}
|
||||
|
||||
func (c *fakeDockerClient) ContainerLogs(ctx context.Context, containerID string, opts types.ContainerLogsOptions) (io.ReadCloser, error) {
|
||||
c.containerLogsCalls = append(c.containerLogsCalls, containerID)
|
||||
return c.logsReader, nil
|
||||
}
|
||||
|
||||
func (c *fakeDockerClient) Close() error {
|
||||
c.closeCalls++
|
||||
return nil
|
||||
}
|
@ -69,7 +69,7 @@ func (f *fileLogSourceFactory) Init(app *kingpin.Application) {
|
||||
app.Flag("postfix.logfile_path", "Path where Postfix writes log entries.").Default("/var/log/maillog").StringVar(&f.path)
|
||||
}
|
||||
|
||||
func (f *fileLogSourceFactory) New() (LogSourceCloser, error) {
|
||||
func (f *fileLogSourceFactory) New(ctx context.Context) (LogSourceCloser, error) {
|
||||
if f.path == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
@ -112,7 +112,7 @@ func (f *systemdLogSourceFactory) Init(app *kingpin.Application) {
|
||||
app.Flag("systemd.journal_path", "Path to the systemd journal").Default("").StringVar(&f.path)
|
||||
}
|
||||
|
||||
func (f *systemdLogSourceFactory) New() (LogSourceCloser, error) {
|
||||
func (f *systemdLogSourceFactory) New(ctx context.Context) (LogSourceCloser, error) {
|
||||
if !f.enable {
|
||||
return nil, nil
|
||||
}
|
||||
|
5
main.go
5
main.go
@ -13,6 +13,7 @@ import (
|
||||
|
||||
func main() {
|
||||
var (
|
||||
ctx = context.Background()
|
||||
app = kingpin.New("postfix_exporter", "Prometheus metrics exporter for postfix")
|
||||
listenAddress = app.Flag("web.listen-address", "Address to listen on for web interface and telemetry.").Default(":9154").String()
|
||||
metricsPath = app.Flag("web.telemetry-path", "Path under which to expose metrics.").Default("/metrics").String()
|
||||
@ -23,7 +24,7 @@ func main() {
|
||||
InitLogSourceFactories(app)
|
||||
kingpin.MustParse(app.Parse(os.Args[1:]))
|
||||
|
||||
logSrc, err := NewLogSourceFromFactories()
|
||||
logSrc, err := NewLogSourceFromFactories(ctx)
|
||||
if err != nil {
|
||||
log.Fatalf("Error opening log source: %s", err)
|
||||
}
|
||||
@ -53,7 +54,7 @@ func main() {
|
||||
panic(err)
|
||||
}
|
||||
})
|
||||
ctx, cancelFunc := context.WithCancel(context.Background())
|
||||
ctx, cancelFunc := context.WithCancel(ctx)
|
||||
defer cancelFunc()
|
||||
go exporter.StartMetricCollection(ctx)
|
||||
log.Print("Listening on ", *listenAddress)
|
||||
|
Loading…
Reference in New Issue
Block a user