Browse code

Add support for reading logs extra attrs

The jsonlog logger currently allows specifying envs and labels that
should be propagated to the log message, however there has been no way
to read that back.

This adds a new API option to enable inserting these attrs back to the
log reader.

With timestamps, this looks like so:
```
92016-04-08T15:28:09.835913720Z foo=bar,hello=world hello
```

The extra attrs are comma separated before the log message but after
timestamps.

Without timestaps it looks like so:
```
foo=bar,hello=world hello
```

Signed-off-by: Brian Goff <cpuguy83@gmail.com>

Brian Goff authored on 2016/04/09 01:15:08
Showing 12 changed files
... ...
@@ -25,6 +25,7 @@ func (cli *DockerCli) CmdLogs(args ...string) error {
25 25
 	follow := cmd.Bool([]string{"f", "-follow"}, false, "Follow log output")
26 26
 	since := cmd.String([]string{"-since"}, "", "Show logs since timestamp")
27 27
 	times := cmd.Bool([]string{"t", "-timestamps"}, false, "Show timestamps")
28
+	details := cmd.Bool([]string{"-details"}, false, "Show extra details provided to logs")
28 29
 	tail := cmd.String([]string{"-tail"}, "all", "Number of lines to show from the end of the logs")
29 30
 	cmd.Require(flag.Exact, 1)
30 31
 
... ...
@@ -48,6 +49,7 @@ func (cli *DockerCli) CmdLogs(args ...string) error {
48 48
 		Timestamps: *times,
49 49
 		Follow:     *follow,
50 50
 		Tail:       *tail,
51
+		Details:    *details,
51 52
 	}
52 53
 	responseBody, err := cli.client.ContainerLogs(context.Background(), name, options)
53 54
 	if err != nil {
... ...
@@ -99,6 +99,7 @@ func (s *containerRouter) getContainersLogs(ctx context.Context, w http.Response
99 99
 			Tail:       r.Form.Get("tail"),
100 100
 			ShowStdout: stdout,
101 101
 			ShowStderr: stderr,
102
+			Details:    httputils.BoolValue(r, "details"),
102 103
 		},
103 104
 		OutStream: w,
104 105
 	}
... ...
@@ -27,6 +27,7 @@ func decodeLogLine(dec *json.Decoder, l *jsonlog.JSONLog) (*logger.Message, erro
27 27
 		Source:    l.Stream,
28 28
 		Timestamp: l.Created,
29 29
 		Line:      []byte(l.Log),
30
+		Attrs:     l.Attrs,
30 31
 	}
31 32
 	return msg, nil
32 33
 }
... ...
@@ -9,6 +9,8 @@ package logger
9 9
 
