| //go:build linux |
| |
| package journald // import "github.com/docker/docker/daemon/logger/journald" |
| |
| import ( |
| "fmt" |
| "strconv" |
| "sync/atomic" |
| "time" |
| "unicode" |
| |
| "github.com/coreos/go-systemd/v22/journal" |
| |
| "github.com/docker/docker/daemon/logger" |
| "github.com/docker/docker/daemon/logger/loggerutils" |
| "github.com/docker/docker/pkg/stringid" |
| ) |
| |
| const name = "journald" |
| |
| // Well-known user journal fields. |
| // https://www.freedesktop.org/software/systemd/man/systemd.journal-fields.html |
| const ( |
| fieldSyslogIdentifier = "SYSLOG_IDENTIFIER" |
| fieldSyslogTimestamp = "SYSLOG_TIMESTAMP" |
| ) |
| |
| // User journal fields used by the log driver. |
| const ( |
| fieldContainerID = "CONTAINER_ID" |
| fieldContainerIDFull = "CONTAINER_ID_FULL" |
| fieldContainerName = "CONTAINER_NAME" |
| fieldContainerTag = "CONTAINER_TAG" |
| fieldImageName = "IMAGE_NAME" |
| |
| // Fields used to serialize PLogMetaData. |
| |
| fieldPLogID = "CONTAINER_PARTIAL_ID" |
| fieldPLogOrdinal = "CONTAINER_PARTIAL_ORDINAL" |
| fieldPLogLast = "CONTAINER_PARTIAL_LAST" |
| fieldPartialMessage = "CONTAINER_PARTIAL_MESSAGE" |
| |
| fieldLogEpoch = "CONTAINER_LOG_EPOCH" |
| fieldLogOrdinal = "CONTAINER_LOG_ORDINAL" |
| ) |
| |
| var waitUntilFlushed func(*journald) error |
| |
| type journald struct { |
| // Sequence number of the most recent message sent by this instance of |
| // the log driver, starting from 1. Corollary: ordinal == 0 implies no |
| // messages have been sent by this instance. |
| ordinal uint64 // Placed first in struct to ensure 8-byte alignment for atomic ops. |
| // Epoch identifier to distinguish sequence numbers from this instance |
| // vs. other instances. |
| epoch string |
| |
| vars map[string]string // additional variables and values to send to the journal along with the log message |
| |
| closed chan struct{} |
| |
| // Overrides for unit tests. |
| |
| sendToJournal func(message string, priority journal.Priority, vars map[string]string) error |
| journalReadDir string //nolint:unused // Referenced in read.go, which has more restrictive build constraints. |
| readSyncTimeout time.Duration |
| } |
| |
| func init() { |
| if err := logger.RegisterLogDriver(name, New); err != nil { |
| panic(err) |
| } |
| if err := logger.RegisterLogOptValidator(name, validateLogOpt); err != nil { |
| panic(err) |
| } |
| } |
| |
| // sanitizeKeyMod returns the sanitized string so that it could be used in journald. |
| // In journald log, there are special requirements for fields. |
| // Fields must be composed of uppercase letters, numbers, and underscores, but must |
| // not start with an underscore. |
| func sanitizeKeyMod(s string) string { |
| n := "" |
| for _, v := range s { |
| if 'a' <= v && v <= 'z' { |
| v = unicode.ToUpper(v) |
| } else if ('Z' < v || v < 'A') && ('9' < v || v < '0') { |
| v = '_' |
| } |
| // If (n == "" && v == '_'), then we will skip as this is the beginning with '_' |
| if !(n == "" && v == '_') { |
| n += string(v) |
| } |
| } |
| return n |
| } |
| |
| // New creates a journald logger using the configuration passed in on |
| // the context. |
| func New(info logger.Info) (logger.Logger, error) { |
| if !journal.Enabled() { |
| return nil, fmt.Errorf("journald is not enabled on this host") |
| } |
| |
| return new(info) |
| } |
| |
| func new(info logger.Info) (*journald, error) { |
| // parse log tag |
| tag, err := loggerutils.ParseLogTag(info, loggerutils.DefaultTemplate) |
| if err != nil { |
| return nil, err |
| } |
| |
| epoch := stringid.GenerateRandomID() |
| |
| vars := map[string]string{ |
| fieldContainerID: info.ContainerID[:12], |
| fieldContainerIDFull: info.ContainerID, |
| fieldContainerName: info.Name(), |
| fieldContainerTag: tag, |
| fieldImageName: info.ImageName(), |
| fieldSyslogIdentifier: tag, |
| fieldLogEpoch: epoch, |
| } |
| extraAttrs, err := info.ExtraAttributes(sanitizeKeyMod) |
| if err != nil { |
| return nil, err |
| } |
| for k, v := range extraAttrs { |
| vars[k] = v |
| } |
| return &journald{ |
| epoch: epoch, |
| vars: vars, |
| closed: make(chan struct{}), |
| sendToJournal: journal.Send, |
| }, nil |
| } |
| |
| // We don't actually accept any options, but we have to supply a callback for |
| // the factory to pass the (probably empty) configuration map to. |
| func validateLogOpt(cfg map[string]string) error { |
| for key := range cfg { |
| switch key { |
| case "labels": |
| case "labels-regex": |
| case "env": |
| case "env-regex": |
| case "tag": |
| default: |
| return fmt.Errorf("unknown log opt '%s' for journald log driver", key) |
| } |
| } |
| return nil |
| } |
| |
| func (s *journald) Log(msg *logger.Message) error { |
| vars := map[string]string{} |
| for k, v := range s.vars { |
| vars[k] = v |
| } |
| if !msg.Timestamp.IsZero() { |
| vars[fieldSyslogTimestamp] = msg.Timestamp.Format(time.RFC3339Nano) |
| } |
| if msg.PLogMetaData != nil { |
| vars[fieldPLogID] = msg.PLogMetaData.ID |
| vars[fieldPLogOrdinal] = strconv.Itoa(msg.PLogMetaData.Ordinal) |
| vars[fieldPLogLast] = strconv.FormatBool(msg.PLogMetaData.Last) |
| if !msg.PLogMetaData.Last { |
| vars[fieldPartialMessage] = "true" |
| } |
| } |
| |
| line := string(msg.Line) |
| source := msg.Source |
| logger.PutMessage(msg) |
| |
| seq := atomic.AddUint64(&s.ordinal, 1) |
| vars[fieldLogOrdinal] = strconv.FormatUint(seq, 10) |
| |
| if source == "stderr" { |
| return s.sendToJournal(line, journal.PriErr, vars) |
| } |
| return s.sendToJournal(line, journal.PriInfo, vars) |
| } |
| |
| func (s *journald) Name() string { |
| return name |
| } |
| |
| func (s *journald) Close() error { |
| close(s.closed) |
| if waitUntilFlushed != nil { |
| return waitUntilFlushed(s) |
| } |
| return nil |
| } |