daemon: Logging drivers refactoring

- noplog driver pkg for '--log-driver=none' (null object pattern)
- centralized factory for log drivers (instead of case/switch)
- logging drivers registers themselves to factory upon import
  (easy plug/unplug of drivers in daemon/logdrivers.go)
- daemon now doesn't start with an invalid log driver
- Name() method of loggers is actually now their cli names (made it useful)
- generalized Read() logic, made it unsupported except json-file (preserves
  existing behavior)

Spotted some duplication code around processing of legacy json-file
format, didn't touch that and refactored in both places.

Signed-off-by: Ahmet Alp Balkan <ahmetalpbalkan@gmail.com>
This commit is contained in:
Ahmet Alp Balkan 2015-04-08 21:23:30 -07:00
parent c9821d8dd6
commit 3a8728b431
12 changed files with 225 additions and 118 deletions

View file

@ -37,8 +37,8 @@ func (cli *DockerCli) CmdLogs(args ...string) error {
return err
}
if c.HostConfig.LogConfig.Type != "json-file" {
return fmt.Errorf("\"logs\" command is supported only for \"json-file\" logging driver")
if logType := c.HostConfig.LogConfig.Type; logType != "json-file" {
return fmt.Errorf("\"logs\" command is supported only for \"json-file\" logging driver (got: %s)", logType)
}
v := url.Values{}

View file

@ -22,9 +22,7 @@ import (
"github.com/Sirupsen/logrus"
"github.com/docker/docker/daemon/execdriver"
"github.com/docker/docker/daemon/logger"
"github.com/docker/docker/daemon/logger/journald"
"github.com/docker/docker/daemon/logger/jsonfilelog"
"github.com/docker/docker/daemon/logger/syslog"
"github.com/docker/docker/daemon/network"
"github.com/docker/docker/daemon/networkdriver/bridge"
"github.com/docker/docker/image"
@ -947,18 +945,6 @@ func (container *Container) Unmount() error {
return container.daemon.Unmount(container)
}
func (container *Container) logPath(name string) (string, error) {
return container.GetRootResourcePath(fmt.Sprintf("%s-%s.log", container.ID, name))
}
func (container *Container) ReadLog(name string) (io.Reader, error) {
pth, err := container.logPath(name)
if err != nil {
return nil, err
}
return os.Open(pth)
}
func (container *Container) hostConfigPath() (string, error) {
return container.GetRootResourcePath("hostconfig.json")
}
@ -1445,41 +1431,45 @@ func (container *Container) setupWorkingDirectory() error {
return nil
}
func (container *Container) startLogging() error {
func (container *Container) getLogConfig() runconfig.LogConfig {
cfg := container.hostConfig.LogConfig
if cfg.Type == "" {
cfg = container.daemon.defaultLogConfig
if cfg.Type != "" { // container has log driver configured
return cfg
}
var l logger.Logger
switch cfg.Type {
case "json-file":
pth, err := container.logPath("json")
if err != nil {
return err
}
container.LogPath = pth
// Use daemon's default log config for containers
return container.daemon.defaultLogConfig
}
dl, err := jsonfilelog.New(pth)
func (container *Container) getLogger() (logger.Logger, error) {
cfg := container.getLogConfig()
c, err := logger.GetLogDriver(cfg.Type)
if err != nil {
return nil, fmt.Errorf("Failed to get logging factory: %v", err)
}
ctx := logger.Context{
ContainerID: container.ID,
ContainerName: container.Name,
}
// Set logging file for "json-logger"
if cfg.Type == jsonfilelog.Name {
ctx.LogPath, err = container.GetRootResourcePath(fmt.Sprintf("%s-json.log", container.ID))
if err != nil {
return err
return nil, err
}
l = dl
case "syslog":
dl, err := syslog.New(container.ID[:12])
if err != nil {
return err
}
l = dl
case "journald":
dl, err := journald.New(container.ID, container.Name)
if err != nil {
return err
}
l = dl
case "none":
return nil
default:
return fmt.Errorf("Unknown logging driver: %s", cfg.Type)
}
return c(ctx)
}
func (container *Container) startLogging() error {
cfg := container.getLogConfig()
if cfg.Type == "none" {
return nil // do not start logging routines
}
l, err := container.getLogger()
if err != nil {
return fmt.Errorf("Failed to initialize logging driver: %v", err)
}
copier, err := logger.NewCopier(container.ID, map[string]io.Reader{"stdout": container.StdoutPipe(), "stderr": container.StderrPipe()}, l)
@ -1490,6 +1480,11 @@ func (container *Container) startLogging() error {
copier.Run()
container.logDriver = l
// set LogPath field only for json-file logdriver
if jl, ok := l.(*jsonfilelog.JSONFileLogger); ok {
container.LogPath = jl.LogPath()
}
return nil
}
@ -1663,28 +1658,13 @@ func (c *Container) Attach(stdin io.ReadCloser, stdout io.Writer, stderr io.Writ
func (c *Container) AttachWithLogs(stdin io.ReadCloser, stdout, stderr io.Writer, logs, stream bool) error {
if logs {
cLog, err := c.ReadLog("json")
if err != nil && os.IsNotExist(err) {
// Legacy logs
logrus.Debugf("Old logs format")
if stdout != nil {
cLog, err := c.ReadLog("stdout")
if err != nil {
logrus.Errorf("Error reading logs (stdout): %s", err)
} else if _, err := io.Copy(stdout, cLog); err != nil {
logrus.Errorf("Error streaming logs (stdout): %s", err)
}
}
if stderr != nil {
cLog, err := c.ReadLog("stderr")
if err != nil {
logrus.Errorf("Error reading logs (stderr): %s", err)
} else if _, err := io.Copy(stderr, cLog); err != nil {
logrus.Errorf("Error streaming logs (stderr): %s", err)
}
}
} else if err != nil {
logrus.Errorf("Error reading logs (json): %s", err)
logDriver, err := c.getLogger()
cLog, err := logDriver.GetReader()
if err != nil {
logrus.Errorf("Error reading logs: %s", err)
} else if c.LogDriverType() != jsonfilelog.Name {
logrus.Errorf("Reading logs not implemented for driver %s", c.LogDriverType())
} else {
dec := json.NewDecoder(cLog)
for {

View file

@ -24,6 +24,7 @@ import (
"github.com/docker/docker/daemon/execdriver/execdrivers"
"github.com/docker/docker/daemon/graphdriver"
_ "github.com/docker/docker/daemon/graphdriver/vfs"
"github.com/docker/docker/daemon/logger"
"github.com/docker/docker/daemon/network"
"github.com/docker/docker/daemon/networkdriver/bridge"
"github.com/docker/docker/graph"
@ -798,6 +799,14 @@ func NewDaemon(config *Config, registryService *registry.Service) (daemon *Daemo
}
}()
// Verify logging driver type
if config.LogConfig.Type != "none" {
if _, err := logger.GetLogDriver(config.LogConfig.Type); err != nil {
return nil, fmt.Errorf("error finding the logging driver: %v", err)
}
}
logrus.Debugf("Using default logging driver %s", config.LogConfig.Type)
if config.EnableSelinuxSupport {
if selinuxEnabled() {
// As Docker on btrfs and SELinux are incompatible at present, error on both being enabled

9
daemon/logdrivers.go Normal file
View file

@ -0,0 +1,9 @@
package daemon
// Importing packages here only to make sure their init gets called and
// therefore they register themselves to the logdriver factory.
import (
_ "github.com/docker/docker/daemon/logger/journald"
_ "github.com/docker/docker/daemon/logger/jsonfilelog"
_ "github.com/docker/docker/daemon/logger/syslog"
)

View file

@ -3,6 +3,7 @@ package logger
import (
"bytes"
"encoding/json"
"errors"
"io"
"testing"
"time"
@ -12,16 +13,14 @@ type TestLoggerJSON struct {
*json.Encoder
}
func (l *TestLoggerJSON) Log(m *Message) error {
return l.Encode(m)
}
func (l *TestLoggerJSON) Log(m *Message) error { return l.Encode(m) }
func (l *TestLoggerJSON) Close() error {
return nil
}
func (l *TestLoggerJSON) Close() error { return nil }
func (l *TestLoggerJSON) Name() string {
return "json"
func (l *TestLoggerJSON) Name() string { return "json" }
func (l *TestLoggerJSON) GetReader() (io.Reader, error) {
return nil, errors.New("not used in the test")
}
type TestLoggerText struct {
@ -33,12 +32,12 @@ func (l *TestLoggerText) Log(m *Message) error {
return err
}
func (l *TestLoggerText) Close() error {
return nil
}
func (l *TestLoggerText) Close() error { return nil }
func (l *TestLoggerText) Name() string {
return "text"
func (l *TestLoggerText) Name() string { return "text" }
func (l *TestLoggerText) GetReader() (io.Reader, error) {
return nil, errors.New("not used in the test")
}
func TestCopier(t *testing.T) {

56
daemon/logger/factory.go Normal file
View file

@ -0,0 +1,56 @@
package logger
import (
"fmt"
"sync"
)
// Creator is a method that builds a logging driver instance with given context
type Creator func(Context) (Logger, error)
// Context provides enough information for a logging driver to do its function
type Context struct {
ContainerID string
ContainerName string
LogPath string
}
type logdriverFactory struct {
registry map[string]Creator
m sync.Mutex
}
func (lf *logdriverFactory) register(name string, c Creator) error {
lf.m.Lock()
defer lf.m.Unlock()
if _, ok := lf.registry[name]; ok {
return fmt.Errorf("logger: log driver named '%s' is already registered", name)
}
lf.registry[name] = c
return nil
}
func (lf *logdriverFactory) get(name string) (Creator, error) {
lf.m.Lock()
defer lf.m.Unlock()
c, ok := lf.registry[name]
if !ok {
return c, fmt.Errorf("logger: no log driver named '%s' is registered", name)
}
return c, nil
}
var factory = &logdriverFactory{registry: make(map[string]Creator)} // global factory instance
// RegisterLogDriver registers the given logging driver builder with given logging
// driver name.
func RegisterLogDriver(name string, c Creator) error {
return factory.register(name, c)
}
// GetLogDriver provides the logging driver builder for a logging driver name.
func GetLogDriver(name string) (Creator, error) {
return factory.get(name)
}

View file

@ -2,27 +2,38 @@ package journald
import (
"fmt"
"io"
"github.com/Sirupsen/logrus"
"github.com/coreos/go-systemd/journal"
"github.com/docker/docker/daemon/logger"
)
const name = "journald"
type Journald struct {
Jmap map[string]string
}
func New(id string, name string) (logger.Logger, error) {
func init() {
if err := logger.RegisterLogDriver(name, New); err != nil {
logrus.Fatal(err)
}
}
func New(ctx logger.Context) (logger.Logger, error) {
if !journal.Enabled() {
return nil, fmt.Errorf("journald is not enabled on this host")
}
// Strip a leading slash so that people can search for
// CONTAINER_NAME=foo rather than CONTAINER_NAME=/foo.
name := ctx.ContainerName
if name[0] == '/' {
name = name[1:]
}
jmap := map[string]string{
"CONTAINER_ID": id[:12],
"CONTAINER_ID_FULL": id,
"CONTAINER_ID": ctx.ContainerID[:12],
"CONTAINER_ID_FULL": ctx.ContainerID,
"CONTAINER_NAME": name}
return &Journald{Jmap: jmap}, nil
}
@ -39,5 +50,9 @@ func (s *Journald) Close() error {
}
func (s *Journald) Name() string {
return "Journald"
return name
}
func (s *Journald) GetReader() (io.Reader, error) {
return nil, logger.ReadLogsNotSupported
}

View file

@ -2,31 +2,46 @@ package jsonfilelog
import (
"bytes"
"io"
"os"
"sync"
"github.com/Sirupsen/logrus"
"github.com/docker/docker/daemon/logger"
"github.com/docker/docker/pkg/jsonlog"
"github.com/docker/docker/pkg/timeutils"
)
const (
Name = "json-file"
)
// JSONFileLogger is Logger implementation for default docker logging:
// JSON objects to file
type JSONFileLogger struct {
buf *bytes.Buffer
f *os.File // store for closing
mu sync.Mutex // protects buffer
ctx logger.Context
}
func init() {
if err := logger.RegisterLogDriver(Name, New); err != nil {
logrus.Fatal(err)
}
}
// New creates new JSONFileLogger which writes to filename
func New(filename string) (logger.Logger, error) {
log, err := os.OpenFile(filename, os.O_RDWR|os.O_APPEND|os.O_CREATE, 0600)
func New(ctx logger.Context) (logger.Logger, error) {
log, err := os.OpenFile(ctx.LogPath, os.O_RDWR|os.O_APPEND|os.O_CREATE, 0600)
if err != nil {
return nil, err
}
return &JSONFileLogger{
f: log,
buf: bytes.NewBuffer(nil),
ctx: ctx,
}, nil
}
@ -34,6 +49,7 @@ func New(filename string) (logger.Logger, error) {
func (l *JSONFileLogger) Log(msg *logger.Message) error {
l.mu.Lock()
defer l.mu.Unlock()
timestamp, err := timeutils.FastMarshalJSON(msg.Timestamp)
if err != nil {
return err
@ -52,6 +68,14 @@ func (l *JSONFileLogger) Log(msg *logger.Message) error {
return nil
}
func (l *JSONFileLogger) GetReader() (io.Reader, error) {
return os.Open(l.ctx.LogPath)
}
func (l *JSONFileLogger) LogPath() string {
return l.ctx.LogPath
}
// Close closes underlying file
func (l *JSONFileLogger) Close() error {
return l.f.Close()
@ -59,5 +83,5 @@ func (l *JSONFileLogger) Close() error {
// Name returns name of this logger
func (l *JSONFileLogger) Name() string {
return "JSONFile"
return Name
}

View file

@ -12,18 +12,22 @@ import (
)
func TestJSONFileLogger(t *testing.T) {
cid := "a7317399f3f857173c6179d44823594f8294678dea9999662e5c625b5a1c7657"
tmp, err := ioutil.TempDir("", "docker-logger-")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tmp)
filename := filepath.Join(tmp, "container.log")
l, err := New(filename)
l, err := New(logger.Context{
ContainerID: cid,
LogPath: filename,
})
if err != nil {
t.Fatal(err)
}
defer l.Close()
cid := "a7317399f3f857173c6179d44823594f8294678dea9999662e5c625b5a1c7657"
if err := l.Log(&logger.Message{ContainerID: cid, Line: []byte("line1"), Source: "src1"}); err != nil {
t.Fatal(err)
}
@ -48,18 +52,22 @@ func TestJSONFileLogger(t *testing.T) {
}
func BenchmarkJSONFileLogger(b *testing.B) {
cid := "a7317399f3f857173c6179d44823594f8294678dea9999662e5c625b5a1c7657"
tmp, err := ioutil.TempDir("", "docker-logger-")
if err != nil {
b.Fatal(err)
}
defer os.RemoveAll(tmp)
filename := filepath.Join(tmp, "container.log")
l, err := New(filename)
l, err := New(logger.Context{
ContainerID: cid,
LogPath: filename,
})
if err != nil {
b.Fatal(err)
}
defer l.Close()
cid := "a7317399f3f857173c6179d44823594f8294678dea9999662e5c625b5a1c7657"
testLine := "Line that thinks that it is log line from docker\n"
msg := &logger.Message{ContainerID: cid, Line: []byte(testLine), Source: "stderr", Timestamp: time.Now().UTC()}
jsonlog, err := (&jsonlog.JSONLog{Log: string(msg.Line) + "\n", Stream: msg.Source, Created: msg.Timestamp}).MarshalJSON()

View file

@ -1,6 +1,12 @@
package logger
import "time"
import (
"errors"
"io"
"time"
)
var ReadLogsNotSupported = errors.New("configured logging reader does not support reading")
// Message is datastructure that represents record from some container
type Message struct {
@ -15,4 +21,5 @@ type Logger interface {
Log(*Message) error
Name() string
Close() error
GetReader() (io.Reader, error)
}

View file

@ -2,22 +2,34 @@ package syslog
import (
"fmt"
"io"
"log/syslog"
"os"
"path"
"github.com/Sirupsen/logrus"
"github.com/docker/docker/daemon/logger"
)
const name = "syslog"
type Syslog struct {
writer *syslog.Writer
}
func New(tag string) (logger.Logger, error) {
func init() {
if err := logger.RegisterLogDriver(name, New); err != nil {
logrus.Fatal(err)
}
}
func New(ctx logger.Context) (logger.Logger, error) {
tag := ctx.ContainerID[:12]
log, err := syslog.New(syslog.LOG_DAEMON, fmt.Sprintf("%s/%s", path.Base(os.Args[0]), tag))
if err != nil {
return nil, err
}
return &Syslog{
writer: log,
}, nil
@ -35,5 +47,9 @@ func (s *Syslog) Close() error {
}
func (s *Syslog) Name() string {
return "Syslog"
return name
}
func (s *Syslog) GetReader() (io.Reader, error) {
return nil, logger.ReadLogsNotSupported
}

View file

@ -11,6 +11,7 @@ import (
"time"
"github.com/Sirupsen/logrus"
"github.com/docker/docker/daemon/logger/jsonfilelog"
"github.com/docker/docker/pkg/jsonlog"
"github.com/docker/docker/pkg/stdcopy"
"github.com/docker/docker/pkg/tailfile"
@ -56,32 +57,15 @@ func (daemon *Daemon) ContainerLogs(name string, config *ContainerLogsConfig) er
errStream = outStream
}
if container.LogDriverType() != "json-file" {
if container.LogDriverType() != jsonfilelog.Name {
return fmt.Errorf("\"logs\" endpoint is supported only for \"json-file\" logging driver")
}
cLog, err := container.ReadLog("json")
if err != nil && os.IsNotExist(err) {
// Legacy logs
logrus.Debugf("Old logs format")
if config.UseStdout {
cLog, err := container.ReadLog("stdout")
if err != nil {
logrus.Errorf("Error reading logs (stdout): %s", err)
} else if _, err := io.Copy(outStream, cLog); err != nil {
logrus.Errorf("Error streaming logs (stdout): %s", err)
}
}
if config.UseStderr {
cLog, err := container.ReadLog("stderr")
if err != nil {
logrus.Errorf("Error reading logs (stderr): %s", err)
} else if _, err := io.Copy(errStream, cLog); err != nil {
logrus.Errorf("Error streaming logs (stderr): %s", err)
}
}
} else if err != nil {
logrus.Errorf("Error reading logs (json): %s", err)
logDriver, err := container.getLogger()
cLog, err := logDriver.GetReader()
if err != nil {
logrus.Errorf("Error reading logs: %s", err)
} else {
// json-file driver
if config.Tail != "all" {
var err error
lines, err = strconv.Atoi(config.Tail)