f1d5d0ad4d
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.
144 lines
3.6 KiB
Go
144 lines
3.6 KiB
Go
// +build !nosystemd,linux
|
|
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"time"
|
|
|
|
"github.com/alecthomas/kingpin"
|
|
"github.com/coreos/go-systemd/v22/sdjournal"
|
|
)
|
|
|
|
// timeNow is a test fake injection point.
|
|
var timeNow = time.Now
|
|
|
|
// A SystemdLogSource reads log records from the given Systemd
|
|
// journal.
|
|
type SystemdLogSource struct {
|
|
journal SystemdJournal
|
|
path string
|
|
}
|
|
|
|
// A SystemdJournal is the journal interface that sdjournal.Journal
|
|
// provides. See https://pkg.go.dev/github.com/coreos/go-systemd/sdjournal?tab=doc
|
|
type SystemdJournal interface {
|
|
io.Closer
|
|
AddMatch(match string) error
|
|
GetEntry() (*sdjournal.JournalEntry, error)
|
|
Next() (uint64, error)
|
|
SeekRealtimeUsec(usec uint64) error
|
|
Wait(timeout time.Duration) int
|
|
}
|
|
|
|
// NewSystemdLogSource returns a log source for reading Systemd
|
|
// journal entries. `unit` and `slice` provide filtering if non-empty
|
|
// (with `slice` taking precedence).
|
|
func NewSystemdLogSource(j SystemdJournal, path, unit, slice string) (*SystemdLogSource, error) {
|
|
logSrc := &SystemdLogSource{journal: j, path: path}
|
|
|
|
var err error
|
|
if slice != "" {
|
|
err = logSrc.journal.AddMatch("_SYSTEMD_SLICE=" + slice)
|
|
} else if unit != "" {
|
|
err = logSrc.journal.AddMatch("_SYSTEMD_UNIT=" + unit)
|
|
}
|
|
if err != nil {
|
|
logSrc.journal.Close()
|
|
return nil, err
|
|
}
|
|
|
|
// Start at end of journal
|
|
if err := logSrc.journal.SeekRealtimeUsec(uint64(timeNow().UnixNano() / 1000)); err != nil {
|
|
logSrc.journal.Close()
|
|
return nil, err
|
|
}
|
|
|
|
if r := logSrc.journal.Wait(1 * time.Second); r < 0 {
|
|
logSrc.journal.Close()
|
|
return nil, err
|
|
}
|
|
|
|
return logSrc, nil
|
|
}
|
|
|
|
func (s *SystemdLogSource) Close() error {
|
|
return s.journal.Close()
|
|
}
|
|
|
|
func (s *SystemdLogSource) Path() string {
|
|
return s.path
|
|
}
|
|
|
|
func (s *SystemdLogSource) Read(ctx context.Context) (string, error) {
|
|
c, err := s.journal.Next()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if c == 0 {
|
|
return "", io.EOF
|
|
}
|
|
|
|
e, err := s.journal.GetEntry()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
ts := time.Unix(0, int64(e.RealtimeTimestamp)*int64(time.Microsecond))
|
|
|
|
return fmt.Sprintf(
|
|
"%s %s %s[%s]: %s",
|
|
ts.Format(time.Stamp),
|
|
e.Fields["_HOSTNAME"],
|
|
e.Fields["SYSLOG_IDENTIFIER"],
|
|
e.Fields["_PID"],
|
|
e.Fields["MESSAGE"],
|
|
), nil
|
|
}
|
|
|
|
// A systemdLogSourceFactory is a factory that can create
|
|
// SystemdLogSources from command line flags.
|
|
type systemdLogSourceFactory struct {
|
|
enable bool
|
|
unit, slice, path string
|
|
}
|
|
|
|
func (f *systemdLogSourceFactory) Init(app *kingpin.Application) {
|
|
app.Flag("systemd.enable", "Read from the systemd journal instead of log").Default("false").BoolVar(&f.enable)
|
|
app.Flag("systemd.unit", "Name of the Postfix systemd unit.").Default("postfix.service").StringVar(&f.unit)
|
|
app.Flag("systemd.slice", "Name of the Postfix systemd slice. Overrides the systemd unit.").Default("").StringVar(&f.slice)
|
|
app.Flag("systemd.journal_path", "Path to the systemd journal").Default("").StringVar(&f.path)
|
|
}
|
|
|
|
func (f *systemdLogSourceFactory) New(ctx context.Context) (LogSourceCloser, error) {
|
|
if !f.enable {
|
|
return nil, nil
|
|
}
|
|
|
|
log.Println("Reading log events from systemd")
|
|
j, path, err := newSystemdJournal(f.path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return NewSystemdLogSource(j, path, f.unit, f.slice)
|
|
}
|
|
|
|
// newSystemdJournal creates a journal handle. It returns the handle
|
|
// and a string representation of it. If `path` is empty, it connects
|
|
// to the local journald.
|
|
func newSystemdJournal(path string) (*sdjournal.Journal, string, error) {
|
|
if path != "" {
|
|
j, err := sdjournal.NewJournalFromDir(path)
|
|
return j, path, err
|
|
}
|
|
|
|
j, err := sdjournal.NewJournal()
|
|
return j, "journald", err
|
|
}
|
|
|
|
func init() {
|
|
RegisterLogSourceFactory(&systemdLogSourceFactory{})
|
|
}
|