Browse code

Add log reading to the journald log driver

If a logdriver doesn't register a callback function to validate log
options, it won't be usable. Fix the journald driver by adding a dummy
validator.

Teach the client and the daemon's "logs" logic that the server can also
supply "logs" data via the "journald" driver. Update documentation and
tests that depend on error messages.

Add support for reading log data from the systemd journal to the
journald log driver. The internal logic uses a goroutine to scan the
journal for matching entries after any specified cutoff time, formats
the messages from those entries as JSONLog messages, and stuffs the
results down a pipe whose reading end we hand back to the caller.

If we are missing any of the 'linux', 'cgo', or 'journald' build tags,
however, we don't implement a reader, so the 'logs' endpoint will still
return an error.

Make the necessary changes to the build setup to ensure that support for
reading container logs from the systemd journal is built.

Rename the Jmap member of the journald logdriver's struct to "vars" to
make it non-public, and to make it easier to tell that it's just there
to hold additional variable values that we want journald to record along
with log data that we're sending to it.

In the client, don't assume that we know which logdrivers the server
implements, and remove the check that looks at the server. It's
redundant because the server already knows, and the check also makes
using older clients with newer servers (which may have new logdrivers in
them) unnecessarily hard.

When we try to "logs" and have to report that the container's logdriver
doesn't support reading, send the error message through the
might-be-a-multiplexer so that clients which are expecting multiplexed
data will be able to properly display the error, instead of tripping
over the data and printing a less helpful "Unrecognized input header"
error.

Signed-off-by: Nalin Dahyabhai <nalin@redhat.com> (github: nalind)

Nalin Dahyabhai authored on 2015/07/24 00:02:56
Showing 27 changed files
... ...
@@ -46,8 +46,10 @@ RUN apt-get update && apt-get install -y \
46 46
 	libapparmor-dev \
47 47
 	libcap-dev \
48 48
 	libsqlite3-dev \
49
+	libsystemd-journal-dev \
49 50
 	mercurial \
50 51
 	parallel \
52
+	pkg-config \
51 53
 	python-mock \
52 54
 	python-pip \
53 55
 	python-websocket \
... ...
@@ -2,7 +2,6 @@ package client
2 2
 
