Signed-off-by: Cedric Davies <cedricda@microsoft.com>
| ... | ... |
@@ -4,6 +4,7 @@ import ( |
| 4 | 4 |
// Importing packages here only to make sure their init gets called and |
| 5 | 5 |
// therefore they register themselves to the logdriver factory. |
| 6 | 6 |
_ "github.com/docker/docker/daemon/logger/awslogs" |
| 7 |
+ _ "github.com/docker/docker/daemon/logger/etwlogs" |
|
| 7 | 8 |
_ "github.com/docker/docker/daemon/logger/jsonfilelog" |
| 8 | 9 |
_ "github.com/docker/docker/daemon/logger/splunk" |
| 9 | 10 |
) |
| 10 | 11 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,183 @@ |
| 0 |
+// Package etwlogs provides a log driver for forwarding container logs |
|
| 1 |
+// as ETW events.(ETW stands for Event Tracing for Windows) |
|
| 2 |
+// A client can then create an ETW listener to listen for events that are sent |
|
| 3 |
+// by the ETW provider that we register, using the provider's GUID "a3693192-9ed6-46d2-a981-f8226c8363bd". |
|
| 4 |
+// Here is an example of how to do this using the logman utility: |
|
| 5 |
+// 1. logman start -ets DockerContainerLogs -p {a3693192-9ed6-46d2-a981-f8226c8363bd} 0 0 -o trace.etl
|
|
| 6 |
+// 2. Run container(s) and generate log messages |
|
| 7 |
+// 3. logman stop -ets DockerContainerLogs |
|
| 8 |
+// 4. You can then convert the etl log file to XML using: tracerpt -y trace.etl |
|
| 9 |
+// |
|
| 10 |
+// Each container log message generates a ETW event that also contains: |
|
| 11 |
+// the container name and ID, the timestamp, and the stream type. |
|
| 12 |
+package etwlogs |
|
| 13 |
+ |
|
| 14 |
+import ( |
|
| 15 |
+ "errors" |
|
| 16 |
+ "fmt" |
|
| 17 |
+ "sync" |
|
| 18 |
+ "syscall" |
|
| 19 |
+ "unsafe" |
|
| 20 |
+ |
|
| 21 |
+ "github.com/Sirupsen/logrus" |
|
| 22 |
+ "github.com/docker/docker/daemon/logger" |
|
| 23 |
+) |
|
| 24 |
+ |
|
| 25 |
+type etwLogs struct {
|
|
| 26 |
+ containerName string |
|
| 27 |
+ imageName string |
|
| 28 |
+ containerID string |
|
| 29 |
+ imageID string |
|
| 30 |
+} |
|
| 31 |
+ |
|
| 32 |
+const ( |
|
| 33 |
+ name = "etwlogs" |
|
| 34 |
+ win32CallSuccess = 0 |
|
| 35 |
+) |
|
| 36 |
+ |
|
| 37 |
+var win32Lib *syscall.DLL |
|
| 38 |
+var providerHandle syscall.Handle |
|
| 39 |
+var refCount int |
|
| 40 |
+var mu sync.Mutex |
|
| 41 |
+ |
|
| 42 |
+func init() {
|
|
| 43 |
+ providerHandle = syscall.InvalidHandle |
|
| 44 |
+ if err := logger.RegisterLogDriver(name, New); err != nil {
|
|
| 45 |
+ logrus.Fatal(err) |
|
| 46 |
+ } |
|
| 47 |
+} |
|
| 48 |
+ |
|
| 49 |
+// New creates a new etwLogs logger for the given container and registers the EWT provider. |
|
| 50 |
+func New(ctx logger.Context) (logger.Logger, error) {
|
|
| 51 |
+ if err := registerETWProvider(); err != nil {
|
|
| 52 |
+ return nil, err |
|
| 53 |
+ } |
|
| 54 |
+ logrus.Debugf("logging driver etwLogs configured for container: %s.", ctx.ContainerID)
|
|
| 55 |
+ |
|
| 56 |
+ return &etwLogs{
|
|
| 57 |
+ containerName: fixContainerName(ctx.ContainerName), |
|
| 58 |
+ imageName: ctx.ContainerImageName, |
|
| 59 |
+ containerID: ctx.ContainerID, |
|
| 60 |
+ imageID: ctx.ContainerImageID, |
|
| 61 |
+ }, nil |
|
| 62 |
+} |
|
| 63 |
+ |
|
| 64 |
+// Log logs the message to the ETW stream. |
|
| 65 |
+func (etwLogger *etwLogs) Log(msg *logger.Message) error {
|
|
| 66 |
+ if providerHandle == syscall.InvalidHandle {
|
|
| 67 |
+ // This should never be hit, if it is, it indicates a programming error. |
|
| 68 |
+ errorMessage := "ETWLogs cannot log the message, because the event provider has not been registered." |
|
| 69 |
+ logrus.Error(errorMessage) |
|
| 70 |
+ return errors.New(errorMessage) |
|
| 71 |
+ } |
|
| 72 |
+ return callEventWriteString(createLogMessage(etwLogger, msg)) |
|
| 73 |
+} |
|
| 74 |
+ |
|
| 75 |
+// Close closes the logger by unregistering the ETW provider. |
|
| 76 |
+func (etwLogger *etwLogs) Close() error {
|
|
| 77 |
+ unregisterETWProvider() |
|
| 78 |
+ return nil |
|
| 79 |
+} |
|
| 80 |
+ |
|
| 81 |
+func (etwLogger *etwLogs) Name() string {
|
|
| 82 |
+ return name |
|
| 83 |
+} |
|
| 84 |
+ |
|
| 85 |
+func createLogMessage(etwLogger *etwLogs, msg *logger.Message) string {
|
|
| 86 |
+ return fmt.Sprintf("container_name: %s, image_name: %s, container_id: %s, image_id: %s, source: %s, log: %s",
|
|
| 87 |
+ etwLogger.containerName, |
|
| 88 |
+ etwLogger.imageName, |
|
| 89 |
+ etwLogger.containerID, |
|
| 90 |
+ etwLogger.imageID, |
|
| 91 |
+ msg.Source, |
|
| 92 |
+ msg.Line) |
|
| 93 |
+} |
|
| 94 |
+ |
|
| 95 |
+// fixContainerName removes the initial '/' from the container name. |
|
| 96 |
+func fixContainerName(cntName string) string {
|
|
| 97 |
+ if len(cntName) > 0 && cntName[0] == '/' {
|
|
| 98 |
+ cntName = cntName[1:] |
|
| 99 |
+ } |
|
| 100 |
+ return cntName |
|
| 101 |
+} |
|
| 102 |
+ |
|
| 103 |
+func registerETWProvider() error {
|
|
| 104 |
+ mu.Lock() |
|
| 105 |
+ defer mu.Unlock() |
|
| 106 |
+ if refCount == 0 {
|
|
| 107 |
+ var err error |
|
| 108 |
+ if win32Lib, err = syscall.LoadDLL("Advapi32.dll"); err != nil {
|
|
| 109 |
+ return err |
|
| 110 |
+ } |
|
| 111 |
+ if err = callEventRegister(); err != nil {
|
|
| 112 |
+ win32Lib.Release() |
|
| 113 |
+ win32Lib = nil |
|
| 114 |
+ return err |
|
| 115 |
+ } |
|
| 116 |
+ } |
|
| 117 |
+ |
|
| 118 |
+ refCount++ |
|
| 119 |
+ return nil |
|
| 120 |
+} |
|
| 121 |
+ |
|
| 122 |
+func unregisterETWProvider() {
|
|
| 123 |
+ mu.Lock() |
|
| 124 |
+ defer mu.Unlock() |
|
| 125 |
+ if refCount == 1 {
|
|
| 126 |
+ if callEventUnregister() {
|
|
| 127 |
+ refCount-- |
|
| 128 |
+ providerHandle = syscall.InvalidHandle |
|
| 129 |
+ win32Lib.Release() |
|
| 130 |
+ win32Lib = nil |
|
| 131 |
+ } |
|
| 132 |
+ // Not returning an error if EventUnregister fails, because etwLogs will continue to work |
|
| 133 |
+ } else {
|
|
| 134 |
+ refCount-- |
|
| 135 |
+ } |
|
| 136 |
+} |
|
| 137 |
+ |
|
| 138 |
+func callEventRegister() error {
|
|
| 139 |
+ proc, err := win32Lib.FindProc("EventRegister")
|
|
| 140 |
+ if err != nil {
|
|
| 141 |
+ return err |
|
| 142 |
+ } |
|
| 143 |
+ // The provider's GUID is {a3693192-9ed6-46d2-a981-f8226c8363bd}
|
|
| 144 |
+ guid := syscall.GUID{
|
|
| 145 |
+ 0xa3693192, 0x9ed6, 0x46d2, |
|
| 146 |
+ [8]byte{0xa9, 0x81, 0xf8, 0x22, 0x6c, 0x83, 0x63, 0xbd},
|
|
| 147 |
+ } |
|
| 148 |
+ |
|
| 149 |
+ ret, _, _ := proc.Call(uintptr(unsafe.Pointer(&guid)), 0, 0, uintptr(unsafe.Pointer(&providerHandle))) |
|
| 150 |
+ if ret != win32CallSuccess {
|
|
| 151 |
+ errorMessage := fmt.Sprintf("Failed to register ETW provider. Error: %d", ret)
|
|
| 152 |
+ logrus.Error(errorMessage) |
|
| 153 |
+ return errors.New(errorMessage) |
|
| 154 |
+ } |
|
| 155 |
+ return nil |
|
| 156 |
+} |
|
| 157 |
+ |
|
| 158 |
+func callEventWriteString(message string) error {
|
|
| 159 |
+ proc, err := win32Lib.FindProc("EventWriteString")
|
|
| 160 |
+ if err != nil {
|
|
| 161 |
+ return err |
|
| 162 |
+ } |
|
| 163 |
+ ret, _, _ := proc.Call(uintptr(providerHandle), 0, 0, uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(message)))) |
|
| 164 |
+ if ret != win32CallSuccess {
|
|
| 165 |
+ errorMessage := fmt.Sprintf("ETWLogs provider failed to log message. Error: %d", ret)
|
|
| 166 |
+ logrus.Error(errorMessage) |
|
| 167 |
+ return errors.New(errorMessage) |
|
| 168 |
+ } |
|
| 169 |
+ return nil |
|
| 170 |
+} |
|
| 171 |
+ |
|
| 172 |
+func callEventUnregister() bool {
|
|
| 173 |
+ proc, err := win32Lib.FindProc("EventUnregister")
|
|
| 174 |
+ if err != nil {
|
|
| 175 |
+ return false |
|
| 176 |
+ } |
|
| 177 |
+ ret, _, _ := proc.Call(uintptr(providerHandle)) |
|
| 178 |
+ if ret != win32CallSuccess {
|
|
| 179 |
+ return false |
|
| 180 |
+ } |
|
| 181 |
+ return true |
|
| 182 |
+} |
| 0 | 183 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,69 @@ |
| 0 |
+<!--[metadata]> |
|
| 1 |
+title = "ETW logging driver" |
|
| 2 |
+description = "Describes how to use the etwlogs logging driver." |
|
| 3 |
+keywords = ["ETW, docker, logging, driver"] |
|
| 4 |
+[menu.main] |
|
| 5 |
+parent = "smn_logging" |
|
| 6 |
+weight=2 |
|
| 7 |
+<![end-metadata]--> |
|
| 8 |
+ |
|
| 9 |
+ |
|
| 10 |
+# ETW logging driver |
|
| 11 |
+ |
|
| 12 |
+The ETW logging driver forwards container logs as ETW events. |
|
| 13 |
+ETW stands for Event Tracing in Windows, and is the common framework |
|
| 14 |
+for tracing applications in Windows. Each ETW event contains a message |
|
| 15 |
+with both the log and its context information. A client can then create |
|
| 16 |
+an ETW listener to listen to these events. |
|
| 17 |
+ |
|
| 18 |
+The ETW provider that this logging driver registers with Windows, has the |
|
| 19 |
+GUID identifier of: `{a3693192-9ed6-46d2-a981-f8226c8363bd}`. A client creates an
|
|
| 20 |
+ETW listener and registers to listen to events from the logging driver's provider. |
|
| 21 |
+It does not matter the order in which the provider and listener are created. |
|
| 22 |
+A client can create their ETW listener and start listening for events from the provider, |
|
| 23 |
+before the provider has been registered with the system. |
|
| 24 |
+ |
|
| 25 |
+## Usage |
|
| 26 |
+ |
|
| 27 |
+Here is an example of how to listen to these events using the logman utility program |
|
| 28 |
+included in most installations of Windows: |
|
| 29 |
+ |
|
| 30 |
+ 1. `logman start -ets DockerContainerLogs -p {a3693192-9ed6-46d2-a981-f8226c8363bd} 0 0 -o trace.etl`
|
|
| 31 |
+ 2. Run your container(s) with the etwlogs driver, by adding `--log-driver=etwlogs` |
|
| 32 |
+ to the Docker run command, and generate log messages. |
|
| 33 |
+ 3. `logman stop -ets DockerContainerLogs` |
|
| 34 |
+ 4. This will generate an etl file that contains the events. One way to convert this file into |
|
| 35 |
+ human-readable form is to run: `tracerpt -y trace.etl`. |
|
| 36 |
+ |
|
| 37 |
+Each ETW event will contain a structured message string in this format: |
|
| 38 |
+ |
|
| 39 |
+ container_name: %s, image_name: %s, container_id: %s, image_id: %s, source: [stdout | stderr], log: %s |
|
| 40 |
+ |
|
| 41 |
+Details on each item in the message can be found below: |
|
| 42 |
+ |
|
| 43 |
+| Field | Description | |
|
| 44 |
+-----------------------|-------------------------------------------------| |
|
| 45 |
+| `container_name` | The container name at the time it was started. | |
|
| 46 |
+| `image_name` | The name of the container's image. | |
|
| 47 |
+| `container_id` | The full 64-character container ID. | |
|
| 48 |
+| `image_id` | The full ID of the container's image. | |
|
| 49 |
+| `source` | `stdout` or `stderr`. | |
|
| 50 |
+| `log` | The container log message. | |
|
| 51 |
+ |
|
| 52 |
+Here is an example event message: |
|
| 53 |
+ |
|
| 54 |
+ container_name: backstabbing_spence, |
|
| 55 |
+ image_name: windowsservercore, |
|
| 56 |
+ container_id: f14bb55aa862d7596b03a33251c1be7dbbec8056bbdead1da8ec5ecebbe29731, |
|
| 57 |
+ image_id: sha256:2f9e19bd998d3565b4f345ac9aaf6e3fc555406239a4fb1b1ba879673713824b, |
|
| 58 |
+ source: stdout, |
|
| 59 |
+ log: Hello world! |
|
| 60 |
+ |
|
| 61 |
+A client can parse this message string to get both the log message, as well as its |
|
| 62 |
+context information. Note that the time stamp is also available within the ETW event. |
|
| 63 |
+ |
|
| 64 |
+**Note** This ETW provider emits only a message string, and not a specially |
|
| 65 |
+structured ETW event. Therefore, it is not required to register a manifest file |
|
| 66 |
+with the system to read and interpret its ETW events. |
| ... | ... |
@@ -26,6 +26,7 @@ container's logging driver. The following options are supported: |
| 26 | 26 |
| `fluentd` | Fluentd logging driver for Docker. Writes log messages to `fluentd` (forward input). | |
| 27 | 27 |
| `awslogs` | Amazon CloudWatch Logs logging driver for Docker. Writes log messages to Amazon CloudWatch Logs. | |
| 28 | 28 |
| `splunk` | Splunk logging driver for Docker. Writes log messages to `splunk` using HTTP Event Collector. | |
| 29 |
+| `etwlogs` | ETW logging driver for Docker on Windows. Writes log messages as ETW events. | |
|
| 29 | 30 |
|
| 30 | 31 |
The `docker logs`command is available only for the `json-file` and `journald` |
| 31 | 32 |
logging drivers. |
| ... | ... |
@@ -204,3 +205,12 @@ The Splunk logging driver requires the following options: |
| 204 | 204 |
|
| 205 | 205 |
For detailed information about working with this logging driver, see the [Splunk logging driver](splunk.md) |
| 206 | 206 |
reference documentation. |
| 207 |
+ |
|
| 208 |
+## ETW logging driver options |
|
| 209 |
+ |
|
| 210 |
+The etwlogs logging driver does not require any options to be specified. This logging driver will forward each log message |
|
| 211 |
+as an ETW event. An ETW listener can then be created to listen for these events. |
|
| 212 |
+ |
|
| 213 |
+For detailed information on working with this logging driver, see [the ETW logging driver](etwlogs.md) reference documentation. |
|
| 214 |
+ |
|
| 215 |
+ |
| ... | ... |
@@ -402,7 +402,7 @@ Json Parameters: |
| 402 | 402 |
systems, such as SELinux. |
| 403 | 403 |
- **LogConfig** - Log configuration for the container, specified as a JSON object in the form |
| 404 | 404 |
`{ "Type": "<driver_name>", "Config": {"key1": "val1"}}`.
|
| 405 |
- Available types: `json-file`, `syslog`, `journald`, `gelf`, `awslogs`, `splunk`, `none`. |
|
| 405 |
+ Available types: `json-file`, `syslog`, `journald`, `gelf`, `fluentd`, `awslogs`, `splunk`, `etwlogs`, `none`. |
|
| 406 | 406 |
`json-file` logging driver. |
| 407 | 407 |
- **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. |
| 408 | 408 |
- **VolumeDriver** - Driver that this container users to mount volumes. |
| ... | ... |
@@ -214,7 +214,7 @@ millions of trillions. |
| 214 | 214 |
Add link to another container in the form of <name or id>:alias or just |
| 215 | 215 |
<name or id> in which case the alias will match the name. |
| 216 | 216 |
|
| 217 |
-**--log-driver**="*json-file*|*syslog*|*journald*|*gelf*|*fluentd*|*awslogs*|*splunk*|*none*" |
|
| 217 |
+**--log-driver**="*json-file*|*syslog*|*journald*|*gelf*|*fluentd*|*awslogs*|*splunk*|*etwlogs*|*none*" |
|
| 218 | 218 |
Logging driver for container. Default is defined by daemon `--log-driver` flag. |
| 219 | 219 |
**Warning**: the `docker logs` command works only for the `json-file` and |
| 220 | 220 |
`journald` logging drivers. |
| ... | ... |
@@ -185,7 +185,7 @@ unix://[/path/to/socket] to use. |
| 185 | 185 |
**--label**="[]" |
| 186 | 186 |
Set key=value labels to the daemon (displayed in `docker info`) |
| 187 | 187 |
|
| 188 |
-**--log-driver**="*json-file*|*syslog*|*journald*|*gelf*|*fluentd*|*awslogs*|*none*" |
|
| 188 |
+**--log-driver**="*json-file*|*syslog*|*journald*|*gelf*|*fluentd*|*awslogs*|*splunk*|*etwlogs*|*none*" |
|
| 189 | 189 |
Default driver for container logs. Default is `json-file`. |
| 190 | 190 |
**Warning**: `docker logs` command works only for `json-file` logging driver. |
| 191 | 191 |
|
| ... | ... |
@@ -320,7 +320,7 @@ container can access the exposed port via a private networking interface. Docker |
| 320 | 320 |
will set some environment variables in the client container to help indicate |
| 321 | 321 |
which interface and port to use. |
| 322 | 322 |
|
| 323 |
-**--log-driver**="*json-file*|*syslog*|*journald*|*gelf*|*fluentd*|*awslogs*|*splunk*|*none*" |
|
| 323 |
+**--log-driver**="*json-file*|*syslog*|*journald*|*gelf*|*fluentd*|*awslogs*|*splunk*|*etwlogs*|*none*" |
|
| 324 | 324 |
Logging driver for container. Default is defined by daemon `--log-driver` flag. |
| 325 | 325 |
**Warning**: the `docker logs` command works only for the `json-file` and |
| 326 | 326 |
`journald` logging drivers. |