10 10
 import (
11 11
 	"errors"
12
+	"sort"
13
+	"strings"
12 14
 	"time"
13 15
 
14 16
 	"github.com/docker/docker/pkg/jsonlog"
... ...
@@ -29,6 +31,31 @@ type Message struct {
29 29
 	Line        []byte
30 30
 	Source      string
31 31
 	Timestamp   time.Time
32
+	Attrs       LogAttributes
33
+}
34
+
35
+// LogAttributes is used to hold the extra attributes available in the log message
36
+// Primarily used for converting the map type to string and sorting.
37
+type LogAttributes map[string]string
38
+type byKey []string
39
+
40
+func (s byKey) Len() int { return len(s) }
41
+func (s byKey) Less(i, j int) bool {
42
+	keyI := strings.Split(s[i], "=")
43
+	keyJ := strings.Split(s[j], "=")
44
+	return keyI[0] < keyJ[0]
45
+}
46
+func (s byKey) Swap(i, j int) {
47
+	s[i], s[j] = s[j], s[i]
48
+}
49
+
50
+func (a LogAttributes) String() string {
51
+	var ss byKey
52
+	for k, v := range a {
53
+		ss = append(ss, k+"="+v)
54
+	}
55
+	sort.Sort(ss)
56
+	return strings.Join(ss, ",")
32 57
 }
33 58
 
34 59
 // Logger is the interface for docker logging drivers.
... ...
@@ -90,6 +90,9 @@ func (daemon *Daemon) ContainerLogs(ctx context.Context, containerName string, c
90 90
 				return nil
91 91
 			}
92 92
 			logLine := msg.Line
93
+			if config.Details {
94
+				logLine = append([]byte(msg.Attrs.String()+" "), logLine...)
95
+			}
93 96
 			if config.Timestamps {
94 97
 				logLine = append([]byte(msg.Timestamp.Format(logger.TimeFormat)+" "), logLine...)
95 98
 			}
... ...
@@ -136,6 +136,7 @@ This section lists each version from latest to oldest.  Each listing includes a
136 136
 * `POST /auth` now returns an `IdentityToken` when supported by a registry.
137 137
 * `POST /containers/create` with both `Hostname` and `Domainname` fields specified will result in the container's hostname being set to `Hostname`, rather than `Hostname.Domainname`.
138 138
 * `GET /volumes` now supports more filters, new added filters are `name` and `driver`.
139
+* `GET /containers/(id or name)/logs` now accepts a `details` query parameter to stream the extra attributes that were provided to the containers `LogOpts`, such as environment variables and labels, with the logs.
139 140
 
140 141
 ### v1.22 API changes
141 142
 
... ...
@@ -770,6 +770,7 @@ Get `stdout` and `stderr` logs from the container ``id``
770 770
 
771 771
 Query Parameters:
772 772
 
773
+-   **details** - 1/True/true or 0/False/flase, Show extra details provided to logs. Default `false`.
773 774
 -   **follow** – 1/True/true or 0/False/false, return stream. Default `false`.
774 775
 -   **stdout** – 1/True/true or 0/False/false, show `stdout` log. Default `false`.
775 776
 -   **stderr** – 1/True/true or 0/False/false, show `stderr` log. Default `false`.
... ...
@@ -14,6 +14,7 @@ parent = "smn_cli"
14 14
 
15 15
     Fetch the logs of a container
16 16
 
17
+      --details                 Show extra details provided to logs
17 18
       -f, --follow              Follow log output
18 19
       --help                    Print usage
19 20
       --since=""                Show logs since timestamp
... ...
@@ -36,6 +37,10 @@ The `docker logs --timestamps` command will add an [RFC3339Nano timestamp](https
36 36
 log entry. To ensure that the timestamps are aligned the
37 37
 nano-second part of the timestamp will be padded with zero when necessary.
38 38
 
39
+The `docker logs --details` command will add on extra attributes, such as
40
+environment variables and labels, provided to `--log-opt` when creating the
41
+container.
42
+
39 43
 The `--since` option shows only the container logs generated after
40 44
 a given date. You can specify the date as an RFC 3339 date, a UNIX
41 45
 timestamp, or a Go duration string (e.g. `1m30s`, `3h`). Besides RFC3339 date
... ...
@@ -307,3 +307,16 @@ func (s *DockerSuite) TestLogsCLIContainerNotFound(c *check.C) {
307 307
 	message := fmt.Sprintf("Error: No such container: %s\n", name)
308 308
 	c.Assert(out, checker.Equals, message)
309 309
 }
310
+
311
+func (s *DockerSuite) TestLogsWithDetails(c *check.C) {
312
+	dockerCmd(c, "run", "--name=test", "--label", "foo=bar", "-e", "baz=qux", "--log-opt", "labels=foo", "--log-opt", "env=baz", "busybox", "echo", "hello")
313
+	out, _ := dockerCmd(c, "logs", "--details", "--timestamps", "test")
314
+
315
+	logFields := strings.Fields(strings.TrimSpace(out))
316
+	c.Assert(len(logFields), checker.Equals, 3, check.Commentf(out))
317
+
318
+	details := strings.Split(logFields[1], ",")
319
+	c.Assert(details, checker.HasLen, 2)
320
+	c.Assert(details[0], checker.Equals, "baz=qux")
321
+	c.Assert(details[1], checker.Equals, "foo=bar")
322
+}
... ...
@@ -30,6 +30,9 @@ logging drivers.
30 30
 **--help**
31 31
   Print usage statement
32 32
 
33
+**--details**=*true*|*false*
34
+   Show extra details provided to logs
35
+
33 36
 **-f**, **--follow**=*true*|*false*
34 37
    Follow log output. The default is *false*.
35 38
 
... ...
@@ -55,6 +58,10 @@ epoch or Unix time), and the optional .nanoseconds field is a fraction of a
55 55
 second no more than nine digits long. You can combine the `--since` option with
56 56
 either or both of the `--follow` or `--tail` options.
57 57
 
58
+The `docker logs --details` command will add on extra attributes, such as
59
+environment variables and labels, provided to `--log-opt` when creating the
60
+container.
61
+
58 62
 # HISTORY
59 63
 April 2014, Originally compiled by William Henry (whenry at redhat dot com)
60 64
 based on docker.com source material and internal work.
... ...
@@ -15,6 +15,8 @@ type JSONLog struct {
15 15
 	Stream string `json:"stream,omitempty"`
16 16
 	// Created is the created timestamp of log
17 17
 	Created time.Time `json:"time"`
18
+	// Attrs is the list of extra attributes provided by the user
19
+	Attrs map[string]string `json:"attrs,omitempty"`
18 20
 }
19 21
 
20 22
 // Format returns the log formatted according to format
... ...
@@ -6,18 +6,18 @@ import (
6 6
 )
7 7
 
8 8
 func TestJSONLogMarshalJSON(t *testing.T) {
9
-	logs := map[JSONLog]string{
10
-		JSONLog{Log: `"A log line with \\"`}:           `^{\"log\":\"\\\"A log line with \\\\\\\\\\\"\",\"time\":\".{20,}\"}$`,
11
-		JSONLog{Log: "A log line"}:                     `^{\"log\":\"A log line\",\"time\":\".{20,}\"}$`,
12
-		JSONLog{Log: "A log line with \r"}:             `^{\"log\":\"A log line with \\r\",\"time\":\".{20,}\"}$`,
13
-		JSONLog{Log: "A log line with & < >"}:          `^{\"log\":\"A log line with \\u0026 \\u003c \\u003e\",\"time\":\".{20,}\"}$`,
14
-		JSONLog{Log: "A log line with utf8 : 🚀 ψ ω β"}: `^{\"log\":\"A log line with utf8 : 🚀 ψ ω β\",\"time\":\".{20,}\"}$`,
15
-		JSONLog{Stream: "stdout"}:                      `^{\"stream\":\"stdout\",\"time\":\".{20,}\"}$`,
16
-		JSONLog{}:                                      `^{\"time\":\".{20,}\"}$`,
9
+	logs := map[*JSONLog]string{
10
+		&JSONLog{Log: `"A log line with \\"`}:           `^{\"log\":\"\\\"A log line with \\\\\\\\\\\"\",\"time\":\".{20,}\"}$`,
11
+		&JSONLog{Log: "A log line"}:                     `^{\"log\":\"A log line\",\"time\":\".{20,}\"}$`,
12
+		&JSONLog{Log: "A log line with \r"}:             `^{\"log\":\"A log line with \\r\",\"time\":\".{20,}\"}$`,
13
+		&JSONLog{Log: "A log line with & < >"}:          `^{\"log\":\"A log line with \\u0026 \\u003c \\u003e\",\"time\":\".{20,}\"}$`,
14
+		&JSONLog{Log: "A log line with utf8 : 🚀 ψ ω β"}: `^{\"log\":\"A log line with utf8 : 🚀 ψ ω β\",\"time\":\".{20,}\"}$`,
15
+		&JSONLog{Stream: "stdout"}:                      `^{\"stream\":\"stdout\",\"time\":\".{20,}\"}$`,
16
+		&JSONLog{}:                                      `^{\"time\":\".{20,}\"}$`,
17 17
 		// These ones are a little weird
18
-		JSONLog{Log: "\u2028 \u2029"}:      `^{\"log\":\"\\u2028 \\u2029\",\"time\":\".{20,}\"}$`,
19
-		JSONLog{Log: string([]byte{0xaF})}: `^{\"log\":\"\\ufffd\",\"time\":\".{20,}\"}$`,
20
-		JSONLog{Log: string([]byte{0x7F})}: `^{\"log\":\"\x7f\",\"time\":\".{20,}\"}$`,
18
+		&JSONLog{Log: "\u2028 \u2029"}:      `^{\"log\":\"\\u2028 \\u2029\",\"time\":\".{20,}\"}$`,
19
+		&JSONLog{Log: string([]byte{0xaF})}: `^{\"log\":\"\\ufffd\",\"time\":\".{20,}\"}$`,
20
+		&JSONLog{Log: string([]byte{0x7F})}: `^{\"log\":\"\x7f\",\"time\":\".{20,}\"}$`,
21 21
 	}
22 22
 	for jsonLog, expression := range logs {
23 23
 		data, err := jsonLog.MarshalJSON()