3 3
 import (
4 4
 	"encoding/json"
5
-	"fmt"
6 5
 	"net/url"
7 6
 	"time"
8 7
 
... ...
@@ -37,10 +36,6 @@ func (cli *DockerCli) CmdLogs(args ...string) error {
37 37
 		return err
38 38
 	}
39 39
 
40
-	if logType := c.HostConfig.LogConfig.Type; logType != "json-file" {
41
-		return fmt.Errorf("\"logs\" command is supported only for \"json-file\" logging driver (got: %s)", logType)
42
-	}
43
-
44 40
 	v := url.Values{}
45 41
 	v.Set("stdout", "1")
46 42
 	v.Set("stderr", "1")
... ...
@@ -141,7 +141,10 @@ func (s *Server) getContainersLogs(ctx context.Context, w http.ResponseWriter, r
141 141
 	}
142 142
 
143 143
 	if err := s.daemon.ContainerLogs(c, logsConfig); err != nil {
144
-		fmt.Fprintf(w, "Error running logs job: %s\n", err)
144
+		// The client may be expecting all of the data we're sending to
145
+		// be multiplexed, so send it through OutStream, which will
146
+		// have been set up to handle that if needed.
147
+		fmt.Fprintf(logsConfig.OutStream, "Error running logs job: %s\n", err)
145 148
 	}
146 149
 
147 150
 	return nil
... ...
@@ -4,7 +4,7 @@
4 4
 
5 5
 FROM debian:jessie
6 6
 
7
-RUN apt-get update && apt-get install -y bash-completion btrfs-tools build-essential curl ca-certificates debhelper dh-systemd git libapparmor-dev libdevmapper-dev libsqlite3-dev --no-install-recommends && rm -rf /var/lib/apt/lists/*
7
+RUN apt-get update && apt-get install -y bash-completion btrfs-tools build-essential curl ca-certificates debhelper dh-systemd git libapparmor-dev libdevmapper-dev libsqlite3-dev libsystemd-journal-dev --no-install-recommends && rm -rf /var/lib/apt/lists/*
8 8
 
9 9
 ENV GO_VERSION 1.4.2
10 10
 RUN curl -fSL "https://storage.googleapis.com/golang/go${GO_VERSION}.linux-amd64.tar.gz" | tar xzC /usr/local
... ...
@@ -4,7 +4,7 @@
4 4
 
5 5
 FROM debian:stretch
6 6
 
7
-RUN apt-get update && apt-get install -y bash-completion btrfs-tools build-essential curl ca-certificates debhelper dh-systemd git libapparmor-dev libdevmapper-dev libsqlite3-dev --no-install-recommends && rm -rf /var/lib/apt/lists/*
7
+RUN apt-get update && apt-get install -y bash-completion btrfs-tools build-essential curl ca-certificates debhelper dh-systemd git libapparmor-dev libdevmapper-dev libsqlite3-dev libsystemd-dev --no-install-recommends && rm -rf /var/lib/apt/lists/*
8 8
 
9 9
 ENV GO_VERSION 1.4.2
10 10
 RUN curl -fSL "https://storage.googleapis.com/golang/go${GO_VERSION}.linux-amd64.tar.gz" | tar xzC /usr/local
... ...
@@ -4,7 +4,7 @@
4 4
 
5 5
 FROM debian:wheezy-backports
6 6
 
7
-RUN apt-get update && apt-get install -y bash-completion btrfs-tools/wheezy-backports build-essential curl ca-certificates debhelper dh-systemd git libapparmor-dev libdevmapper-dev libsqlite3-dev --no-install-recommends && rm -rf /var/lib/apt/lists/*
7
+RUN apt-get update && apt-get install -y bash-completion btrfs-tools/wheezy-backports build-essential curl ca-certificates debhelper dh-systemd git libapparmor-dev libdevmapper-dev libsqlite3-dev libsystemd-journal-dev --no-install-recommends && rm -rf /var/lib/apt/lists/*
8 8
 
9 9
 ENV GO_VERSION 1.4.2
10 10
 RUN curl -fSL "https://storage.googleapis.com/golang/go${GO_VERSION}.linux-amd64.tar.gz" | tar xzC /usr/local
... ...
@@ -56,6 +56,12 @@ for version in "${versions[@]}"; do
56 56
 		libdevmapper-dev # for "libdevmapper.h"
57 57
 		libsqlite3-dev # for "sqlite3.h"
58 58
 	)
59
+	# packaging for "sd-journal.h" and libraries varies
60
+	case "$suite" in
61
+		precise) ;;
62
+		sid|stretch|wily) packages+=( libsystemd-dev );;
63
+		*) packages+=( libsystemd-journal-dev );;
64
+	esac
59 65
 
60 66
 	if [ "$suite" = 'precise' ]; then
61 67
 		# precise has a few package issues
... ...
@@ -4,7 +4,7 @@
4 4
 
5 5
 FROM ubuntu:trusty
6 6
 
7
-RUN apt-get update && apt-get install -y bash-completion btrfs-tools build-essential curl ca-certificates debhelper dh-systemd git libapparmor-dev libdevmapper-dev libsqlite3-dev --no-install-recommends && rm -rf /var/lib/apt/lists/*
7
+RUN apt-get update && apt-get install -y bash-completion btrfs-tools build-essential curl ca-certificates debhelper dh-systemd git libapparmor-dev libdevmapper-dev libsqlite3-dev libsystemd-journal-dev --no-install-recommends && rm -rf /var/lib/apt/lists/*
8 8
 
9 9
 ENV GO_VERSION 1.4.2
10 10
 RUN curl -fSL "https://storage.googleapis.com/golang/go${GO_VERSION}.linux-amd64.tar.gz" | tar xzC /usr/local
... ...
@@ -4,7 +4,7 @@
4 4
 
5 5
 FROM ubuntu:vivid
6 6
 
7
-RUN apt-get update && apt-get install -y bash-completion btrfs-tools build-essential curl ca-certificates debhelper dh-systemd git libapparmor-dev libdevmapper-dev libsqlite3-dev --no-install-recommends && rm -rf /var/lib/apt/lists/*
7
+RUN apt-get update && apt-get install -y bash-completion btrfs-tools build-essential curl ca-certificates debhelper dh-systemd git libapparmor-dev libdevmapper-dev libsqlite3-dev libsystemd-journal-dev --no-install-recommends && rm -rf /var/lib/apt/lists/*
8 8
 
9 9
 ENV GO_VERSION 1.4.2
10 10
 RUN curl -fSL "https://storage.googleapis.com/golang/go${GO_VERSION}.linux-amd64.tar.gz" | tar xzC /usr/local
... ...
@@ -4,7 +4,7 @@
4 4
 
5 5
 FROM ubuntu:wily
6 6
 
7
-RUN apt-get update && apt-get install -y bash-completion btrfs-tools build-essential curl ca-certificates debhelper dh-systemd git libapparmor-dev libdevmapper-dev libsqlite3-dev --no-install-recommends && rm -rf /var/lib/apt/lists/*
7
+RUN apt-get update && apt-get install -y bash-completion btrfs-tools build-essential curl ca-certificates debhelper dh-systemd git libapparmor-dev libdevmapper-dev libsqlite3-dev libsystemd-dev --no-install-recommends && rm -rf /var/lib/apt/lists/*
8 8
 
9 9
 ENV GO_VERSION 1.4.2
10 10
 RUN curl -fSL "https://storage.googleapis.com/golang/go${GO_VERSION}.linux-amd64.tar.gz" | tar xzC /usr/local
... ...
@@ -6,6 +6,7 @@ package journald
6 6
 
7 7
 import (
8 8
 	"fmt"
9
+	"sync"
9 10
 
10 11
 	"github.com/Sirupsen/logrus"
11 12
 	"github.com/coreos/go-systemd/journal"
... ...
@@ -15,13 +16,22 @@ import (
15 15
 const name = "journald"
16 16
 
17 17
 type journald struct {
18
-	Jmap map[string]string
18
+	vars    map[string]string // additional variables and values to send to the journal along with the log message
19
+	readers readerList
20
+}
21
+
22
+type readerList struct {
23
+	mu      sync.Mutex
24
+	readers map[*logger.LogWatcher]*logger.LogWatcher
19 25
 }
20 26
 
21 27
 func init() {
22 28
 	if err := logger.RegisterLogDriver(name, New); err != nil {
23 29
 		logrus.Fatal(err)
24 30
 	}
31
+	if err := logger.RegisterLogOptValidator(name, validateLogOpt); err != nil {
32
+		logrus.Fatal(err)
33
+	}
25 34
 }
26 35
 
27 36
 // New creates a journald logger using the configuration passed in on
... ...
@@ -36,22 +46,30 @@ func New(ctx logger.Context) (logger.Logger, error) {
36 36
 	if name[0] == '/' {
37 37
 		name = name[1:]
38 38
 	}
39
-	jmap := map[string]string{
39
+	vars := map[string]string{
40 40
 		"CONTAINER_ID":      ctx.ContainerID[:12],
41 41
 		"CONTAINER_ID_FULL": ctx.ContainerID,
42 42
 		"CONTAINER_NAME":    name}
43
-	return &journald{Jmap: jmap}, nil
43
+	return &journald{vars: vars, readers: readerList{readers: make(map[*logger.LogWatcher]*logger.LogWatcher)}}, nil
44 44
 }
45 45
 
46
-func (s *journald) Log(msg *logger.Message) error {
47
-	if msg.Source == "stderr" {
48
-		return journal.Send(string(msg.Line), journal.PriErr, s.Jmap)
46
+// We don't actually accept any options, but we have to supply a callback for
47
+// the factory to pass the (probably empty) configuration map to.
48
+func validateLogOpt(cfg map[string]string) error {
49
+	for key := range cfg {
50
+		switch key {
51
+		default:
52
+			return fmt.Errorf("unknown log opt '%s' for journald log driver", key)
53
+		}
49 54
 	}
50
-	return journal.Send(string(msg.Line), journal.PriInfo, s.Jmap)
55
+	return nil
51 56
 }
52 57
 
53
-func (s *journald) Close() error {
54
-	return nil
58
+func (s *journald) Log(msg *logger.Message) error {
59
+	if msg.Source == "stderr" {
60
+		return journal.Send(string(msg.Line), journal.PriErr, s.vars)
61
+	}
62
+	return journal.Send(string(msg.Line), journal.PriInfo, s.vars)
55 63
 }
56 64
 
57 65
 func (s *journald) Name() string {
58 66
new file mode 100644
... ...
@@ -0,0 +1,303 @@
0
+// +build linux,cgo,!static_build,journald
1
+
2
+package journald
3
+
4
+// #cgo pkg-config: libsystemd-journal
5
+// #include <sys/types.h>
6
+// #include <sys/poll.h>
7
+// #include <systemd/sd-journal.h>
8
+// #include <errno.h>
9
+// #include <stdio.h>
10
+// #include <stdlib.h>
11
+// #include <string.h>
12
+// #include <time.h>
13
+// #include <unistd.h>
14
+//
15
+//static int get_message(sd_journal *j, const char **msg, size_t *length)
16
+//{
17
+//	int rc;
18
+//	*msg = NULL;
19
+//	*length = 0;
20
+//	rc = sd_journal_get_data(j, "MESSAGE", (const void **) msg, length);
21
+//	if (rc == 0) {
22
+//		if (*length > 8) {
23
+//			(*msg) += 8;
24
+//			*length -= 8;
25
+//		} else {
26
+//			*msg = NULL;
27
+//			*length = 0;
28
+//			rc = -ENOENT;
29
+//		}
30
+//	}
31
+//	return rc;
32
+//}
33
+//static int get_priority(sd_journal *j, int *priority)
34
+//{
35
+//	const void *data;
36
+//	size_t i, length;
37
+//	int rc;
38
+//	*priority = -1;
39
+//	rc = sd_journal_get_data(j, "PRIORITY", &data, &length);
40
+//	if (rc == 0) {
41
+//		if ((length > 9) && (strncmp(data, "PRIORITY=", 9) == 0)) {
42
+//			*priority = 0;
43
+//			for (i = 9; i < length; i++) {
44
+//				*priority = *priority * 10 + ((const char *)data)[i] - '0';
45
+//			}
46
+//			if (length > 9) {
47
+//				rc = 0;
48
+//			}
49
+//		}
50
+//	}
51
+//	return rc;
52
+//}
53
+//static int wait_for_data_or_close(sd_journal *j, int pipefd)
54
+//{
55
+//	struct pollfd fds[2];
56
+//	uint64_t when = 0;
57
+//	int timeout, jevents, i;
58
+//	struct timespec ts;
59
+//	uint64_t now;
60
+//	do {
61
+//		memset(&fds, 0, sizeof(fds));
62
+//		fds[0].fd = pipefd;
63
+//		fds[0].events = POLLHUP;
64
+//		fds[1].fd = sd_journal_get_fd(j);
65
+//		if (fds[1].fd < 0) {
66
+//			return -1;
67
+//		}
68
+//		jevents = sd_journal_get_events(j);
69
+//		if (jevents < 0) {
70
+//			return -1;
71
+//		}
72
+//		fds[1].events = jevents;
73
+//		sd_journal_get_timeout(j, &when);
74
+//		if (when == -1) {
75
+//			timeout = -1;
76
+//		} else {
77
+//			clock_gettime(CLOCK_MONOTONIC, &ts);
78
+//			now = (uint64_t) ts.tv_sec * 1000000 + ts.tv_nsec / 1000;
79
+//			timeout = when > now ? (int) ((when - now + 999) / 1000) : 0;
80
+//		}
81
+//		i = poll(fds, 2, timeout);
82
+//		if ((i == -1) && (errno != EINTR)) {
83
+//			/* An unexpected error. */
84
+//			return -1;
85
+//		}
86
+//		if (fds[0].revents & POLLHUP) {
87
+//			/* The close notification pipe was closed. */
88
+//			return 0;
89
+//		}
90
+//		if (sd_journal_process(j) == SD_JOURNAL_APPEND) {
91
+//			/* Data, which we might care about, was appended. */
92
+//			return 1;
93
+//		}
94
+//	} while ((fds[0].revents & POLLHUP) == 0);
95
+//	return 0;
96
+//}
97
+import "C"
98
+
99
+import (
100
+	"fmt"
101
+	"time"
102
+	"unsafe"
103
+
104
+	"github.com/coreos/go-systemd/journal"
105
+	"github.com/docker/docker/daemon/logger"
106
+)
107
+
108
+func (s *journald) Close() error {
109
+	s.readers.mu.Lock()
110
+	for reader := range s.readers.readers {
111
+		reader.Close()
112
+	}
113
+	s.readers.mu.Unlock()
114
+	return nil
115
+}
116
+
117
+func (s *journald) drainJournal(logWatcher *logger.LogWatcher, config logger.ReadConfig, j *C.sd_journal, oldCursor string) string {
118
+	var msg, cursor *C.char
119
+	var length C.size_t
120
+	var stamp C.uint64_t
121
+	var priority C.int
122
+
123
+	// Walk the journal from here forward until we run out of new entries.
124
+drain:
125
+	for {
126
+		// Try not to send a given entry twice.
127
+		if oldCursor != "" {
128
+			ccursor := C.CString(oldCursor)
129
+			defer C.free(unsafe.Pointer(ccursor))
130
+			for C.sd_journal_test_cursor(j, ccursor) > 0 {
131
+				if C.sd_journal_next(j) <= 0 {
132
+					break drain
133
+				}
134
+			}
135
+		}
136
+		// Read and send the logged message, if there is one to read.
137
+		i := C.get_message(j, &msg, &length)
138
+		if i != -C.ENOENT && i != -C.EADDRNOTAVAIL {
139
+			// Read the entry's timestamp.
140
+			if C.sd_journal_get_realtime_usec(j, &stamp) != 0 {
141
+				break
142
+			}
143
+			// Set up the time and text of the entry.
144
+			timestamp := time.Unix(int64(stamp)/1000000, (int64(stamp)%1000000)*1000)
145
+			line := append(C.GoBytes(unsafe.Pointer(msg), C.int(length)), "\n"...)
146
+			// Recover the stream name by mapping
147
+			// from the journal priority back to
148
+			// the stream that we would have
149
+			// assigned that value.
150
+			source := ""
151
+			if C.get_priority(j, &priority) != 0 {
152
+				source = ""
153
+			} else if priority == C.int(journal.PriErr) {
154
+				source = "stderr"
155
+			} else if priority == C.int(journal.PriInfo) {
156
+				source = "stdout"
157
+			}
158
+			// Send the log message.
159
+			cid := s.vars["CONTAINER_ID_FULL"]
160
+			logWatcher.Msg <- &logger.Message{ContainerID: cid, Line: line, Source: source, Timestamp: timestamp}
161
+		}
162
+		// If we're at the end of the journal, we're done (for now).
163
+		if C.sd_journal_next(j) <= 0 {
164
+			break
165
+		}
166
+	}
167
+	retCursor := ""
168
+	if C.sd_journal_get_cursor(j, &cursor) == 0 {
169
+		retCursor = C.GoString(cursor)
170
+		C.free(unsafe.Pointer(cursor))
171
+	}
172
+	return retCursor
173
+}
174
+
175
+func (s *journald) followJournal(logWatcher *logger.LogWatcher, config logger.ReadConfig, j *C.sd_journal, pfd [2]C.int, cursor string) {
176
+	go func() {
177
+		// Keep copying journal data out until we're notified to stop.
178
+		for C.wait_for_data_or_close(j, pfd[0]) == 1 {
179
+			cursor = s.drainJournal(logWatcher, config, j, cursor)
180
+		}
181
+		// Clean up.
182
+		C.close(pfd[0])
183
+		s.readers.mu.Lock()
184
+		delete(s.readers.readers, logWatcher)
185
+		s.readers.mu.Unlock()
186
+	}()
187
+	s.readers.mu.Lock()
188
+	s.readers.readers[logWatcher] = logWatcher
189
+	s.readers.mu.Unlock()
190
+	// Wait until we're told to stop.
191
+	select {
192
+	case <-logWatcher.WatchClose():
193
+		// Notify the other goroutine that its work is done.
194
+		C.close(pfd[1])
195
+	}
196
+}
197
+
198
+func (s *journald) readLogs(logWatcher *logger.LogWatcher, config logger.ReadConfig) {
199
+	var j *C.sd_journal
200
+	var cmatch *C.char
201
+	var stamp C.uint64_t
202
+	var sinceUnixMicro uint64
203
+	var pipes [2]C.int
204
+	cursor := ""
205
+
206
+	defer close(logWatcher.Msg)
207
+	// Get a handle to the journal.
208
+	rc := C.sd_journal_open(&j, C.int(0))
209
+	if rc != 0 {
210
+		logWatcher.Err <- fmt.Errorf("error opening journal")
211
+		return
212
+	}
213
+	defer C.sd_journal_close(j)
214
+	// Remove limits on the size of data items that we'll retrieve.
215
+	rc = C.sd_journal_set_data_threshold(j, C.size_t(0))
216
+	if rc != 0 {
217
+		logWatcher.Err <- fmt.Errorf("error setting journal data threshold")
218
+		return
219
+	}
220
+	// Add a match to have the library do the searching for us.
221
+	cmatch = C.CString("CONTAINER_ID_FULL=" + s.vars["CONTAINER_ID_FULL"])
222
+	if cmatch == nil {
223
+		logWatcher.Err <- fmt.Errorf("error reading container ID")
224
+		return
225
+	}
226
+	defer C.free(unsafe.Pointer(cmatch))
227
+	rc = C.sd_journal_add_match(j, unsafe.Pointer(cmatch), C.strlen(cmatch))
228
+	if rc != 0 {
229
+		logWatcher.Err <- fmt.Errorf("error setting journal match")
230
+		return
231
+	}
232
+	// If we have a cutoff time, convert it to Unix time once.
233
+	if !config.Since.IsZero() {
234
+		nano := config.Since.UnixNano()
235
+		sinceUnixMicro = uint64(nano / 1000)
236
+	}
237
+	if config.Tail > 0 {
238
+		lines := config.Tail
239
+		// Start at the end of the journal.
240
+		if C.sd_journal_seek_tail(j) < 0 {
241
+			logWatcher.Err <- fmt.Errorf("error seeking to end of journal")
242
+			return
243
+		}
244
+		if C.sd_journal_previous(j) < 0 {
245
+			logWatcher.Err <- fmt.Errorf("error backtracking to previous journal entry")
246
+			return
247
+		}
248
+		// Walk backward.
249
+		for lines > 0 {
250
+			// Stop if the entry time is before our cutoff.
251
+			// We'll need the entry time if it isn't, so go
252
+			// ahead and parse it now.
253
+			if C.sd_journal_get_realtime_usec(j, &stamp) != 0 {
254
+				break
255
+			} else {
256
+				// Compare the timestamp on the entry
257
+				// to our threshold value.
258
+				if sinceUnixMicro != 0 && sinceUnixMicro > uint64(stamp) {
259
+					break
260
+				}
261
+			}
262
+			lines--
263
+			// If we're at the start of the journal, or
264
+			// don't need to back up past any more entries,
265
+			// stop.
266
+			if lines == 0 || C.sd_journal_previous(j) <= 0 {
267
+				break
268
+			}
269
+		}
270
+	} else {
271
+		// Start at the beginning of the journal.
272
+		if C.sd_journal_seek_head(j) < 0 {
273
+			logWatcher.Err <- fmt.Errorf("error seeking to start of journal")
274
+			return
275
+		}
276
+		// If we have a cutoff date, fast-forward to it.
277
+		if sinceUnixMicro != 0 && C.sd_journal_seek_realtime_usec(j, C.uint64_t(sinceUnixMicro)) != 0 {
278
+			logWatcher.Err <- fmt.Errorf("error seeking to start time in journal")
279
+			return
280
+		}
281
+		if C.sd_journal_next(j) < 0 {
282
+			logWatcher.Err <- fmt.Errorf("error skipping to next journal entry")
283
+			return
284
+		}
285
+	}
286
+	cursor = s.drainJournal(logWatcher, config, j, "")
287
+	if config.Follow {
288
+		// Create a pipe that we can poll at the same time as the journald descriptor.
289
+		if C.pipe(&pipes[0]) == C.int(-1) {
290
+			logWatcher.Err <- fmt.Errorf("error opening journald close notification pipe")
291
+		} else {
292
+			s.followJournal(logWatcher, config, j, pipes, cursor)
293
+		}
294
+	}
295
+	return
296
+}
297
+
298
+func (s *journald) ReadLogs(config logger.ReadConfig) *logger.LogWatcher {
299
+	logWatcher := logger.NewLogWatcher()
300
+	go s.readLogs(logWatcher, config)
301
+	return logWatcher
302
+}
0 303
new file mode 100644
... ...
@@ -0,0 +1,7 @@
0
+// +build !linux !cgo static_build !journald
1
+
2
+package journald
3
+
4
+func (s *journald) Close() error {
5
+	return nil
6
+}
... ...
@@ -41,6 +41,7 @@ func (daemon *Daemon) ContainerLogs(container *Container, config *ContainerLogsC
41 41
 		errStream = stdcopy.NewStdWriter(outStream, stdcopy.Stderr)
42 42
 		outStream = stdcopy.NewStdWriter(outStream, stdcopy.Stdout)
43 43
 	}
44
+	config.OutStream = outStream
44 45
 
45 46
 	cLog, err := container.getLogger()
46 47
 	if err != nil {
... ...
@@ -279,7 +279,7 @@ Json Parameters:
279 279
         systems, such as SELinux.
280 280
     -   **LogConfig** - Log configuration for the container, specified as
281 281
           `{ "Type": "<driver_name>", "Config": {"key1": "val1"}}`.
282
-          Available types: `json-file`, `syslog`, `none`.
282
+          Available types: `json-file`, `syslog`, `journald`, `none`.
283 283
           `json-file` logging driver.
284 284
     -   **CgroupParent** - Path to cgroups under which the cgroup for the container will be created. If the path is not absolute, the path is considered to be relative to the cgroups path of the init process. Cgroups will be created if they do not already exist.
285 285
 
... ...
@@ -480,7 +480,7 @@ Status Codes:
480 480
 Get stdout and stderr logs from the container ``id``
481 481
 
482 482
 > **Note**:
483
-> This endpoint works only for containers with `json-file` logging driver.
483
+> This endpoint works only for containers with the `json-file` or `journald` logging drivers.
484 484
 
485 485
 **Example request**:
486 486
 
... ...
@@ -493,7 +493,7 @@ Status Codes:
493 493
 Get `stdout` and `stderr` logs from the container ``id``
494 494
 
495 495
 > **Note**:
496
-> This endpoint works only for containers with `json-file` logging driver.
496
+> This endpoint works only for containers with the `json-file` or `journald` logging drivers.
497 497
 
498 498
 **Example request**:
499 499
 
... ...
@@ -504,7 +504,7 @@ Status Codes:
504 504
 Get `stdout` and `stderr` logs from the container ``id``
505 505
 
506 506
 > **Note**:
507
-> This endpoint works only for containers with `json-file` logging driver.
507
+> This endpoint works only for containers with the `json-file` or `journald` logging drivers.
508 508
 
509 509
 **Example request**:
510 510
 
... ...
@@ -20,8 +20,8 @@ weight=1
20 20
       -t, --timestamps=false    Show timestamps
21 21
       --tail="all"              Number of lines to show from the end of the logs
22 22
 
23
-NOTE: this command is available only for containers with `json-file` logging
24
-driver.
23
+NOTE: this command is available only for containers with `json-file` and
24
+`journald` logging drivers.
25 25
 
26 26
 The `docker logs` command batch-retrieves logs present at the time of execution.
27 27
 
... ...
@@ -12,7 +12,7 @@ parent = "smn_logging"
12 12
 
13 13
 The `journald` logging driver sends container logs to the [systemd
14 14
 journal](http://www.freedesktop.org/software/systemd/man/systemd-journald.service.html).  Log entries can be retrieved using the `journalctl`
15
-command or through use of the journal API.
15
+command, through use of the journal API, or using the `docker logs` command.
16 16
 
17 17
 In addition to the text of the log message itself, the `journald` log
18 18
 driver stores the following metadata in the journal with each message:
... ...
@@ -1013,8 +1013,8 @@ container's logging driver. The following options are supported:
1013 1013
 | `fluentd`   | Fluentd logging driver for Docker. Writes log messages to `fluentd` (forward input).                                          |
1014 1014
 | `awslogs`   | Amazon CloudWatch Logs logging driver for Docker. Writes log messages to Amazon CloudWatch Logs                               |
1015 1015
 
1016
-	The `docker logs`command is available only for the `json-file` logging
1017
-driver.  For detailed information on working with logging drivers, see
1016
+The `docker logs` command is available only for the `json-file` and `journald`
1017
+logging drivers.  For detailed information on working with logging drivers, see
1018 1018
 [Configure a logging driver](/reference/logging/overview/).
1019 1019
 
1020 1020
 
... ...
@@ -104,6 +104,9 @@ fi
104 104
 
105 105
 if [ -z "$DOCKER_CLIENTONLY" ]; then
106 106
 	DOCKER_BUILDTAGS+=" daemon"
107
+	if pkg-config libsystemd-journal 2> /dev/null ; then
108
+		DOCKER_BUILDTAGS+=" journald"
109
+	fi
107 110
 fi
108 111
 
109 112
 if [ "$DOCKER_EXECDRIVER" = 'lxc' ]; then
... ...
@@ -26,6 +26,7 @@ Packager: Docker <support@docker.com>
26 26
 # only require systemd on those systems
27 27
 %if 0%{?is_systemd}
28 28
 BuildRequires: pkgconfig(systemd)
29
+BuildRequires: pkgconfig(libsystemd-journal)
29 30
 Requires: systemd-units
30 31
 %else
31 32
 Requires(post): chkconfig
... ...
@@ -1196,11 +1196,11 @@ func (s *DockerDaemonSuite) TestDaemonLoggingDriverNoneLogsError(c *check.C) {
1196 1196
 	}
1197 1197
 	id := strings.TrimSpace(out)
1198 1198
 	out, err = s.d.Cmd("logs", id)
1199
-	if err == nil {
1200
-		c.Fatalf("Logs should fail with \"none\" driver")
1199
+	if err != nil {
1200
+		c.Fatalf("Logs request should be sent and then fail with \"none\" driver")
1201 1201
 	}
1202
-	if !strings.Contains(out, `"logs" command is supported only for "json-file" logging driver`) {
1203
-		c.Fatalf("There should be error about non-json-file driver, got: %s", out)
1202
+	if !strings.Contains(out, `Error running logs job: Failed to get logging factory: logger: no log driver named 'none' is registered`) {
1203
+		c.Fatalf("There should be an error about none not being a recognized log driver, got: %s", out)
1204 1204
 	}
1205 1205
 }
1206 1206
 
... ...
@@ -171,7 +171,8 @@ millions of trillions.
171 171
 
172 172
 **--log-driver**="|*json-file*|*syslog*|*journald*|*gelf*|*fluentd*|*awslogs*|*none*"
173 173
   Logging driver for container. Default is defined by daemon `--log-driver` flag.
174
-  **Warning**: `docker logs` command works only for `json-file` logging driver.
174
+  **Warning**: the `docker logs` command works only for the `json-file` and
175
+  `journald` logging drivers.
175 176
 
176 177
 **--log-opt**=[]
177 178
   Logging driver specific options.
... ...
@@ -23,7 +23,8 @@ The **docker logs --follow** command combines commands **docker logs** and
23 23
 **docker attach**. It will first return all logs from the beginning and
24 24
 then continue streaming new output from the container’s stdout and stderr.
25 25
 
26
-**Warning**: This command works only for **json-file** logging driver.
26
+**Warning**: This command works only for the **json-file** or **journald**
27
+logging drivers.
27 28
 
28 29
 # OPTIONS
29 30
 **--help**
... ...
@@ -271,7 +271,8 @@ which interface and port to use.
271 271
 
272 272
 **--log-driver**="|*json-file*|*syslog*|*journald*|*gelf*|*fluentd*|*awslogs*|*none*"
273 273
   Logging driver for container. Default is defined by daemon `--log-driver` flag.
274
-  **Warning**: `docker logs` command works only for `json-file` logging driver.
274
+  **Warning**: the `docker logs` command works only for the `json-file` and
275
+  `journald` logging drivers.
275 276
 
276 277
 **--log-opt**=[]
277 278
   Logging driver specific options.
... ...
@@ -121,7 +121,8 @@ unix://[/path/to/socket] to use.
121 121
 
122 122
 **--log-driver**="*json-file*|*syslog*|*journald*|*gelf*|*fluentd*|*awslogs*|*none*"
123 123
   Default driver for container logs. Default is `json-file`.
124
-  **Warning**: `docker logs` command works only for `json-file` logging driver.
124
+  **Warning**: the `docker logs` command works only for the `json-file` and
125
+  `journald` logging drivers.
125 126
 
126 127
 **--log-opt**=[]
127 128
   Logging driver specific options.