diff --git a/cmd/dockerd/daemon.go b/cmd/dockerd/daemon.go index 6ca78142e7..50ea1d1522 100644 --- a/cmd/dockerd/daemon.go +++ b/cmd/dockerd/daemon.go @@ -478,6 +478,7 @@ func loadDaemonCliConfig(opts *daemonOptions) (*config.Config, error) { conf.Debug = opts.Debug conf.Hosts = opts.Hosts conf.LogLevel = opts.LogLevel + conf.LogFormat = opts.LogFormat if flags.Changed(FlagTLS) { conf.TLS = &opts.TLS @@ -657,6 +658,10 @@ func (cli *DaemonCli) getContainerdDaemonOpts() ([]supervisor.DaemonOpt, error) opts = append(opts, supervisor.WithLogLevel(cli.LogLevel)) } + if logFormat := cli.Config.LogFormat; logFormat != "" { + opts = append(opts, supervisor.WithLogFormat(logFormat)) + } + if !cli.CriContainerd { // CRI support in the managed daemon is currently opt-in. // @@ -867,11 +872,26 @@ func configureDaemonLogs(conf *config.Config) { } else { logrus.SetLevel(logrus.InfoLevel) } - logrus.SetFormatter(&logrus.TextFormatter{ - TimestampFormat: jsonmessage.RFC3339NanoFixed, - DisableColors: conf.RawLogs, - FullTimestamp: true, - }) + logFormat := conf.LogFormat + if logFormat == "" { + logFormat = log.TextFormat + } + var formatter logrus.Formatter + switch logFormat { + case log.JSONFormat: + formatter = &logrus.JSONFormatter{ + TimestampFormat: jsonmessage.RFC3339NanoFixed, + } + case log.TextFormat: + formatter = &logrus.TextFormatter{ + TimestampFormat: jsonmessage.RFC3339NanoFixed, + DisableColors: conf.RawLogs, + FullTimestamp: true, + } + default: + panic("unsupported log format " + logFormat) + } + logrus.SetFormatter(formatter) } func configureProxyEnv(conf *config.Config) { diff --git a/cmd/dockerd/daemon_test.go b/cmd/dockerd/daemon_test.go index 4b783edaf9..7f8d2c7383 100644 --- a/cmd/dockerd/daemon_test.go +++ b/cmd/dockerd/daemon_test.go @@ -3,6 +3,7 @@ package main import ( "testing" + "github.com/containerd/containerd/log" "github.com/docker/docker/daemon/config" "github.com/sirupsen/logrus" "github.com/spf13/pflag" @@ -155,6 +156,26 @@ func TestLoadDaemonCliConfigWithLogLevel(t *testing.T) { assert.Check(t, is.Equal("warn", loadedConfig.LogLevel)) } +func TestLoadDaemonCliConfigWithLogFormat(t *testing.T) { + tempFile := fs.NewFile(t, "config", fs.WithContent(`{"log-format": "json"}`)) + defer tempFile.Remove() + + opts := defaultOptions(t, tempFile.Path()) + loadedConfig, err := loadDaemonCliConfig(opts) + assert.NilError(t, err) + assert.Assert(t, loadedConfig != nil) + assert.Check(t, is.Equal(log.JSONFormat, loadedConfig.LogFormat)) +} + +func TestLoadDaemonCliConfigWithInvalidLogFormat(t *testing.T) { + tempFile := fs.NewFile(t, "config", fs.WithContent(`{"log-format": "foo"}`)) + defer tempFile.Remove() + + opts := defaultOptions(t, tempFile.Path()) + _, err := loadDaemonCliConfig(opts) + assert.Check(t, is.ErrorContains(err, "invalid log format: foo")) +} + func TestLoadDaemonConfigWithEmbeddedOptions(t *testing.T) { content := `{"tlscacert": "/etc/certs/ca.pem", "log-driver": "syslog"}` tempFile := fs.NewFile(t, "config", fs.WithContent(content)) diff --git a/cmd/dockerd/options.go b/cmd/dockerd/options.go index 8d0396ce5f..c272977932 100644 --- a/cmd/dockerd/options.go +++ b/cmd/dockerd/options.go @@ -1,9 +1,11 @@ package main import ( + "fmt" "os" "path/filepath" + "github.com/containerd/containerd/log" "github.com/docker/docker/daemon/config" "github.com/docker/docker/opts" "github.com/docker/docker/pkg/homedir" @@ -72,6 +74,7 @@ type daemonOptions struct { Debug bool Hosts []string LogLevel string + LogFormat string TLS bool TLSVerify bool TLSOptions *tlsconfig.Options @@ -104,6 +107,8 @@ func (o *daemonOptions) installFlags(flags *pflag.FlagSet) { flags.BoolVarP(&o.Debug, "debug", "D", false, "Enable debug mode") flags.BoolVar(&o.Validate, "validate", false, "Validate daemon configuration and exit") flags.StringVarP(&o.LogLevel, "log-level", "l", "info", `Set the logging level ("debug"|"info"|"warn"|"error"|"fatal")`) + flags.StringVar(&o.LogFormat, "log-format", log.TextFormat, + fmt.Sprintf(`Set the logging format ("%s"|"%s")`, log.TextFormat, log.JSONFormat)) flags.BoolVar(&o.TLS, FlagTLS, DefaultTLSValue, "Use TLS; implied by --tlsverify") flags.BoolVar(&o.TLSVerify, FlagTLSVerify, dockerTLSVerify || DefaultTLSValue, "Use TLS and verify the remote") diff --git a/daemon/config/config.go b/daemon/config/config.go index 9ebf2dd8cc..04edc579ec 100644 --- a/daemon/config/config.go +++ b/daemon/config/config.go @@ -185,6 +185,7 @@ type CommonConfig struct { Debug bool `json:"debug,omitempty"` Hosts []string `json:"hosts,omitempty"` LogLevel string `json:"log-level,omitempty"` + LogFormat string `json:"log-format,omitempty"` TLS *bool `json:"tls,omitempty"` TLSVerify *bool `json:"tlsverify,omitempty"` @@ -592,6 +593,16 @@ func Validate(config *Config) error { } } + // validate log-format + if logFormat := config.LogFormat; logFormat != "" { + switch logFormat { + case log.TextFormat, log.JSONFormat: + // These are valid + default: + return errors.Errorf("invalid log format: %s", logFormat) + } + } + // validate DNS for _, dns := range config.DNS { if _, err := opts.ValidateIPAddress(dns); err != nil { diff --git a/libcontainerd/supervisor/remote_daemon_options.go b/libcontainerd/supervisor/remote_daemon_options.go index 49ca83cf4c..54359cf8ad 100644 --- a/libcontainerd/supervisor/remote_daemon_options.go +++ b/libcontainerd/supervisor/remote_daemon_options.go @@ -13,6 +13,15 @@ func WithLogLevel(lvl string) DaemonOpt { } } +// WithLogFormat defines the containerd log format. +// This only makes sense if WithStartDaemon() was set to true. +func WithLogFormat(format string) DaemonOpt { + return func(r *remote) error { + r.Debug.Format = format + return nil + } +} + // WithCRIDisabled disables the CRI plugin. func WithCRIDisabled() DaemonOpt { return func(r *remote) error {