diff --git a/components/engine/daemon/logdrivers_windows.go b/components/engine/daemon/logdrivers_windows.go index d3710ec174..129b06650b 100644 --- a/components/engine/daemon/logdrivers_windows.go +++ b/components/engine/daemon/logdrivers_windows.go @@ -4,6 +4,7 @@ import ( // Importing packages here only to make sure their init gets called and // therefore they register themselves to the logdriver factory. _ "github.com/docker/docker/daemon/logger/awslogs" + _ "github.com/docker/docker/daemon/logger/etwlogs" _ "github.com/docker/docker/daemon/logger/jsonfilelog" _ "github.com/docker/docker/daemon/logger/splunk" ) diff --git a/components/engine/daemon/logger/etwlogs/etwlogs_windows.go b/components/engine/daemon/logger/etwlogs/etwlogs_windows.go new file mode 100644 index 0000000000..de128a2e93 --- /dev/null +++ b/components/engine/daemon/logger/etwlogs/etwlogs_windows.go @@ -0,0 +1,183 @@ +// Package etwlogs provides a log driver for forwarding container logs +// as ETW events.(ETW stands for Event Tracing for Windows) +// A client can then create an ETW listener to listen for events that are sent +// by the ETW provider that we register, using the provider's GUID "a3693192-9ed6-46d2-a981-f8226c8363bd". +// Here is an example of how to do this using the logman utility: +// 1. logman start -ets DockerContainerLogs -p {a3693192-9ed6-46d2-a981-f8226c8363bd} 0 0 -o trace.etl +// 2. Run container(s) and generate log messages +// 3. logman stop -ets DockerContainerLogs +// 4. You can then convert the etl log file to XML using: tracerpt -y trace.etl +// +// Each container log message generates a ETW event that also contains: +// the container name and ID, the timestamp, and the stream type. +package etwlogs + +import ( + "errors" + "fmt" + "sync" + "syscall" + "unsafe" + + "github.com/Sirupsen/logrus" + "github.com/docker/docker/daemon/logger" +) + +type etwLogs struct { + containerName string + imageName string + containerID string + imageID string +} + +const ( + name = "etwlogs" + win32CallSuccess = 0 +) + +var win32Lib *syscall.DLL +var providerHandle syscall.Handle +var refCount int +var mu sync.Mutex + +func init() { + providerHandle = syscall.InvalidHandle + if err := logger.RegisterLogDriver(name, New); err != nil { + logrus.Fatal(err) + } +} + +// New creates a new etwLogs logger for the given container and registers the EWT provider. +func New(ctx logger.Context) (logger.Logger, error) { + if err := registerETWProvider(); err != nil { + return nil, err + } + logrus.Debugf("logging driver etwLogs configured for container: %s.", ctx.ContainerID) + + return &etwLogs{ + containerName: fixContainerName(ctx.ContainerName), + imageName: ctx.ContainerImageName, + containerID: ctx.ContainerID, + imageID: ctx.ContainerImageID, + }, nil +} + +// Log logs the message to the ETW stream. +func (etwLogger *etwLogs) Log(msg *logger.Message) error { + if providerHandle == syscall.InvalidHandle { + // This should never be hit, if it is, it indicates a programming error. + errorMessage := "ETWLogs cannot log the message, because the event provider has not been registered." + logrus.Error(errorMessage) + return errors.New(errorMessage) + } + return callEventWriteString(createLogMessage(etwLogger, msg)) +} + +// Close closes the logger by unregistering the ETW provider. +func (etwLogger *etwLogs) Close() error { + unregisterETWProvider() + return nil +} + +func (etwLogger *etwLogs) Name() string { + return name +} + +func createLogMessage(etwLogger *etwLogs, msg *logger.Message) string { + return fmt.Sprintf("container_name: %s, image_name: %s, container_id: %s, image_id: %s, source: %s, log: %s", + etwLogger.containerName, + etwLogger.imageName, + etwLogger.containerID, + etwLogger.imageID, + msg.Source, + msg.Line) +} + +// fixContainerName removes the initial '/' from the container name. +func fixContainerName(cntName string) string { + if len(cntName) > 0 && cntName[0] == '/' { + cntName = cntName[1:] + } + return cntName +} + +func registerETWProvider() error { + mu.Lock() + defer mu.Unlock() + if refCount == 0 { + var err error + if win32Lib, err = syscall.LoadDLL("Advapi32.dll"); err != nil { + return err + } + if err = callEventRegister(); err != nil { + win32Lib.Release() + win32Lib = nil + return err + } + } + + refCount++ + return nil +} + +func unregisterETWProvider() { + mu.Lock() + defer mu.Unlock() + if refCount == 1 { + if callEventUnregister() { + refCount-- + providerHandle = syscall.InvalidHandle + win32Lib.Release() + win32Lib = nil + } + // Not returning an error if EventUnregister fails, because etwLogs will continue to work + } else { + refCount-- + } +} + +func callEventRegister() error { + proc, err := win32Lib.FindProc("EventRegister") + if err != nil { + return err + } + // The provider's GUID is {a3693192-9ed6-46d2-a981-f8226c8363bd} + guid := syscall.GUID{ + 0xa3693192, 0x9ed6, 0x46d2, + [8]byte{0xa9, 0x81, 0xf8, 0x22, 0x6c, 0x83, 0x63, 0xbd}, + } + + ret, _, _ := proc.Call(uintptr(unsafe.Pointer(&guid)), 0, 0, uintptr(unsafe.Pointer(&providerHandle))) + if ret != win32CallSuccess { + errorMessage := fmt.Sprintf("Failed to register ETW provider. Error: %d", ret) + logrus.Error(errorMessage) + return errors.New(errorMessage) + } + return nil +} + +func callEventWriteString(message string) error { + proc, err := win32Lib.FindProc("EventWriteString") + if err != nil { + return err + } + ret, _, _ := proc.Call(uintptr(providerHandle), 0, 0, uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(message)))) + if ret != win32CallSuccess { + errorMessage := fmt.Sprintf("ETWLogs provider failed to log message. Error: %d", ret) + logrus.Error(errorMessage) + return errors.New(errorMessage) + } + return nil +} + +func callEventUnregister() bool { + proc, err := win32Lib.FindProc("EventUnregister") + if err != nil { + return false + } + ret, _, _ := proc.Call(uintptr(providerHandle)) + if ret != win32CallSuccess { + return false + } + return true +} diff --git a/components/engine/docs/admin/logging/etwlogs.md b/components/engine/docs/admin/logging/etwlogs.md new file mode 100644 index 0000000000..5b98fd5495 --- /dev/null +++ b/components/engine/docs/admin/logging/etwlogs.md @@ -0,0 +1,69 @@ + + + +# ETW logging driver + +The ETW logging driver forwards container logs as ETW events. +ETW stands for Event Tracing in Windows, and is the common framework +for tracing applications in Windows. Each ETW event contains a message +with both the log and its context information. A client can then create +an ETW listener to listen to these events. + +The ETW provider that this logging driver registers with Windows, has the +GUID identifier of: `{a3693192-9ed6-46d2-a981-f8226c8363bd}`. A client creates an +ETW listener and registers to listen to events from the logging driver's provider. +It does not matter the order in which the provider and listener are created. +A client can create their ETW listener and start listening for events from the provider, +before the provider has been registered with the system. + +## Usage + +Here is an example of how to listen to these events using the logman utility program +included in most installations of Windows: + + 1. `logman start -ets DockerContainerLogs -p {a3693192-9ed6-46d2-a981-f8226c8363bd} 0 0 -o trace.etl` + 2. Run your container(s) with the etwlogs driver, by adding `--log-driver=etwlogs` + to the Docker run command, and generate log messages. + 3. `logman stop -ets DockerContainerLogs` + 4. This will generate an etl file that contains the events. One way to convert this file into + human-readable form is to run: `tracerpt -y trace.etl`. + +Each ETW event will contain a structured message string in this format: + + container_name: %s, image_name: %s, container_id: %s, image_id: %s, source: [stdout | stderr], log: %s + +Details on each item in the message can be found below: + +| Field | Description | +-----------------------|-------------------------------------------------| +| `container_name` | The container name at the time it was started. | +| `image_name` | The name of the container's image. | +| `container_id` | The full 64-character container ID. | +| `image_id` | The full ID of the container's image. | +| `source` | `stdout` or `stderr`. | +| `log` | The container log message. | + +Here is an example event message: + + container_name: backstabbing_spence, + image_name: windowsservercore, + container_id: f14bb55aa862d7596b03a33251c1be7dbbec8056bbdead1da8ec5ecebbe29731, + image_id: sha256:2f9e19bd998d3565b4f345ac9aaf6e3fc555406239a4fb1b1ba879673713824b, + source: stdout, + log: Hello world! + +A client can parse this message string to get both the log message, as well as its +context information. Note that the time stamp is also available within the ETW event. + +**Note** This ETW provider emits only a message string, and not a specially +structured ETW event. Therefore, it is not required to register a manifest file +with the system to read and interpret its ETW events. diff --git a/components/engine/docs/admin/logging/index.md b/components/engine/docs/admin/logging/index.md index 64f9c526aa..4300565e8f 100644 --- a/components/engine/docs/admin/logging/index.md +++ b/components/engine/docs/admin/logging/index.md @@ -20,3 +20,4 @@ weight=8 * [Journald logging driver](journald.md) * [Amazon CloudWatch Logs logging driver](awslogs.md) * [Splunk logging driver](splunk.md) +* [ETW logging driver](etwlogs.md) diff --git a/components/engine/docs/admin/logging/overview.md b/components/engine/docs/admin/logging/overview.md index 531b338ed0..825e3ecac0 100644 --- a/components/engine/docs/admin/logging/overview.md +++ b/components/engine/docs/admin/logging/overview.md @@ -26,6 +26,7 @@ container's logging driver. The following options are supported: | `fluentd` | Fluentd logging driver for Docker. Writes log messages to `fluentd` (forward input). | | `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. | The `docker logs`command is available only for the `json-file` and `journald` logging drivers. @@ -204,3 +205,12 @@ The Splunk logging driver requires the following options: For detailed information about working with this logging driver, see the [Splunk logging driver](splunk.md) reference documentation. + +## ETW logging driver options + +The etwlogs logging driver does not require any options to be specified. This logging driver will forward each log message +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. + + diff --git a/components/engine/docs/reference/api/docker_remote_api_v1.23.md b/components/engine/docs/reference/api/docker_remote_api_v1.23.md index 2cd9758892..422920f083 100644 --- a/components/engine/docs/reference/api/docker_remote_api_v1.23.md +++ b/components/engine/docs/reference/api/docker_remote_api_v1.23.md @@ -402,7 +402,7 @@ Json Parameters: systems, such as SELinux. - **LogConfig** - Log configuration for the container, specified as a JSON object in the form `{ "Type": "", "Config": {"key1": "val1"}}`. - Available types: `json-file`, `syslog`, `journald`, `gelf`, `awslogs`, `splunk`, `none`. + Available types: `json-file`, `syslog`, `journald`, `gelf`, `fluentd`, `awslogs`, `splunk`, `etwlogs`, `none`. `json-file` logging driver. - **CgroupParent** - Path to `cgroups` under which the container's `cgroup` is created. If the path is not absolute, the path is considered to be relative to the `cgroups` path of the init process. Cgroups are created if they do not already exist. - **VolumeDriver** - Driver that this container users to mount volumes. diff --git a/components/engine/man/docker-create.1.md b/components/engine/man/docker-create.1.md index a45eef7d63..36f0d94ef3 100644 --- a/components/engine/man/docker-create.1.md +++ b/components/engine/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*|*none*" +**--log-driver**="*json-file*|*syslog*|*journald*|*gelf*|*fluentd*|*awslogs*|*splunk*|*etwlogs*|*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/components/engine/man/docker-daemon.8.md b/components/engine/man/docker-daemon.8.md index 051c9e0748..7584c91bd1 100644 --- a/components/engine/man/docker-daemon.8.md +++ b/components/engine/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*|*none*" +**--log-driver**="*json-file*|*syslog*|*journald*|*gelf*|*fluentd*|*awslogs*|*splunk*|*etwlogs*|*none*" Default driver for container logs. Default is `json-file`. **Warning**: `docker logs` command works only for `json-file` logging driver. diff --git a/components/engine/man/docker-run.1.md b/components/engine/man/docker-run.1.md index 210343e3e4..90e3ebdf44 100644 --- a/components/engine/man/docker-run.1.md +++ b/components/engine/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*|*none*" +**--log-driver**="*json-file*|*syslog*|*journald*|*gelf*|*fluentd*|*awslogs*|*splunk*|*etwlogs*|*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.