1
0
Fork 0
mirror of https://github.com/moby/moby.git synced 2022-11-09 12:21:53 -05:00

Merge pull request #10568 from LK4D4/logging_drivers

Logging drivers
This commit is contained in:
Arnaud Porterie 2015-03-17 09:45:58 -07:00
commit 1ff5a91007
23 changed files with 615 additions and 50 deletions

View file

@ -1961,6 +1961,10 @@ func (cli *DockerCli) CmdLogs(args ...string) error {
return err 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 := url.Values{}
v.Set("stdout", "1") v.Set("stdout", "1")
v.Set("stderr", "1") v.Set("stderr", "1")

View file

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

View file

@ -21,6 +21,8 @@ import (
log "github.com/Sirupsen/logrus" log "github.com/Sirupsen/logrus"
"github.com/docker/docker/daemon/execdriver" "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/engine"
"github.com/docker/docker/image" "github.com/docker/docker/image"
"github.com/docker/docker/links" "github.com/docker/docker/links"
@ -98,9 +100,11 @@ type Container struct {
VolumesRW map[string]bool VolumesRW map[string]bool
hostConfig *runconfig.HostConfig hostConfig *runconfig.HostConfig
activeLinks map[string]*links.Link activeLinks map[string]*links.Link
monitor *containerMonitor monitor *containerMonitor
execCommands *execStore execCommands *execStore
// logDriver for closing
logDriver logger.Logger
AppliedVolumesFrom map[string]struct{} AppliedVolumesFrom map[string]struct{}
} }
@ -1355,21 +1359,36 @@ func (container *Container) setupWorkingDirectory() error {
return nil return nil
} }
func (container *Container) startLoggingToDisk() error { func (container *Container) startLogging() error {
// Setup logging of stdout and stderr to disk cfg := container.hostConfig.LogConfig
logPath, err := container.logPath("json") if cfg.Type == "" {
if err != nil { cfg = container.daemon.defaultLogConfig
return err
} }
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 { dl, err := jsonfilelog.New(pth)
return err 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 return err
} else {
copier.Run()
} }
container.logDriver = l
return nil return nil
} }
@ -1470,3 +1489,12 @@ func (container *Container) getNetworkedContainer() (*Container, error) {
func (container *Container) Stats() (*execdriver.ResourceStats, error) { func (container *Container) Stats() (*execdriver.ResourceStats, error) {
return container.daemon.Stats(container) 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
}

View file

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

View file

@ -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)) 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) out.SetJson("HostConfig", container.hostConfig)

48
daemon/logger/copier.go Normal file
View file

@ -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)
}
}

View file

@ -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)
}
}
}
}

View file

@ -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"
}

View file

@ -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
daemon/logger/logger.go Normal file
View file

@ -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
}

View file

@ -44,6 +44,9 @@ func (daemon *Daemon) ContainerLogs(job *engine.Job) engine.Status {
if err != nil { if err != nil {
return job.Error(err) 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") cLog, err := container.ReadLog("json")
if err != nil && os.IsNotExist(err) { if err != nil && os.IsNotExist(err) {
// Legacy logs // Legacy logs

View file

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

View file

@ -28,6 +28,7 @@ docker-create - Create a new container
[**--label-file**[=*[]*]] [**--label-file**[=*[]*]]
[**--link**[=*[]*]] [**--link**[=*[]*]]
[**--lxc-conf**[=*[]*]] [**--lxc-conf**[=*[]*]]
[**--log-driver**[=*[]*]]
[**-m**|**--memory**[=*MEMORY*]] [**-m**|**--memory**[=*MEMORY*]]
[**--memory-swap**[=*MEMORY-SWAP*]] [**--memory-swap**[=*MEMORY-SWAP*]]
[**--mac-address**[=*MAC-ADDRESS*]] [**--mac-address**[=*MAC-ADDRESS*]]
@ -116,6 +117,10 @@ IMAGE [COMMAND] [ARG...]
**--lxc-conf**=[] **--lxc-conf**=[]
(lxc exec-driver only) Add custom lxc options --lxc-conf="lxc.cgroup.cpuset.cpus = 0,1" (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**="" **-m**, **--memory**=""
Memory limit (format: <number><optional unit>, where unit = b, k, m or g) Memory limit (format: <number><optional unit>, where unit = b, k, m or g)

View file

@ -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 **docker attach**. It will first return all logs from the beginning and
then continue streaming new output from the containers stdout and stderr. then continue streaming new output from the containers stdout and stderr.
**Warning**: This command works only for **json-file** logging driver.
# OPTIONS # OPTIONS
**--help** **--help**
Print usage statement Print usage statement

View file

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

View file

@ -89,6 +89,10 @@ unix://[/path/to/socket] to use.
**--label**="[]" **--label**="[]"
Set key=value labels to the daemon (displayed in `docker info`) 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 **--mtu**=VALUE
Set the containers network mtu. Default is `0`. Set the containers network mtu. Default is `0`.

View file

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

View file

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

View file

@ -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 > you can use `--lxc-conf` to set a container's IP address, but this will not be
> reflected in the `/etc/hosts` file. > 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 ## Overriding Dockerfile image defaults
When a developer builds an image from a [*Dockerfile*](/reference/builder) When a developer builds an image from a [*Dockerfile*](/reference/builder)

View file

@ -11,6 +11,7 @@ import (
"path/filepath" "path/filepath"
"strings" "strings"
"testing" "testing"
"time"
"github.com/docker/libtrust" "github.com/docker/libtrust"
) )
@ -560,3 +561,168 @@ func TestDaemonRestartRenameContainer(t *testing.T) {
logDone("daemon - rename persists through daemon restart") 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")
}

View file

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

View file

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

View file

@ -70,6 +70,7 @@ func Parse(cmd *flag.FlagSet, args []string) (*Config, *HostConfig, *flag.FlagSe
flIpcMode = cmd.String([]string{"-ipc"}, "", "IPC namespace to use") flIpcMode = cmd.String([]string{"-ipc"}, "", "IPC namespace to use")
flRestartPolicy = cmd.String([]string{"-restart"}, "no", "Restart policy to apply when a container exits") 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") 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") 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(), SecurityOpt: flSecurityOpt.GetAll(),
ReadonlyRootfs: *flReadonlyRootfs, ReadonlyRootfs: *flReadonlyRootfs,
Ulimits: flUlimits.GetList(), Ulimits: flUlimits.GetList(),
LogConfig: LogConfig{Type: *flLoggingDriver},
} }
// When allocating stdin in attached mode, close stdin at client disconnect // When allocating stdin in attached mode, close stdin at client disconnect