Skip to content

Commit

Permalink
Merge pull request #16961 from vdemeester/pr-15975-carry-for-docs
Browse files Browse the repository at this point in the history
Carry #15975 - Add extra fields based on label and env for gelf/fluentd/json-file/journald log drivers
  • Loading branch information
thaJeztah committed Oct 13, 2015
2 parents 0bc748b + cd426eb commit 3856c5e
Show file tree
Hide file tree
Showing 12 changed files with 254 additions and 41 deletions.
2 changes: 2 additions & 0 deletions daemon/container.go
Expand Up @@ -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"
Expand Down
40 changes: 40 additions & 0 deletions daemon/logger/context.go
Expand Up @@ -17,9 +17,49 @@ type Context struct {
ContainerImageID string
ContainerImageName string
ContainerCreated time.Time
ContainerEnv []string
ContainerLabels map[string]string
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()
Expand Down
12 changes: 9 additions & 3 deletions daemon/logger/fluentd/fluentd.go
Expand Up @@ -20,6 +20,7 @@ type fluentd struct {
containerID string
containerName string
writer *fluent.Fluent
extra map[string]string
}

const (
Expand Down Expand Up @@ -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})
Expand All @@ -65,6 +65,7 @@ func New(ctx logger.Context) (logger.Logger, error) {
containerID: ctx.ContainerID,
containerName: ctx.ContainerName,
writer: log,
extra: extra,
}, nil
}

Expand All @@ -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)
Expand All @@ -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)
}
Expand Down
66 changes: 30 additions & 36 deletions daemon/logger/gelf/gelf.go
Expand Up @@ -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() {
Expand Down Expand Up @@ -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
Expand All @@ -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
}

Expand All @@ -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 {
Expand All @@ -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)
}
Expand Down
11 changes: 10 additions & 1 deletion daemon/logger/journald/journald.go
Expand Up @@ -6,6 +6,7 @@ package journald

import (
"fmt"
"strings"
"sync"

"github.com/Sirupsen/logrus"
Expand Down Expand Up @@ -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
}

Expand All @@ -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)
}
Expand Down
21 changes: 20 additions & 1 deletion daemon/logger/jsonfilelog/jsonfilelog.go
Expand Up @@ -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() {
Expand Down Expand Up @@ -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),
Expand All @@ -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
}

Expand All @@ -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
}
Expand Down Expand Up @@ -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)
}
Expand Down
50 changes: 50 additions & 0 deletions daemon/logger/jsonfilelog/jsonfilelog_test.go
@@ -1,9 +1,11 @@
package jsonfilelog

import (
"encoding/json"
"io/ioutil"
"os"
"path/filepath"
"reflect"
"strconv"
"testing"
"time"
Expand Down Expand Up @@ -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)
}
}
18 changes: 18 additions & 0 deletions docs/reference/logging/fluentd.md
Expand Up @@ -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)
Expand Down

0 comments on commit 3856c5e

Please sign in to comment.