From 656cdbb0e96a1f8531b118caedd8e9b3d281c201 Mon Sep 17 00:00:00 2001 From: Daniel Dao Date: Tue, 11 Aug 2015 11:27:19 +0000 Subject: [PATCH 1/6] add env and labels to log context Signed-off-by: Daniel Dao --- daemon/container.go | 2 ++ daemon/logger/context.go | 2 ++ 2 files changed, 4 insertions(+) diff --git a/daemon/container.go b/daemon/container.go index 66ef0aac0e..53807f90e6 100644 --- a/daemon/container.go +++ b/daemon/container.go @@ -721,6 +721,8 @@ func (container *Container) getLogger() (logger.Logger, error) { ContainerImageID: container.ImageID, ContainerImageName: container.Config.Image, ContainerCreated: container.Created, + ContainerEnv: container.Config.Env, + ContainerLabels: container.Config.Labels, } // Set logging file for "json-logger" diff --git a/daemon/logger/context.go b/daemon/logger/context.go index 3526d563e8..42820acb9c 100644 --- a/daemon/logger/context.go +++ b/daemon/logger/context.go @@ -17,6 +17,8 @@ type Context struct { ContainerImageID string ContainerImageName string ContainerCreated time.Time + ContainerEnv []string + ContainerLabels map[string]string LogPath string } From 5794a0190d7505e998116f1321ae39b001d0e710 Mon Sep 17 00:00:00 2001 From: Daniel Dao Date: Sun, 4 Oct 2015 21:03:11 +0000 Subject: [PATCH 2/6] add labels/env log option for gelf this allows gelf logger to collect extra metadata from containers with `--log-opt labels=label1,label2 --log-opt env=env1,env2` Additional log field will be prefixed with `_` as per gelf protocol https://www.graylog.org/resources/gelf/ Signed-off-by: Daniel Dao --- daemon/logger/context.go | 38 ++++++++++++++++++++++ daemon/logger/gelf/gelf.go | 66 +++++++++++++++++--------------------- 2 files changed, 68 insertions(+), 36 deletions(-) diff --git a/daemon/logger/context.go b/daemon/logger/context.go index 42820acb9c..582117af93 100644 --- a/daemon/logger/context.go +++ b/daemon/logger/context.go @@ -22,6 +22,44 @@ type Context struct { LogPath string } +// ExtraAttributes returns the user-defined extra attributes (labels, +// environment variables) in key-value format. This can be used by log drivers +// that support metadata to add more context to a log. +func (ctx *Context) ExtraAttributes(keyMod func(string) string) map[string]string { + extra := make(map[string]string) + labels, ok := ctx.Config["labels"] + if ok && len(labels) > 0 { + for _, l := range strings.Split(labels, ",") { + if v, ok := ctx.ContainerLabels[l]; ok { + if keyMod != nil { + l = keyMod(l) + } + extra[l] = v + } + } + } + + env, ok := ctx.Config["env"] + if ok && len(env) > 0 { + envMapping := make(map[string]string) + for _, e := range ctx.ContainerEnv { + if kv := strings.SplitN(e, "=", 2); len(kv) == 2 { + envMapping[kv[0]] = kv[1] + } + } + for _, l := range strings.Split(env, ",") { + if v, ok := envMapping[l]; ok { + if keyMod != nil { + l = keyMod(l) + } + extra[l] = v + } + } + } + + return extra +} + // Hostname returns the hostname from the underlying OS. func (ctx *Context) Hostname() (string, error) { hostname, err := os.Hostname() diff --git a/daemon/logger/gelf/gelf.go b/daemon/logger/gelf/gelf.go index 6a62672d72..2387028913 100644 --- a/daemon/logger/gelf/gelf.go +++ b/daemon/logger/gelf/gelf.go @@ -21,20 +21,10 @@ import ( const name = "gelf" type gelfLogger struct { - writer *gelf.Writer - ctx logger.Context - fields gelfFields -} - -type gelfFields struct { - hostname string - containerID string - containerName string - imageID string - imageName string - command string - tag string - created time.Time + writer *gelf.Writer + ctx logger.Context + hostname string + extra map[string]interface{} } func init() { @@ -71,15 +61,24 @@ func New(ctx logger.Context) (logger.Logger, error) { return nil, err } - fields := gelfFields{ - hostname: hostname, - containerID: ctx.ContainerID, - containerName: string(containerName), - imageID: ctx.ContainerImageID, - imageName: ctx.ContainerImageName, - command: ctx.Command(), - tag: tag, - created: ctx.ContainerCreated, + extra := map[string]interface{}{ + "_container_id": ctx.ContainerID, + "_container_name": string(containerName), + "_image_id": ctx.ContainerImageID, + "_image_name": ctx.ContainerImageName, + "_command": ctx.Command(), + "_tag": tag, + "_created": ctx.ContainerCreated, + } + + extraAttrs := ctx.ExtraAttributes(func(key string) string { + if key[0] == '_' { + return key + } + return "_" + key + }) + for k, v := range extraAttrs { + extra[k] = v } // create new gelfWriter @@ -89,9 +88,10 @@ func New(ctx logger.Context) (logger.Logger, error) { } return &gelfLogger{ - writer: gelfWriter, - ctx: ctx, - fields: fields, + writer: gelfWriter, + ctx: ctx, + hostname: hostname, + extra: extra, }, nil } @@ -106,19 +106,11 @@ func (s *gelfLogger) Log(msg *logger.Message) error { m := gelf.Message{ Version: "1.1", - Host: s.fields.hostname, + Host: s.hostname, Short: string(short), TimeUnix: float64(msg.Timestamp.UnixNano()/int64(time.Millisecond)) / 1000.0, Level: level, - Extra: map[string]interface{}{ - "_container_id": s.fields.containerID, - "_container_name": s.fields.containerName, - "_image_id": s.fields.imageID, - "_image_name": s.fields.imageName, - "_command": s.fields.command, - "_tag": s.fields.tag, - "_created": s.fields.created, - }, + Extra: s.extra, } if err := s.writer.WriteMessage(&m); err != nil { @@ -143,6 +135,8 @@ func ValidateLogOpt(cfg map[string]string) error { case "gelf-address": case "gelf-tag": case "tag": + case "labels": + case "env": default: return fmt.Errorf("unknown log opt '%s' for gelf log driver", key) } From 4cc8490283fdc8315f595be3e32951929e65bb40 Mon Sep 17 00:00:00 2001 From: Daniel Dao Date: Sun, 4 Oct 2015 21:05:43 +0000 Subject: [PATCH 3/6] add labels/env log option for fluentd this allows fluentd logger to collect extra metadata from containers with `--log-opt labels=label1,label2 --log-opt env=env1,env2` Signed-off-by: Daniel Dao --- daemon/logger/fluentd/fluentd.go | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/daemon/logger/fluentd/fluentd.go b/daemon/logger/fluentd/fluentd.go index 974c2e21d9..51e63d5236 100644 --- a/daemon/logger/fluentd/fluentd.go +++ b/daemon/logger/fluentd/fluentd.go @@ -20,6 +20,7 @@ type fluentd struct { containerID string containerName string writer *fluent.Fluent + extra map[string]string } const ( @@ -51,9 +52,8 @@ func New(ctx logger.Context) (logger.Logger, error) { if err != nil { return nil, err } - - logrus.Debugf("logging driver fluentd configured for container:%s, host:%s, port:%d, tag:%s.", ctx.ContainerID, host, port, tag) - + extra := ctx.ExtraAttributes(nil) + logrus.Debugf("logging driver fluentd configured for container:%s, host:%s, port:%d, tag:%s, extra:%v.", ctx.ContainerID, host, port, tag, extra) // logger tries to recoonect 2**32 - 1 times // failed (and panic) after 204 years [ 1.5 ** (2**32 - 1) - 1 seconds] log, err := fluent.New(fluent.Config{FluentPort: port, FluentHost: host, RetryWait: 1000, MaxRetry: math.MaxInt32}) @@ -65,6 +65,7 @@ func New(ctx logger.Context) (logger.Logger, error) { containerID: ctx.ContainerID, containerName: ctx.ContainerName, writer: log, + extra: extra, }, nil } @@ -75,6 +76,9 @@ func (f *fluentd) Log(msg *logger.Message) error { "source": msg.Source, "log": string(msg.Line), } + for k, v := range f.extra { + data[k] = v + } // fluent-logger-golang buffers logs from failures and disconnections, // and these are transferred again automatically. return f.writer.PostWithTime(f.tag, msg.Timestamp, data) @@ -95,6 +99,8 @@ func ValidateLogOpt(cfg map[string]string) error { case "fluentd-address": case "fluentd-tag": case "tag": + case "labels": + case "env": default: return fmt.Errorf("unknown log opt '%s' for fluentd log driver", key) } From 11a24f19c2da88b6c3b50114863f24c06c5ce2fd Mon Sep 17 00:00:00 2001 From: Daniel Dao Date: Sun, 4 Oct 2015 21:06:19 +0000 Subject: [PATCH 4/6] add labels/env log option for journald this allows journald logger to collect extra metadata from containers with `--log-opt labels=label1,label2 --log-opt env=env1,env2` Signed-off-by: Daniel Dao --- daemon/logger/journald/journald.go | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/daemon/logger/journald/journald.go b/daemon/logger/journald/journald.go index bc4302fb3b..c729b56d19 100644 --- a/daemon/logger/journald/journald.go +++ b/daemon/logger/journald/journald.go @@ -6,6 +6,7 @@ package journald import ( "fmt" + "strings" "sync" "github.com/Sirupsen/logrus" @@ -46,10 +47,16 @@ func New(ctx logger.Context) (logger.Logger, error) { if name[0] == '/' { name = name[1:] } + vars := map[string]string{ "CONTAINER_ID": ctx.ContainerID[:12], "CONTAINER_ID_FULL": ctx.ContainerID, - "CONTAINER_NAME": name} + "CONTAINER_NAME": name, + } + extraAttrs := ctx.ExtraAttributes(strings.ToTitle) + for k, v := range extraAttrs { + vars[k] = v + } return &journald{vars: vars, readers: readerList{readers: make(map[*logger.LogWatcher]*logger.LogWatcher)}}, nil } @@ -58,6 +65,8 @@ func New(ctx logger.Context) (logger.Logger, error) { func validateLogOpt(cfg map[string]string) error { for key := range cfg { switch key { + case "labels": + case "env": default: return fmt.Errorf("unknown log opt '%s' for journald log driver", key) } From 0083f6e984894b4d3697c1ae63547c07eea697af Mon Sep 17 00:00:00 2001 From: Daniel Dao Date: Sun, 4 Oct 2015 21:07:09 +0000 Subject: [PATCH 5/6] add labels/env log option for jsonfile this allows jsonfile logger to collect extra metadata from containers with `--log-opt labels=label1,label2 --log-opt env=env1,env2`. Extra attributes are saved into `attrs` attributes for each log data. Signed-off-by: Daniel Dao --- daemon/logger/jsonfilelog/jsonfilelog.go | 21 +++++++- daemon/logger/jsonfilelog/jsonfilelog_test.go | 50 +++++++++++++++++++ pkg/jsonlog/jsonlogbytes.go | 13 +++++ pkg/jsonlog/jsonlogbytes_test.go | 2 + 4 files changed, 85 insertions(+), 1 deletion(-) diff --git a/daemon/logger/jsonfilelog/jsonfilelog.go b/daemon/logger/jsonfilelog/jsonfilelog.go index 38ff7171b3..490e84ca9c 100644 --- a/daemon/logger/jsonfilelog/jsonfilelog.go +++ b/daemon/logger/jsonfilelog/jsonfilelog.go @@ -41,6 +41,7 @@ type JSONFileLogger struct { ctx logger.Context readers map[*logger.LogWatcher]struct{} // stores the active log followers notifyRotate *pubsub.Publisher + extra []byte // json-encoded extra attributes } func init() { @@ -77,6 +78,16 @@ func New(ctx logger.Context) (logger.Logger, error) { return nil, fmt.Errorf("max-file cannot be less than 1") } } + + var extra []byte + if attrs := ctx.ExtraAttributes(nil); len(attrs) > 0 { + var err error + extra, err = json.Marshal(attrs) + if err != nil { + return nil, err + } + } + return &JSONFileLogger{ f: log, buf: bytes.NewBuffer(nil), @@ -85,6 +96,7 @@ func New(ctx logger.Context) (logger.Logger, error) { n: maxFiles, readers: make(map[*logger.LogWatcher]struct{}), notifyRotate: pubsub.NewPublisher(0, 1), + extra: extra, }, nil } @@ -97,7 +109,12 @@ func (l *JSONFileLogger) Log(msg *logger.Message) error { if err != nil { return err } - err = (&jsonlog.JSONLogs{Log: append(msg.Line, '\n'), Stream: msg.Source, Created: timestamp}).MarshalJSONBuf(l.buf) + err = (&jsonlog.JSONLogs{ + Log: append(msg.Line, '\n'), + Stream: msg.Source, + Created: timestamp, + RawAttrs: l.extra, + }).MarshalJSONBuf(l.buf) if err != nil { return err } @@ -181,6 +198,8 @@ func ValidateLogOpt(cfg map[string]string) error { switch key { case "max-file": case "max-size": + case "labels": + case "env": default: return fmt.Errorf("unknown log opt '%s' for json-file log driver", key) } diff --git a/daemon/logger/jsonfilelog/jsonfilelog_test.go b/daemon/logger/jsonfilelog/jsonfilelog_test.go index cd8a0f7113..162c685b24 100644 --- a/daemon/logger/jsonfilelog/jsonfilelog_test.go +++ b/daemon/logger/jsonfilelog/jsonfilelog_test.go @@ -1,9 +1,11 @@ package jsonfilelog import ( + "encoding/json" "io/ioutil" "os" "path/filepath" + "reflect" "strconv" "testing" "time" @@ -149,3 +151,51 @@ func TestJSONFileLoggerWithOpts(t *testing.T) { } } + +func TestJSONFileLoggerWithLabelsEnv(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") + config := map[string]string{"labels": "rack,dc", "env": "environ,debug,ssl"} + l, err := New(logger.Context{ + ContainerID: cid, + LogPath: filename, + Config: config, + ContainerLabels: map[string]string{"rack": "101", "dc": "lhr"}, + ContainerEnv: []string{"environ=production", "debug=false", "port=10001", "ssl=true"}, + }) + if err != nil { + t.Fatal(err) + } + defer l.Close() + if err := l.Log(&logger.Message{ContainerID: cid, Line: []byte("line"), Source: "src1"}); err != nil { + t.Fatal(err) + } + res, err := ioutil.ReadFile(filename) + if err != nil { + t.Fatal(err) + } + + var jsonLog jsonlog.JSONLogs + if err := json.Unmarshal(res, &jsonLog); err != nil { + t.Fatal(err) + } + extra := make(map[string]string) + if err := json.Unmarshal(jsonLog.RawAttrs, &extra); err != nil { + t.Fatal(err) + } + expected := map[string]string{ + "rack": "101", + "dc": "lhr", + "environ": "production", + "debug": "false", + "ssl": "true", + } + if !reflect.DeepEqual(extra, expected) { + t.Fatalf("Wrong log attrs: %q, expected %q", extra, expected) + } +} diff --git a/pkg/jsonlog/jsonlogbytes.go b/pkg/jsonlog/jsonlogbytes.go index b2b1f98489..ff7aaf16e3 100644 --- a/pkg/jsonlog/jsonlogbytes.go +++ b/pkg/jsonlog/jsonlogbytes.go @@ -2,6 +2,7 @@ package jsonlog import ( "bytes" + "encoding/json" "unicode/utf8" ) @@ -12,6 +13,9 @@ type JSONLogs struct { Log []byte `json:"log,omitempty"` Stream string `json:"stream,omitempty"` Created string `json:"time"` + + // json-encoded bytes + RawAttrs json.RawMessage `json:"attrs,omitempty"` } // MarshalJSONBuf is based on the same method from JSONLog @@ -34,6 +38,15 @@ func (mj *JSONLogs) MarshalJSONBuf(buf *bytes.Buffer) error { buf.WriteString(`"stream":`) ffjsonWriteJSONString(buf, mj.Stream) } + if len(mj.RawAttrs) > 0 { + if first == true { + first = false + } else { + buf.WriteString(`,`) + } + buf.WriteString(`"attrs":`) + buf.Write(mj.RawAttrs) + } if first == true { first = false } else { diff --git a/pkg/jsonlog/jsonlogbytes_test.go b/pkg/jsonlog/jsonlogbytes_test.go index 6c36828a03..6d6ad21583 100644 --- a/pkg/jsonlog/jsonlogbytes_test.go +++ b/pkg/jsonlog/jsonlogbytes_test.go @@ -21,6 +21,8 @@ func TestJSONLogsMarshalJSONBuf(t *testing.T) { &JSONLogs{Log: []byte("\u2028 \u2029")}: `^{\"log\":\"\\u2028 \\u2029\",\"time\":}$`, &JSONLogs{Log: []byte{0xaF}}: `^{\"log\":\"\\ufffd\",\"time\":}$`, &JSONLogs{Log: []byte{0x7F}}: `^{\"log\":\"\x7f\",\"time\":}$`, + // with raw attributes + &JSONLogs{Log: []byte("A log line"), RawAttrs: []byte(`{"hello":"world","value":1234}`)}: `^{\"log\":\"A log line\",\"attrs\":{\"hello\":\"world\",\"value\":1234},\"time\":}$`, } for jsonLog, expression := range logs { var buf bytes.Buffer From cd426ebed79e29153eb29ea1ccb260e15db2fea5 Mon Sep 17 00:00:00 2001 From: Vincent Demeester Date: Tue, 13 Oct 2015 08:39:47 +0200 Subject: [PATCH 6/6] Add docs for label/env log-opts Signed-off-by: Vincent Demeester --- docs/reference/logging/fluentd.md | 18 +++++++++++++++ docs/reference/logging/journald.md | 25 +++++++++++++++++++++ docs/reference/logging/overview.md | 35 ++++++++++++++++++++++++++++++ 3 files changed, 78 insertions(+) diff --git a/docs/reference/logging/fluentd.md b/docs/reference/logging/fluentd.md index 12ea2ab387..91b35a9062 100644 --- a/docs/reference/logging/fluentd.md +++ b/docs/reference/logging/fluentd.md @@ -73,6 +73,24 @@ Refer to the [log tag option documentation](log_tags.md) for customizing the log tag format. +### labels and env + +The `labels` and `env` options takes a comma-separated list of keys. If there is collision between `label` and `env` keys, the value of the `env` takes precedence. + +To use attributes, specify them when you start the Docker daemon. + +``` +docker daemon --log-driver=fluentd --log-opt labels=foo --log-opt env=foo,fizz +``` + +Then, run a container and specify values for the `labels` or `env`. For example, you might use this: + +``` +docker run --label foo=bar -e fizz=buzz -d -P training/webapp python app.py +```` + +This adds additional fields to the extra attributes of a logging message. + ## Fluentd daemon management with Docker About `Fluentd` itself, see [the project webpage](http://www.fluentd.org) diff --git a/docs/reference/logging/journald.md b/docs/reference/logging/journald.md index cd7358bd0f..5d3d1c9153 100644 --- a/docs/reference/logging/journald.md +++ b/docs/reference/logging/journald.md @@ -36,6 +36,31 @@ You can set the logging driver for a specific container by using the docker run --log-driver=journald ... +## Options + +Users can use the `--log-opt NAME=VALUE` flag to specify additional +journald logging driver options. + +### labels and env + +The `labels` and `env` options takes a comma-separated list of keys. If there is collision between `label` and `env` keys, the value of the `env` takes precedence. + +To use attributes, specify them when you start the Docker daemon. + +``` +docker daemon --log-driver=journald --log-opt labels=foo --log-opt env=foo,fizz +``` + +Then, run a container and specify values for the `labels` or `env`. For example, you might use this: + +``` +docker run --label foo=bar -e fizz=buzz -d -P training/webapp python app.py +```` + +This adds additional metadata in the journal with each message, one +for each key that matches. + + ## Note regarding container names The value logged in the `CONTAINER_NAME` field is the container name diff --git a/docs/reference/logging/overview.md b/docs/reference/logging/overview.md index 9911a77133..f76f7b7f8c 100644 --- a/docs/reference/logging/overview.md +++ b/docs/reference/logging/overview.md @@ -27,12 +27,15 @@ container's logging driver. The following options are supported: The `docker logs`command is available only for the `json-file` logging driver. + ## json-file options The following logging options are supported for the `json-file` logging driver: --log-opt max-size=[0-9+][k|m|g] --log-opt max-file=[0-9+] + --log-opt labels=label1,label2 + --log-opt env=env1,env2 Logs that reach `max-size` are rolled over. You can set the size in kilobytes(k), megabytes(m), or gigabytes(g). eg `--log-opt max-size=50m`. If `max-size` is not set, then logs are not rolled over. @@ -41,6 +44,26 @@ Logs that reach `max-size` are rolled over. You can set the size in kilobytes(k) If `max-size` and `max-file` are set, `docker logs` only returns the log lines from the newest log file. +The `labels` and `env` options add additional attributes for use with logging drivers that accept them. Each of these options takes a comma-separated list of keys. If there is collision between `label` and `env` keys, the value of the `env` takes precedence. + +To use attributes, specify them when you start the Docker daemon. + +``` +docker daemon --log-driver=json-file --log-opt labels=foo --log-opt env=foo,fizz +``` + +Then, run a container and specify values for the `labels` or `env`. For example, you might use this: + +``` +docker run --label foo=bar -e fizz=buzz -d -P training/webapp python app.py +```` + +This adds additional fields depending on the driver, e.g. for +`json-file` that looks like: + + "attrs":{"fizz":"buzz","foo":"bar"} + + ## syslog options The following logging options are supported for the `syslog` logging driver: @@ -100,6 +123,8 @@ The GELF logging driver supports the following options: --log-opt gelf-address=udp://host:port --log-opt tag="database" + --log-opt labels=label1,label2 + --log-opt env=env1,env2 The `gelf-address` option specifies the remote GELF server address that the driver connects to. Currently, only `udp` is supported as the transport and you must @@ -112,6 +137,15 @@ By default, Docker uses the first 12 characters of the container ID to tag log m Refer to the [log tag option documentation](log_tags.md) for customizing the log tag format. +The `labels` and `env` options are supported by the gelf logging +driver. It adds additional key on the `extra` fields, prefixed by an +underscore (`_`). + + // […] + "_foo": "bar", + "_fizz": "buzz", + // […] + ## fluentd options @@ -128,6 +162,7 @@ If container cannot connect to the Fluentd daemon on the specified address, the container stops immediately. For detailed information on working with this logging driver, see [the fluentd logging driver](fluentd.md) + ## Specify Amazon CloudWatch Logs options The Amazon CloudWatch Logs logging driver supports the following options: