From ed1b9fa07a0b34315d2fa624b978d3f8627319c2 Mon Sep 17 00:00:00 2001 From: Mike Danese Date: Fri, 18 Dec 2015 09:43:32 -0800 Subject: [PATCH] daemon/logger: Add logging driver for Google Cloud Logging Signed-off-by: Mike Danese --- contrib/completion/bash/docker | 7 +- contrib/completion/zsh/_docker | 2 + daemon/logdrivers_linux.go | 1 + daemon/logger/gcplogs/gcplogging.go | 181 ++++++++++++++++++++++++++++ docs/admin/logging/gcplogs.md | 70 +++++++++++ docs/admin/logging/overview.md | 11 ++ man/docker-create.1.md | 2 +- man/docker-daemon.8.md | 2 +- man/docker-run.1.md | 2 +- 9 files changed, 274 insertions(+), 4 deletions(-) create mode 100644 daemon/logger/gcplogs/gcplogging.go create mode 100644 docs/admin/logging/gcplogs.md diff --git a/contrib/completion/bash/docker b/contrib/completion/bash/docker index 0c98b4d801..b565cc6c34 100644 --- a/contrib/completion/bash/docker +++ b/contrib/completion/bash/docker @@ -397,6 +397,7 @@ __docker_complete_log_drivers() { awslogs etwlogs fluentd + gcplogs gelf journald json-file @@ -410,13 +411,14 @@ __docker_complete_log_options() { # see docs/reference/logging/index.md local awslogs_options="awslogs-region awslogs-group awslogs-stream" local fluentd_options="env fluentd-address labels tag" + local gcplogs_options="env gcp-log-cmd gcp-project labels" local gelf_options="env gelf-address labels tag" local journald_options="env labels tag" local json_file_options="env labels max-file max-size" local syslog_options="syslog-address syslog-tls-ca-cert syslog-tls-cert syslog-tls-key syslog-tls-skip-verify syslog-facility tag" local splunk_options="env labels splunk-caname splunk-capath splunk-index splunk-insecureskipverify splunk-source splunk-sourcetype splunk-token splunk-url tag" - local all_options="$fluentd_options $gelf_options $journald_options $json_file_options $syslog_options $splunk_options" + local all_options="$fluentd_options $gcplogs_options $gelf_options $journald_options $json_file_options $syslog_options $splunk_options" case $(__docker_value_of_option --log-driver) in '') @@ -428,6 +430,9 @@ __docker_complete_log_options() { fluentd) COMPREPLY=( $( compgen -W "$fluentd_options" -S = -- "$cur" ) ) ;; + gcplogs) + COMPREPLY=( $( compgen -W "$gcplogs_options" -S = -- "$cur" ) ) + ;; gelf) COMPREPLY=( $( compgen -W "$gelf_options" -S = -- "$cur" ) ) ;; diff --git a/contrib/completion/zsh/_docker b/contrib/completion/zsh/_docker index 4e6a18f11b..c9a261726b 100644 --- a/contrib/completion/zsh/_docker +++ b/contrib/completion/zsh/_docker @@ -201,6 +201,7 @@ __docker_get_log_options() { awslogs_options=("awslogs-region" "awslogs-group" "awslogs-stream") fluentd_options=("env" "fluentd-address" "labels" "tag") + gcplogs_options=("env" "gcp-log-cmd" "gcp-project" "labels") gelf_options=("env" "gelf-address" "labels" "tag") journald_options=("env" "labels") json_file_options=("env" "labels" "max-file" "max-size") @@ -209,6 +210,7 @@ __docker_get_log_options() { [[ $log_driver = (awslogs|all) ]] && _describe -t awslogs-options "awslogs options" awslogs_options "$@" && ret=0 [[ $log_driver = (fluentd|all) ]] && _describe -t fluentd-options "fluentd options" fluentd_options "$@" && ret=0 + [[ $log_driver = (gcplogs|all) ]] && _describe -t gcplogs-options "gcplogs options" gcplogs_options "$@" && ret=0 [[ $log_driver = (gelf|all) ]] && _describe -t gelf-options "gelf options" gelf_options "$@" && ret=0 [[ $log_driver = (journald|all) ]] && _describe -t journald-options "journald options" journald_options "$@" && ret=0 [[ $log_driver = (json-file|all) ]] && _describe -t json-file-options "json-file options" json_file_options "$@" && ret=0 diff --git a/daemon/logdrivers_linux.go b/daemon/logdrivers_linux.go index 0abc6269de..89fe49a858 100644 --- a/daemon/logdrivers_linux.go +++ b/daemon/logdrivers_linux.go @@ -5,6 +5,7 @@ import ( // therefore they register themselves to the logdriver factory. _ "github.com/docker/docker/daemon/logger/awslogs" _ "github.com/docker/docker/daemon/logger/fluentd" + _ "github.com/docker/docker/daemon/logger/gcplogs" _ "github.com/docker/docker/daemon/logger/gelf" _ "github.com/docker/docker/daemon/logger/journald" _ "github.com/docker/docker/daemon/logger/jsonfilelog" diff --git a/daemon/logger/gcplogs/gcplogging.go b/daemon/logger/gcplogs/gcplogging.go new file mode 100644 index 0000000000..b9b8af5871 --- /dev/null +++ b/daemon/logger/gcplogs/gcplogging.go @@ -0,0 +1,181 @@ +package gcplogs + +import ( + "fmt" + "sync/atomic" + "time" + + "github.com/docker/docker/daemon/logger" + + "github.com/Sirupsen/logrus" + "golang.org/x/net/context" + "google.golang.org/cloud/compute/metadata" + "google.golang.org/cloud/logging" +) + +const ( + name = "gcplogs" + + projectOptKey = "gcp-project" + logLabelsKey = "labels" + logEnvKey = "env" + logCmdKey = "gcp-log-cmd" +) + +var ( + // The number of logs the gcplogs driver has dropped. + droppedLogs uint64 + + onGCE = metadata.OnGCE() + + // instance metadata populated from the metadata server if available + projectID string + zone string + instanceName string + instanceID string +) + +func init() { + if onGCE { + // These will fail on instances if the metadata service is + // down or the client is compiled with an API version that + // has been removed. Since these are not vital, let's ignore + // them and make their fields in the dockeLogEntry ,omitempty + projectID, _ = metadata.ProjectID() + zone, _ = metadata.Zone() + instanceName, _ = metadata.InstanceName() + instanceID, _ = metadata.InstanceID() + } + + if err := logger.RegisterLogDriver(name, New); err != nil { + logrus.Fatal(err) + } + + if err := logger.RegisterLogOptValidator(name, ValidateLogOpts); err != nil { + logrus.Fatal(err) + } +} + +type gcplogs struct { + client *logging.Client + instance *instanceInfo + container *containerInfo +} + +type dockerLogEntry struct { + Instance *instanceInfo `json:"instance,omitempty"` + Container *containerInfo `json:"container,omitempty"` + Data string `json:"data,omitempty"` +} + +type instanceInfo struct { + Zone string `json:"zone,omitempty"` + Name string `json:"name,omitempty"` + ID string `json:"id,omitempty"` +} + +type containerInfo struct { + Name string `json:"name,omitempty"` + ID string `json:"id,omitempty"` + ImageName string `json:"imageName,omitempty"` + ImageID string `json:"imageId,omitempty"` + Created time.Time `json:"created,omitempty"` + Command string `json:"command,omitempty"` + Metadata map[string]string `json:"metadata,omitempty"` +} + +// New creates a new logger that logs to Google Cloud Logging using the application +// default credentials. +// +// See https://developers.google.com/identity/protocols/application-default-credentials +func New(ctx logger.Context) (logger.Logger, error) { + + var project string + if projectID != "" { + project = projectID + } + if projectID, found := ctx.Config[projectOptKey]; found { + project = projectID + } + if project == "" { + return nil, fmt.Errorf("No project was specified and couldn't read project from the meatadata server. Please specify a project") + } + + c, err := logging.NewClient(context.Background(), project, "gcplogs-docker-driver") + if err != nil { + return nil, err + } + + if err := c.Ping(); err != nil { + return nil, fmt.Errorf("unable to connect or authenticate with Google Cloud Logging: %v", err) + } + + l := &gcplogs{ + client: c, + container: &containerInfo{ + Name: ctx.ContainerName, + ID: ctx.ContainerID, + ImageName: ctx.ContainerImageName, + ImageID: ctx.ContainerImageID, + Created: ctx.ContainerCreated, + Metadata: ctx.ExtraAttributes(nil), + }, + } + + if ctx.Config[logCmdKey] == "true" { + l.container.Command = ctx.Command() + } + + if onGCE { + l.instance = &instanceInfo{ + Zone: zone, + Name: instanceName, + ID: instanceID, + } + } + + // The logger "overflows" at a rate of 10,000 logs per second and this + // overflow func is called. We want to surface the error to the user + // without overly spamming /var/log/docker.log so we log the first time + // we overflow and every 1000th time after. + c.Overflow = func(_ *logging.Client, _ logging.Entry) error { + if i := atomic.AddUint64(&droppedLogs, 1); i%1000 == 1 { + logrus.Errorf("gcplogs driver has dropped %v logs", i) + } + return nil + } + + return l, nil +} + +// ValidateLogOpts validates the opts passed to the gcplogs driver. Currently, the gcplogs +// driver doesn't take any arguments. +func ValidateLogOpts(cfg map[string]string) error { + for k := range cfg { + switch k { + case projectOptKey, logLabelsKey, logEnvKey, logCmdKey: + default: + return fmt.Errorf("%q is not a valid option for the gcplogs driver", k) + } + } + return nil +} + +func (l *gcplogs) Log(m *logger.Message) error { + return l.client.Log(logging.Entry{ + Time: m.Timestamp, + Payload: &dockerLogEntry{ + Instance: l.instance, + Container: l.container, + Data: string(m.Line), + }, + }) +} + +func (l *gcplogs) Close() error { + return l.client.Flush() +} + +func (l *gcplogs) Name() string { + return name +} diff --git a/docs/admin/logging/gcplogs.md b/docs/admin/logging/gcplogs.md new file mode 100644 index 0000000000..08fd858da0 --- /dev/null +++ b/docs/admin/logging/gcplogs.md @@ -0,0 +1,70 @@ + + +# Google Cloud Logging driver + +The Google Cloud Logging driver sends container logs to Google Cloud +Logging. + +## Usage + +You can configure the default logging driver by passing the `--log-driver` +option to the Docker daemon: + + docker daemon --log-driver=gcplogs + +You can set the logging driver for a specific container by using the +`--log-driver` option to `docker run`: + + docker run --log-driver=gcplogs ... + +This log driver does not implement a reader so it is incompatible with +`docker logs`. + +If Docker detects that it is running in a Google Cloud Project, it will discover configuration +from the instance metadata service. +Otherwise, the user must specify which project to log to using the `--gcp-project` +log option and Docker will attempt to obtain credentials from the +Google Application Default Credential. +The `--gcp-project` takes precedence over information discovered from the metadata server +so a Docker daemon running in a Google Cloud Project can be overriden to log to a different +Google Cloud Project using `--gcp-project`. + +## gcplogs options + +You can use the `--log-opt NAME=VALUE` flag to specify these additional Google +Cloud Logging driver options: + +| Option | Required | Description | +|-----------------------------|----------|---------------------------------------------------------------------------------------------------------------------------------------------| +| `gcp-project` | optional | Which GCP project to log to. Defaults to discovering this value from the GCE metadata service. | +| `gcp-log-cmd` | optional | Whether to log the command that the container was started with. Defaults to false. | +| `labels` | optional | Comma-separated list of keys of labels, which should be included in message, if these labels are specified for container. | +| `env` | optional | Comma-separated list of keys of environment variables, which should be included in message, if these variables are specified for container. | + +If there is collision between `label` and `env` keys, the value of the `env` +takes precedence. Both options add additional fields to the attributes of a +logging message. + +Below is an example of the logging options required to log to the default +logging destination which is discovered by querying the GCE metadata server. + + docker run --log-driver=gcplogs \ + --log-opt labels=location + --log-opt env=TEST + --log-opt gcp-log-cmd=true + --env "TEST=false" + --label location=west + your/application + +This configuration also directs the driver to include in the payload the label +`location`, the environment variable `ENV`, and the command used to start the +container. diff --git a/docs/admin/logging/overview.md b/docs/admin/logging/overview.md index 825e3ecac0..e3d3d11256 100644 --- a/docs/admin/logging/overview.md +++ b/docs/admin/logging/overview.md @@ -27,6 +27,7 @@ container's logging driver. The following options are supported: | `awslogs` | Amazon CloudWatch Logs logging driver for Docker. Writes log messages to Amazon CloudWatch Logs. | | `splunk` | Splunk logging driver for Docker. Writes log messages to `splunk` using HTTP Event Collector. | | `etwlogs` | ETW logging driver for Docker on Windows. Writes log messages as ETW events. | +| `gcplogs` | Google Cloud Logging driver for Docker. Writes log messages to Google Cloud Logging. | The `docker logs`command is available only for the `json-file` and `journald` logging drivers. @@ -213,4 +214,14 @@ as an ETW event. An ETW listener can then be created to listen for these events. For detailed information on working with this logging driver, see [the ETW logging driver](etwlogs.md) reference documentation. +## Google Cloud Logging +The Google Cloud Logging driver supports the following options: + + --log-opt gcp-project= + --log-opt labels=, + --log-opt env=, + --log-opt log-cmd=true + +For detailed information about working with this logging driver, see the [Google Cloud Logging driver](gcplogs.md). +reference documentation. diff --git a/man/docker-create.1.md b/man/docker-create.1.md index 36f0d94ef3..6a2640d205 100644 --- a/man/docker-create.1.md +++ b/man/docker-create.1.md @@ -214,7 +214,7 @@ millions of trillions. Add link to another container in the form of :alias or just in which case the alias will match the name. -**--log-driver**="*json-file*|*syslog*|*journald*|*gelf*|*fluentd*|*awslogs*|*splunk*|*etwlogs*|*none*" +**--log-driver**="*json-file*|*syslog*|*journald*|*gelf*|*fluentd*|*awslogs*|*splunk*|*etwlogs*|*gcplogs*|*none*" Logging driver for container. Default is defined by daemon `--log-driver` flag. **Warning**: the `docker logs` command works only for the `json-file` and `journald` logging drivers. diff --git a/man/docker-daemon.8.md b/man/docker-daemon.8.md index c7ab68628b..9f699c7124 100644 --- a/man/docker-daemon.8.md +++ b/man/docker-daemon.8.md @@ -185,7 +185,7 @@ unix://[/path/to/socket] to use. **--label**="[]" Set key=value labels to the daemon (displayed in `docker info`) -**--log-driver**="*json-file*|*syslog*|*journald*|*gelf*|*fluentd*|*awslogs*|*splunk*|*etwlogs*|*none*" +**--log-driver**="*json-file*|*syslog*|*journald*|*gelf*|*fluentd*|*awslogs*|*splunk*|*etwlogs*|*gcplogs*|*none*" Default driver for container logs. Default is `json-file`. **Warning**: `docker logs` command works only for `json-file` logging driver. diff --git a/man/docker-run.1.md b/man/docker-run.1.md index 90e3ebdf44..bf75fb68ef 100644 --- a/man/docker-run.1.md +++ b/man/docker-run.1.md @@ -320,7 +320,7 @@ container can access the exposed port via a private networking interface. Docker will set some environment variables in the client container to help indicate which interface and port to use. -**--log-driver**="*json-file*|*syslog*|*journald*|*gelf*|*fluentd*|*awslogs*|*splunk*|*etwlogs*|*none*" +**--log-driver**="*json-file*|*syslog*|*journald*|*gelf*|*fluentd*|*awslogs*|*splunk*|*etwlogs*|*gcplogs*|*none*" Logging driver for container. Default is defined by daemon `--log-driver` flag. **Warning**: the `docker logs` command works only for the `json-file` and `journald` logging drivers.