瀏覽代碼

Merge pull request #10568 from LK4D4/logging_drivers

Logging drivers
Arnaud Porterie 10 年之前
父節點
當前提交
1ff5a91007

+ 4 - 0
api/client/commands.go

@@ -1961,6 +1961,10 @@ func (cli *DockerCli) CmdLogs(args ...string) error {
 		return err
 	}
 
+	if env.GetSubEnv("HostConfig").GetSubEnv("LogConfig").Get("Type") != "json-file" {
+		return fmt.Errorf("\"logs\" command is supported only for \"json-file\" logging driver")
+	}
+
 	v := url.Values{}
 	v.Set("stdout", "1")
 	v.Set("stderr", "1")

+ 3 - 0
daemon/config.go

@@ -7,6 +7,7 @@ import (
 	"github.com/docker/docker/opts"
 	flag "github.com/docker/docker/pkg/mflag"
 	"github.com/docker/docker/pkg/ulimit"
+	"github.com/docker/docker/runconfig"
 )
 
 const (
@@ -47,6 +48,7 @@ type Config struct {
 	TrustKeyPath                string
 	Labels                      []string
 	Ulimits                     map[string]*ulimit.Ulimit
+	LogConfig                   runconfig.LogConfig
 }
 
 // InstallFlags adds command-line options to the top-level flag parser for
@@ -81,6 +83,7 @@ func (config *Config) InstallFlags() {
 	opts.LabelListVar(&config.Labels, []string{"-label"}, "Set key=value labels to the daemon")
 	config.Ulimits = make(map[string]*ulimit.Ulimit)
 	opts.UlimitMapVar(config.Ulimits, []string{"-default-ulimit"}, "Set default ulimits for containers")
+	flag.StringVar(&config.LogConfig.Type, []string{"-log-driver"}, "json-file", "Containers logging driver(json-file/none)")
 }
 
 func getDefaultNetworkMtu() int {

+ 40 - 12
daemon/container.go

@@ -21,6 +21,8 @@ import (
 
 	log "github.com/Sirupsen/logrus"
 	"github.com/docker/docker/daemon/execdriver"
+	"github.com/docker/docker/daemon/logger"
+	"github.com/docker/docker/daemon/logger/jsonfilelog"
 	"github.com/docker/docker/engine"
 	"github.com/docker/docker/image"
 	"github.com/docker/docker/links"
@@ -98,9 +100,11 @@ type Container struct {
 	VolumesRW  map[string]bool
 	hostConfig *runconfig.HostConfig
 
-	activeLinks        map[string]*links.Link
-	monitor            *containerMonitor
-	execCommands       *execStore
+	activeLinks  map[string]*links.Link
+	monitor      *containerMonitor
+	execCommands *execStore
+	// logDriver for closing
+	logDriver          logger.Logger
 	AppliedVolumesFrom map[string]struct{}
 }
 
@@ -1355,21 +1359,36 @@ func (container *Container) setupWorkingDirectory() error {
 	return nil
 }
 
-func (container *Container) startLoggingToDisk() error {
-	// Setup logging of stdout and stderr to disk
-	logPath, err := container.logPath("json")
-	if err != nil {
-		return err
+func (container *Container) startLogging() error {
+	cfg := container.hostConfig.LogConfig
+	if cfg.Type == "" {
+		cfg = container.daemon.defaultLogConfig
 	}
-	container.LogPath = logPath
+	var l logger.Logger
+	switch cfg.Type {
+	case "json-file":
+		pth, err := container.logPath("json")
+		if err != nil {
+			return err
+		}
 
-	if err := container.daemon.LogToDisk(container.stdout, container.LogPath, "stdout"); err != nil {
-		return err
+		dl, err := jsonfilelog.New(pth)
+		if err != nil {
+			return err
+		}
+		l = dl
+	case "none":
+		return nil
+	default:
+		return fmt.Errorf("Unknown logging driver: %s", cfg.Type)
 	}
 
-	if err := container.daemon.LogToDisk(container.stderr, container.LogPath, "stderr"); err != nil {
+	if copier, err := logger.NewCopier(container.ID, map[string]io.Reader{"stdout": container.StdoutPipe(), "stderr": container.StderrPipe()}, l); err != nil {
 		return err
+	} else {
+		copier.Run()
 	}
+	container.logDriver = l
 
 	return nil
 }
@@ -1470,3 +1489,12 @@ func (container *Container) getNetworkedContainer() (*Container, error) {
 func (container *Container) Stats() (*execdriver.ResourceStats, error) {
 	return container.daemon.Stats(container)
 }
+
+func (c *Container) LogDriverType() string {
+	c.Lock()
+	defer c.Unlock()
+	if c.hostConfig.LogConfig.Type == "" {
+		return c.daemon.defaultLogConfig.Type
+	}
+	return c.hostConfig.LogConfig.Type
+}

+ 36 - 34
daemon/daemon.go

@@ -89,23 +89,24 @@ func (c *contStore) List() []*Container {
 }
 
 type Daemon struct {
-	ID             string
-	repository     string
-	sysInitPath    string
-	containers     *contStore
-	execCommands   *execStore
-	graph          *graph.Graph
-	repositories   *graph.TagStore
-	idIndex        *truncindex.TruncIndex
-	sysInfo        *sysinfo.SysInfo
-	volumes        *volumes.Repository
-	eng            *engine.Engine
-	config         *Config
-	containerGraph *graphdb.Database
-	driver         graphdriver.Driver
-	execDriver     execdriver.Driver
-	trustStore     *trust.TrustStore
-	statsCollector *statsCollector
+	ID               string
+	repository       string
+	sysInitPath      string
+	containers       *contStore
+	execCommands     *execStore
+	graph            *graph.Graph
+	repositories     *graph.TagStore
+	idIndex          *truncindex.TruncIndex
+	sysInfo          *sysinfo.SysInfo
+	volumes          *volumes.Repository
+	eng              *engine.Engine
+	config           *Config
+	containerGraph   *graphdb.Database
+	driver           graphdriver.Driver
+	execDriver       execdriver.Driver
+	trustStore       *trust.TrustStore
+	statsCollector   *statsCollector
+	defaultLogConfig runconfig.LogConfig
 }
 
 // Install installs daemon capabilities to eng.
@@ -991,23 +992,24 @@ func NewDaemonFromDirectory(config *Config, eng *engine.Engine) (*Daemon, error)
 	}
 
 	daemon := &Daemon{
-		ID:             trustKey.PublicKey().KeyID(),
-		repository:     daemonRepo,
-		containers:     &contStore{s: make(map[string]*Container)},
-		execCommands:   newExecStore(),
-		graph:          g,
-		repositories:   repositories,
-		idIndex:        truncindex.NewTruncIndex([]string{}),
-		sysInfo:        sysInfo,
-		volumes:        volumes,
-		config:         config,
-		containerGraph: graph,
-		driver:         driver,
-		sysInitPath:    sysInitPath,
-		execDriver:     ed,
-		eng:            eng,
-		trustStore:     t,
-		statsCollector: newStatsCollector(1 * time.Second),
+		ID:               trustKey.PublicKey().KeyID(),
+		repository:       daemonRepo,
+		containers:       &contStore{s: make(map[string]*Container)},
+		execCommands:     newExecStore(),
+		graph:            g,
+		repositories:     repositories,
+		idIndex:          truncindex.NewTruncIndex([]string{}),
+		sysInfo:          sysInfo,
+		volumes:          volumes,
+		config:           config,
+		containerGraph:   graph,
+		driver:           driver,
+		sysInitPath:      sysInitPath,
+		execDriver:       ed,
+		eng:              eng,
+		trustStore:       t,
+		statsCollector:   newStatsCollector(1 * time.Second),
+		defaultLogConfig: config.LogConfig,
 	}
 
 	// Setup shutdown handlers

+ 8 - 0
daemon/inspect.go

@@ -62,6 +62,14 @@ func (daemon *Daemon) ContainerInspect(job *engine.Job) engine.Status {
 			container.hostConfig.Links = append(container.hostConfig.Links, fmt.Sprintf("%s:%s", child.Name, linkAlias))
 		}
 	}
+	// we need this trick to preserve empty log driver, so
+	// container will use daemon defaults even if daemon change them
+	if container.hostConfig.LogConfig.Type == "" {
+		container.hostConfig.LogConfig = daemon.defaultLogConfig
+		defer func() {
+			container.hostConfig.LogConfig = runconfig.LogConfig{}
+		}()
+	}
 
 	out.SetJson("HostConfig", container.hostConfig)
 

+ 48 - 0
daemon/logger/copier.go

@@ -0,0 +1,48 @@
+package logger
+
+import (
+	"bufio"
+	"io"
+	"time"
+
+	"github.com/Sirupsen/logrus"
+)
+
+// Copier can copy logs from specified sources to Logger and attach
+// ContainerID and Timestamp.
+// Writes are concurrent, so you need implement some sync in your logger
+type Copier struct {
+	// cid is container id for which we copying logs
+	cid string
+	// srcs is map of name -> reader pairs, for example "stdout", "stderr"
+	srcs map[string]io.Reader
+	dst  Logger
+}
+
+// NewCopier creates new Copier
+func NewCopier(cid string, srcs map[string]io.Reader, dst Logger) (*Copier, error) {
+	return &Copier{
+		cid:  cid,
+		srcs: srcs,
+		dst:  dst,
+	}, nil
+}
+
+// Run starts logs copying
+func (c *Copier) Run() {
+	for src, w := range c.srcs {
+		go c.copySrc(src, w)
+	}
+}
+
+func (c *Copier) copySrc(name string, src io.Reader) {
+	scanner := bufio.NewScanner(src)
+	for scanner.Scan() {
+		if err := c.dst.Log(&Message{ContainerID: c.cid, Line: scanner.Bytes(), Source: name, Timestamp: time.Now().UTC()}); err != nil {
+			logrus.Errorf("Failed to log msg %q for logger %s: %s", scanner.Bytes(), c.dst.Name(), err)
+		}
+	}
+	if err := scanner.Err(); err != nil {
+		logrus.Errorf("Error scanning log stream: %s", err)
+	}
+}

+ 100 - 0
daemon/logger/copier_test.go

@@ -0,0 +1,100 @@
+package logger
+
+import (
+	"bytes"
+	"encoding/json"
+	"io"
+	"testing"
+	"time"
+)
+
+type TestLoggerJSON struct {
+	*json.Encoder
+}
+
+func (l *TestLoggerJSON) Log(m *Message) error {
+	return l.Encode(m)
+}
+
+func (l *TestLoggerJSON) Close() error {
+	return nil
+}
+
+func (l *TestLoggerJSON) Name() string {
+	return "json"
+}
+
+type TestLoggerText struct {
+	*bytes.Buffer
+}
+
+func (l *TestLoggerText) Log(m *Message) error {
+	_, err := l.WriteString(m.ContainerID + " " + m.Source + " " + string(m.Line) + "\n")
+	return err
+}
+
+func (l *TestLoggerText) Close() error {
+	return nil
+}
+
+func (l *TestLoggerText) Name() string {
+	return "text"
+}
+
+func TestCopier(t *testing.T) {
+	stdoutLine := "Line that thinks that it is log line from docker stdout"
+	stderrLine := "Line that thinks that it is log line from docker stderr"
+	var stdout bytes.Buffer
+	var stderr bytes.Buffer
+	for i := 0; i < 30; i++ {
+		if _, err := stdout.WriteString(stdoutLine + "\n"); err != nil {
+			t.Fatal(err)
+		}
+		if _, err := stderr.WriteString(stderrLine + "\n"); err != nil {
+			t.Fatal(err)
+		}
+	}
+
+	var jsonBuf bytes.Buffer
+
+	jsonLog := &TestLoggerJSON{Encoder: json.NewEncoder(&jsonBuf)}
+
+	cid := "a7317399f3f857173c6179d44823594f8294678dea9999662e5c625b5a1c7657"
+	c, err := NewCopier(cid,
+		map[string]io.Reader{
+			"stdout": &stdout,
+			"stderr": &stderr,
+		},
+		jsonLog)
+	if err != nil {
+		t.Fatal(err)
+	}
+	c.Run()
+	time.Sleep(100 * time.Millisecond)
+	dec := json.NewDecoder(&jsonBuf)
+	for {
+		var msg Message
+		if err := dec.Decode(&msg); err != nil {
+			if err == io.EOF {
+				break
+			}
+			t.Fatal(err)
+		}
+		if msg.Source != "stdout" && msg.Source != "stderr" {
+			t.Fatalf("Wrong Source: %q, should be %q or %q", msg.Source, "stdout", "stderr")
+		}
+		if msg.ContainerID != cid {
+			t.Fatalf("Wrong ContainerID: %q, expected %q", msg.ContainerID, cid)
+		}
+		if msg.Source == "stdout" {
+			if string(msg.Line) != stdoutLine {
+				t.Fatalf("Wrong Line: %q, expected %q", msg.Line, stdoutLine)
+			}
+		}
+		if msg.Source == "stderr" {
+			if string(msg.Line) != stderrLine {
+				t.Fatalf("Wrong Line: %q, expected %q", msg.Line, stderrLine)
+			}
+		}
+	}
+}

+ 49 - 0
daemon/logger/jsonfilelog/jsonfilelog.go

@@ -0,0 +1,49 @@
+package jsonfilelog
+
+import (
+	"bytes"
+	"os"
+
+	"github.com/docker/docker/daemon/logger"
+	"github.com/docker/docker/pkg/jsonlog"
+)
+
+// JSONFileLogger is Logger implementation for default docker logging:
+// JSON objects to file
+type JSONFileLogger struct {
+	buf *bytes.Buffer
+	f   *os.File // store for closing
+}
+
+// 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)
+	if err != nil {
+		return nil, err
+	}
+	return &JSONFileLogger{
+		f:   log,
+		buf: bytes.NewBuffer(nil),
+	}, nil
+}
+
+// Log converts logger.Message to jsonlog.JSONLog and serializes it to file
+func (l *JSONFileLogger) Log(msg *logger.Message) error {
+	err := (&jsonlog.JSONLog{Log: string(msg.Line) + "\n", Stream: msg.Source, Created: msg.Timestamp}).MarshalJSONBuf(l.buf)
+	if err != nil {
+		return err
+	}
+	l.buf.WriteByte('\n')
+	_, err = l.buf.WriteTo(l.f)
+	return err
+}
+
+// Close closes underlying file
+func (l *JSONFileLogger) Close() error {
+	return l.f.Close()
+}
+
+// Name returns name of this logger
+func (l *JSONFileLogger) Name() string {
+	return "JSONFile"
+}

+ 78 - 0
daemon/logger/jsonfilelog/jsonfilelog_test.go

@@ -0,0 +1,78 @@
+package jsonfilelog
+
+import (
+	"io/ioutil"
+	"os"
+	"path/filepath"
+	"testing"
+	"time"
+
+	"github.com/docker/docker/daemon/logger"
+	"github.com/docker/docker/pkg/jsonlog"
+)
+
+func TestJSONFileLogger(t *testing.T) {
+	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)
+	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)
+	}
+	if err := l.Log(&logger.Message{ContainerID: cid, Line: []byte("line2"), Source: "src2"}); err != nil {
+		t.Fatal(err)
+	}
+	if err := l.Log(&logger.Message{ContainerID: cid, Line: []byte("line3"), Source: "src3"}); err != nil {
+		t.Fatal(err)
+	}
+	res, err := ioutil.ReadFile(filename)
+	if err != nil {
+		t.Fatal(err)
+	}
+	expected := `{"log":"line1\n","stream":"src1","time":"0001-01-01T00:00:00Z"}
+{"log":"line2\n","stream":"src2","time":"0001-01-01T00:00:00Z"}
+{"log":"line3\n","stream":"src3","time":"0001-01-01T00:00:00Z"}
+`
+
+	if string(res) != expected {
+		t.Fatalf("Wrong log content: %q, expected %q", res, expected)
+	}
+}
+
+func BenchmarkJSONFileLogger(b *testing.B) {
+	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)
+	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()
+	if err != nil {
+		b.Fatal(err)
+	}
+	b.SetBytes(int64(len(jsonlog)+1) * 30)
+	b.ResetTimer()
+	for i := 0; i < b.N; i++ {
+		for j := 0; j < 30; j++ {
+			if err := l.Log(msg); err != nil {
+				b.Fatal(err)
+			}
+		}
+	}
+}

+ 18 - 0
daemon/logger/logger.go

@@ -0,0 +1,18 @@
+package logger
+
+import "time"
+
+// Message is datastructure that represents record from some container
+type Message struct {
+	ContainerID string
+	Line        []byte
+	Source      string
+	Timestamp   time.Time
+}
+
+// Logger is interface for docker logging drivers
+type Logger interface {
+	Log(*Message) error
+	Name() string
+	Close() error
+}

+ 3 - 0
daemon/logs.go

@@ -44,6 +44,9 @@ func (daemon *Daemon) ContainerLogs(job *engine.Job) engine.Status {
 	if err != nil {
 		return job.Error(err)
 	}
+	if container.LogDriverType() != "json-file" {
+		return job.Errorf("\"logs\" endpoint is supported only for \"json-file\" logging driver")
+	}
 	cLog, err := container.ReadLog("json")
 	if err != nil && os.IsNotExist(err) {
 		// Legacy logs

+ 6 - 1
daemon/monitor.go

@@ -123,7 +123,7 @@ func (m *containerMonitor) Start() error {
 	for {
 		m.container.RestartCount++
 
-		if err := m.container.startLoggingToDisk(); err != nil {
+		if err := m.container.startLogging(); err != nil {
 			m.resetContainer(false)
 
 			return err
@@ -302,6 +302,11 @@ func (m *containerMonitor) resetContainer(lock bool) {
 		container.stdin, container.stdinPipe = io.Pipe()
 	}
 
+	if container.logDriver != nil {
+		container.logDriver.Close()
+		container.logDriver = nil
+	}
+
 	c := container.command.ProcessConfig.Cmd
 
 	container.command.ProcessConfig.Cmd = exec.Cmd{

+ 5 - 0
docs/man/docker-create.1.md

@@ -28,6 +28,7 @@ docker-create - Create a new container
 [**--label-file**[=*[]*]]
 [**--link**[=*[]*]]
 [**--lxc-conf**[=*[]*]]
+[**--log-driver**[=*[]*]]
 [**-m**|**--memory**[=*MEMORY*]]
 [**--memory-swap**[=*MEMORY-SWAP*]]
 [**--mac-address**[=*MAC-ADDRESS*]]
@@ -116,6 +117,10 @@ IMAGE [COMMAND] [ARG...]
 **--lxc-conf**=[]
    (lxc exec-driver only) Add custom lxc options --lxc-conf="lxc.cgroup.cpuset.cpus = 0,1"
 
+**--log-driver**="|*json-file*|*none*"
+  Logging driver for container. Default is defined by daemon `--log-driver` flag.
+  **Warning**: `docker logs` command works only for `json-file` logging driver.
+
 **-m**, **--memory**=""
    Memory limit (format: <number><optional unit>, where unit = b, k, m or g)
 

+ 2 - 0
docs/man/docker-logs.1.md

@@ -22,6 +22,8 @@ The **docker logs --follow** command combines commands **docker logs** and
 **docker attach**. It will first return all logs from the beginning and
 then continue streaming new output from the container’s stdout and stderr.
 
+**Warning**: This command works only for **json-file** logging driver.
+
 # OPTIONS
 **--help**
   Print usage statement

+ 5 - 0
docs/man/docker-run.1.md

@@ -29,6 +29,7 @@ docker-run - Run a command in a new container
 [**--label-file**[=*[]*]]
 [**--link**[=*[]*]]
 [**--lxc-conf**[=*[]*]]
+[**--log-driver**[=*[]*]]
 [**-m**|**--memory**[=*MEMORY*]]
 [**--memory-swap**[=*MEMORY-SWAP]]
 [**--mac-address**[=*MAC-ADDRESS*]]
@@ -217,6 +218,10 @@ which interface and port to use.
 **--lxc-conf**=[]
    (lxc exec-driver only) Add custom lxc options --lxc-conf="lxc.cgroup.cpuset.cpus = 0,1"
 
+**--log-driver**="|*json-file*|*none*"
+  Logging driver for container. Default is defined by daemon `--log-driver` flag.
+  **Warning**: `docker logs` command works only for `json-file` logging driver.
+
 **-m**, **--memory**=""
    Memory limit (format: <number><optional unit>, where unit = b, k, m or g)
 

+ 4 - 0
docs/man/docker.1.md

@@ -89,6 +89,10 @@ unix://[/path/to/socket] to use.
 **--label**="[]"
   Set key=value labels to the daemon (displayed in `docker info`)
 
+**--log-driver**="*json-file*|*none*"
+  Container's logging driver. Default is `default`.
+  **Warning**: `docker logs` command works only for `json-file` logging driver.
+
 **--mtu**=VALUE
   Set the containers network mtu. Default is `0`.
 

+ 10 - 1
docs/sources/reference/api/docker_remote_api_v1.18.md

@@ -161,7 +161,8 @@ Create a container
                "RestartPolicy": { "Name": "", "MaximumRetryCount": 0 },
                "NetworkMode": "bridge",
                "Devices": [],
-               "Ulimits": [{}]
+               "Ulimits": [{}],
+               "LogConfig": { "Type": "json-file", Config: {} }
             }
         }
 
@@ -255,6 +256,10 @@ Json Parameters:
   -   **Ulimits** - A list of ulimits to be set in the container, specified as
         `{ "Name": <name>, "Soft": <soft limit>, "Hard": <hard limit> }`, for example:
         `Ulimits: { "Name": "nofile", "Soft": 1024, "Hard", 2048 }}`
+  -   **LogConfig** - Logging configuration to container, format
+        `{ "Type": "<driver_name>", "Config": {"key1": "val1"}}
+        Available types: `json-file`, `none`.
+        `json-file` logging driver.
 
 Query Parameters:
 
@@ -352,6 +357,7 @@ Return low-level information on the container `id`
 				"MaximumRetryCount": 2,
 				"Name": "on-failure"
 			},
+           "LogConfig": { "Type": "json-file", Config: {} },
 			"SecurityOpt": null,
 			"VolumesFrom": null,
 			"Ulimits": [{}]
@@ -448,6 +454,9 @@ Status Codes:
 
 Get stdout and stderr logs from the container ``id``
 
+> **Note**:
+> This endpoint works only for containers with `json-file` logging driver.
+
 **Example request**:
 
        GET /containers/4fa6e0f0c678/logs?stderr=1&stdout=1&timestamps=1&follow=1&tail=10 HTTP/1.1

+ 6 - 0
docs/sources/reference/commandline/cli.md

@@ -97,6 +97,7 @@ expect an integer, and they can only be specified once.
       --ipv6=false                           Enable IPv6 networking
       -l, --log-level="info"                 Set the logging level
       --label=[]                             Set key=value labels to the daemon
+      --log-driver="json-file"               Container's logging driver (json-file/none)
       --mtu=0                                Set the containers network MTU
       -p, --pidfile="/var/run/docker.pid"    Path to use for daemon PID file
       --registry-mirror=[]                   Preferred Docker registry mirror
@@ -817,6 +818,7 @@ Creates a new container.
       -l, --label=[]             Set metadata on the container (e.g., --label=com.example.key=value)
       --label-file=[]            Read in a line delimited file of labels
       --link=[]                  Add link to another container
+      --log-driver=""            Logging driver for container
       --lxc-conf=[]              Add custom lxc options
       -m, --memory=""            Memory limit
       --mac-address=""           Container MAC address (e.g. 92:d0:c6:0a:29:33)
@@ -1447,6 +1449,9 @@ For example:
       -t, --timestamps=false    Show timestamps
       --tail="all"              Number of lines to show from the end of the logs
 
+NOTE: this command is available only for containers with `json-file` logging
+driver.
+
 The `docker logs` command batch-retrieves logs present at the time of execution.
 
 The `docker logs --follow` command will continue streaming the new output from
@@ -1722,6 +1727,7 @@ To remove an image using its digest:
       -i, --interactive=false    Keep STDIN open even if not attached
       --ipc=""                   IPC namespace to use
       --link=[]                  Add link to another container
+      --log-driver=""            Logging driver for container
       --lxc-conf=[]              Add custom lxc options
       -m, --memory=""            Memory limit
       -l, --label=[]             Set metadata on the container (e.g., --label=com.example.key=value)

+ 14 - 0
docs/sources/reference/run.md

@@ -642,6 +642,20 @@ familiar with using LXC directly.
 > you can use `--lxc-conf` to set a container's IP address, but this will not be
 > reflected in the `/etc/hosts` file.
 
+## Logging drivers (--log-driver)
+
+You can specify a different logging driver for the container than for the daemon.
+
+### Logging driver: none
+
+Disables any logging for the container. `docker logs` won't be available with
+this driver.
+
+### Log driver: json-file
+
+Default logging driver for Docker. Writes JSON messages to file. `docker logs`
+command is available only for this logging driver
+
 ## Overriding Dockerfile image defaults
 
 When a developer builds an image from a [*Dockerfile*](/reference/builder)

+ 166 - 0
integration-cli/docker_cli_daemon_test.go

@@ -11,6 +11,7 @@ import (
 	"path/filepath"
 	"strings"
 	"testing"
+	"time"
 
 	"github.com/docker/libtrust"
 )
@@ -560,3 +561,168 @@ func TestDaemonRestartRenameContainer(t *testing.T) {
 
 	logDone("daemon - rename persists through daemon restart")
 }
+
+func TestDaemonLoggingDriverDefault(t *testing.T) {
+	d := NewDaemon(t)
+
+	if err := d.StartWithBusybox(); err != nil {
+		t.Fatal(err)
+	}
+	defer d.Stop()
+
+	out, err := d.Cmd("run", "-d", "busybox", "echo", "testline")
+	if err != nil {
+		t.Fatal(out, err)
+	}
+	id := strings.TrimSpace(out)
+
+	if out, err := d.Cmd("wait", id); err != nil {
+		t.Fatal(out, err)
+	}
+	logPath := filepath.Join(d.folder, "graph", "containers", id, id+"-json.log")
+
+	if _, err := os.Stat(logPath); err != nil {
+		t.Fatal(err)
+	}
+	f, err := os.Open(logPath)
+	if err != nil {
+		t.Fatal(err)
+	}
+	var res struct {
+		Log    string    `json:log`
+		Stream string    `json:stream`
+		Time   time.Time `json:time`
+	}
+	if err := json.NewDecoder(f).Decode(&res); err != nil {
+		t.Fatal(err)
+	}
+	if res.Log != "testline\n" {
+		t.Fatalf("Unexpected log line: %q, expected: %q", res.Log, "testline\n")
+	}
+	if res.Stream != "stdout" {
+		t.Fatalf("Unexpected stream: %q, expected: %q", res.Stream, "stdout")
+	}
+	if !time.Now().After(res.Time) {
+		t.Fatalf("Log time %v in future", res.Time)
+	}
+	logDone("daemon - default 'json-file' logging driver")
+}
+
+func TestDaemonLoggingDriverDefaultOverride(t *testing.T) {
+	d := NewDaemon(t)
+
+	if err := d.StartWithBusybox(); err != nil {
+		t.Fatal(err)
+	}
+	defer d.Stop()
+
+	out, err := d.Cmd("run", "-d", "--log-driver=none", "busybox", "echo", "testline")
+	if err != nil {
+		t.Fatal(out, err)
+	}
+	id := strings.TrimSpace(out)
+
+	if out, err := d.Cmd("wait", id); err != nil {
+		t.Fatal(out, err)
+	}
+	logPath := filepath.Join(d.folder, "graph", "containers", id, id+"-json.log")
+
+	if _, err := os.Stat(logPath); err == nil || !os.IsNotExist(err) {
+		t.Fatalf("%s shouldn't exits, error on Stat: %s", logPath, err)
+	}
+	logDone("daemon - default logging driver override in run")
+}
+
+func TestDaemonLoggingDriverNone(t *testing.T) {
+	d := NewDaemon(t)
+
+	if err := d.StartWithBusybox("--log-driver=none"); err != nil {
+		t.Fatal(err)
+	}
+	defer d.Stop()
+
+	out, err := d.Cmd("run", "-d", "busybox", "echo", "testline")
+	if err != nil {
+		t.Fatal(out, err)
+	}
+	id := strings.TrimSpace(out)
+	if out, err := d.Cmd("wait", id); err != nil {
+		t.Fatal(out, err)
+	}
+
+	logPath := filepath.Join(d.folder, "graph", "containers", id, id+"-json.log")
+
+	if _, err := os.Stat(logPath); err == nil || !os.IsNotExist(err) {
+		t.Fatalf("%s shouldn't exits, error on Stat: %s", logPath, err)
+	}
+	logDone("daemon - 'none' logging driver")
+}
+
+func TestDaemonLoggingDriverNoneOverride(t *testing.T) {
+	d := NewDaemon(t)
+
+	if err := d.StartWithBusybox("--log-driver=none"); err != nil {
+		t.Fatal(err)
+	}
+	defer d.Stop()
+
+	out, err := d.Cmd("run", "-d", "--log-driver=json-file", "busybox", "echo", "testline")
+	if err != nil {
+		t.Fatal(out, err)
+	}
+	id := strings.TrimSpace(out)
+
+	if out, err := d.Cmd("wait", id); err != nil {
+		t.Fatal(out, err)
+	}
+	logPath := filepath.Join(d.folder, "graph", "containers", id, id+"-json.log")
+
+	if _, err := os.Stat(logPath); err != nil {
+		t.Fatal(err)
+	}
+	f, err := os.Open(logPath)
+	if err != nil {
+		t.Fatal(err)
+	}
+	var res struct {
+		Log    string    `json:log`
+		Stream string    `json:stream`
+		Time   time.Time `json:time`
+	}
+	if err := json.NewDecoder(f).Decode(&res); err != nil {
+		t.Fatal(err)
+	}
+	if res.Log != "testline\n" {
+		t.Fatalf("Unexpected log line: %q, expected: %q", res.Log, "testline\n")
+	}
+	if res.Stream != "stdout" {
+		t.Fatalf("Unexpected stream: %q, expected: %q", res.Stream, "stdout")
+	}
+	if !time.Now().After(res.Time) {
+		t.Fatalf("Log time %v in future", res.Time)
+	}
+	logDone("daemon - 'none' logging driver override in run")
+}
+
+func TestDaemonLoggingDriverNoneLogsError(t *testing.T) {
+	d := NewDaemon(t)
+
+	if err := d.StartWithBusybox("--log-driver=none"); err != nil {
+		t.Fatal(err)
+	}
+	defer d.Stop()
+
+	out, err := d.Cmd("run", "-d", "busybox", "echo", "testline")
+	if err != nil {
+		t.Fatal(out, err)
+	}
+	id := strings.TrimSpace(out)
+	out, err = d.Cmd("logs", id)
+	if err == nil {
+		t.Fatalf("Logs should fail with \"none\" driver")
+	}
+	if !strings.Contains(out, `\"logs\" command is supported only for \"json-file\" logging driver`) {
+		t.Fatalf("There should be error about non-json-file driver, got %s", out)
+	}
+	logDone("daemon - logs not available for non-json-file drivers")
+}

+ 1 - 0
integration/utils_test.go

@@ -191,6 +191,7 @@ func newTestEngine(t Fataler, autorestart bool, root string) *engine.Engine {
 		// otherwise NewDaemon will fail because of conflicting settings.
 		InterContainerCommunication: true,
 		TrustKeyPath:                filepath.Join(root, "key.json"),
+		LogConfig:                   runconfig.LogConfig{Type: "json-file"},
 	}
 	d, err := daemon.NewDaemon(cfg, eng)
 	if err != nil {

+ 7 - 2
runconfig/hostconfig.go

@@ -99,6 +99,11 @@ type RestartPolicy struct {
 	MaximumRetryCount int
 }
 
+type LogConfig struct {
+	Type   string
+	Config map[string]string
+}
+
 type HostConfig struct {
 	Binds           []string
 	ContainerIDFile string
@@ -125,6 +130,7 @@ type HostConfig struct {
 	SecurityOpt     []string
 	ReadonlyRootfs  bool
 	Ulimits         []*ulimit.Ulimit
+	LogConfig       LogConfig
 }
 
 // This is used by the create command when you want to set both the
@@ -189,9 +195,8 @@ func ContainerHostConfigFromJob(job *engine.Job) *HostConfig {
 	job.GetenvJson("PortBindings", &hostConfig.PortBindings)
 	job.GetenvJson("Devices", &hostConfig.Devices)
 	job.GetenvJson("RestartPolicy", &hostConfig.RestartPolicy)
-
 	job.GetenvJson("Ulimits", &hostConfig.Ulimits)
-
+	job.GetenvJson("LogConfig", &hostConfig.LogConfig)
 	hostConfig.SecurityOpt = job.GetenvList("SecurityOpt")
 	if Binds := job.GetenvList("Binds"); Binds != nil {
 		hostConfig.Binds = Binds

+ 2 - 0
runconfig/parse.go

@@ -70,6 +70,7 @@ func Parse(cmd *flag.FlagSet, args []string) (*Config, *HostConfig, *flag.FlagSe
 		flIpcMode         = cmd.String([]string{"-ipc"}, "", "IPC namespace to use")
 		flRestartPolicy   = cmd.String([]string{"-restart"}, "no", "Restart policy to apply when a container exits")
 		flReadonlyRootfs  = cmd.Bool([]string{"-read-only"}, false, "Mount the container's root filesystem as read only")
+		flLoggingDriver   = cmd.String([]string{"-log-driver"}, "", "Logging driver for container")
 	)
 
 	cmd.Var(&flAttach, []string{"a", "-attach"}, "Attach to STDIN, STDOUT or STDERR")
@@ -330,6 +331,7 @@ func Parse(cmd *flag.FlagSet, args []string) (*Config, *HostConfig, *flag.FlagSe
 		SecurityOpt:     flSecurityOpt.GetAll(),
 		ReadonlyRootfs:  *flReadonlyRootfs,
 		Ulimits:         flUlimits.GetList(),
+		LogConfig:       LogConfig{Type: *flLoggingDriver},
 	}
 
 	// When allocating stdin in attached mode, close stdin at client disconnect