Browse code

Support reads for all log drivers.

This supplements any log driver which does not support reads with a
custom read implementation that uses a local file cache.

Signed-off-by: Brian Goff <cpuguy83@gmail.com>
(cherry picked from commit d675e2bf2b75865915c7a4552e00802feeb0847f)
Signed-off-by: Madhu Venugopal <madhu@docker.com>

Brian Goff authored on 2018/04/06 01:42:31
Showing 7 changed files
... ...
@@ -23,6 +23,7 @@ import (
23 23
 	"github.com/docker/docker/daemon/logger"
24 24
 	"github.com/docker/docker/daemon/logger/jsonfilelog"
25 25
 	"github.com/docker/docker/daemon/logger/local"
26
+	"github.com/docker/docker/daemon/logger/loggerutils/cache"
26 27
 	"github.com/docker/docker/daemon/network"
27 28
 	"github.com/docker/docker/errdefs"
28 29
 	"github.com/docker/docker/image"
... ...
@@ -104,8 +105,13 @@ type Container struct {
104 104
 	NoNewPrivileges bool
105 105
 
106 106
 	// Fields here are specific to Windows
107
-	NetworkSharedContainerID string   `json:"-"`
108
-	SharedEndpointList       []string `json:"-"`
107
+	NetworkSharedContainerID string            `json:"-"`
108
+	SharedEndpointList       []string          `json:"-"`
109
+	LocalLogCacheMeta        localLogCacheMeta `json:",omitempty"`
110
+}
111
+
112
+type localLogCacheMeta struct {
113
+	HaveNotifyEnabled bool
109 114
 }
110 115
 
111 116
 // NewBaseContainer creates a new container with its
... ...
@@ -415,6 +421,22 @@ func (container *Container) StartLogger() (logger.Logger, error) {
415 415
 		}
416 416
 		l = logger.NewRingLogger(l, info, bufferSize)
417 417
 	}
418
+
419
+	if _, ok := l.(logger.LogReader); !ok {
420
+		logPath, err := container.GetRootResourcePath("container-cached.log")
421
+		if err != nil {
422
+			return nil, err
423
+		}
424
+		info.LogPath = logPath
425
+
426
+		if !container.LocalLogCacheMeta.HaveNotifyEnabled {
427
+			logrus.WithField("container", container.ID).Info("Configured log driver does not support reads, enabling local file cache for container logs")
428
+		}
429
+		l, err = cache.WithLocalCache(l, info)
430
+		if err != nil {
431
+			return nil, errors.Wrap(err, "error setting up local container log cache")
432
+		}
433
+	}
418 434
 	return l, nil
419 435
 }
420 436
 
421 437
new file mode 100644
... ...
@@ -0,0 +1,63 @@
0
+package cache // import "github.com/docker/docker/daemon/logger/loggerutils/cache"
1
+
2
+import (
3
+	"github.com/docker/docker/daemon/logger"
4
+	"github.com/docker/docker/daemon/logger/local"
5
+	"github.com/sirupsen/logrus"
6
+)
7
+
8
+// WithLocalCache wraps the passed in logger with a logger caches all writes locally
9
+// in addition to writing to the passed in logger.
10
+func WithLocalCache(l logger.Logger, logInfo logger.Info) (logger.Logger, error) {
11
+	localLogger, err := local.New(logInfo)
12
+	if err != nil {
13
+		return nil, err
14
+	}
15
+	return &loggerWithCache{
16
+		l: l,
17
+		// TODO(@cpuguy83): Should this be configurable?
18
+		cache: logger.NewRingLogger(localLogger, logInfo, -1),
19
+	}, nil
20
+}
21
+
22
+type loggerWithCache struct {
23
+	l     logger.Logger
24
+	cache logger.Logger
25
+}
26
+
27
+func (l *loggerWithCache) Log(msg *logger.Message) error {
28
+	// copy the message since the underlying logger will return the passed in message to the message pool
29
+	dup := logger.NewMessage()
30
+	dumbCopyMessage(dup, msg)
31
+	if err := l.l.Log(msg); err != nil {
32
+		return err
33
+	}
34
+	return l.cache.Log(dup)
35
+}
36
+
37
+func (l *loggerWithCache) Name() string {
38
+	return l.l.Name()
39
+}
40
+
41
+func (l *loggerWithCache) ReadLogs(config logger.ReadConfig) *logger.LogWatcher {
42
+	return l.cache.(logger.LogReader).ReadLogs(config)
43
+}
44
+
45
+func (l *loggerWithCache) Close() error {
46
+	err := l.l.Close()
47
+	if err := l.cache.Close(); err != nil {
48
+		logrus.WithError(err).Warn("error while shutting cache logger")
49
+	}
50
+	return err
51
+}
52
+
53
+// dumbCopyMessage is a bit of a fake copy but avoids extra allocations which
54
+// are not necessary for this use case.
55
+func dumbCopyMessage(dst, src *logger.Message) {
56
+	dst.Source = src.Source
57
+	dst.Timestamp = src.Timestamp
58
+	dst.PLogMetaData = src.PLogMetaData
59
+	dst.Err = src.Err
60
+	dst.Attrs = src.Attrs
61
+	dst.Line = src.Line
62
+}
0 63
new file mode 100644
... ...
@@ -0,0 +1,85 @@
0
+package main
1
+
2
+import (
3
+	"encoding/json"
4
+	"io"
5
+	"io/ioutil"
6
+	"net/http"
7
+	"os"
8
+	"sync"
9
+	"syscall"
10
+)
11
+
12
+type startLoggingRequest struct {
13
+	File string
14
+}
15
+
16
+type capabilitiesResponse struct {
17
+	Cap struct {
18
+		ReadLogs bool
19
+	}
20
+}
21
+
22
+type driver struct {
23
+	mu   sync.Mutex
24
+	logs map[string]io.Closer
25
+}
26
+
27
+type stopLoggingRequest struct {
28
+	File string
29
+}
30
+
31
+func handle(mux *http.ServeMux) {
32
+	d := &driver{logs: make(map[string]io.Closer)}
33
+	mux.HandleFunc("/LogDriver.StartLogging", func(w http.ResponseWriter, r *http.Request) {
34
+		var req startLoggingRequest
35
+		if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
36
+			http.Error(w, err.Error(), http.StatusBadRequest)
37
+			return
38
+		}
39
+
40
+		f, err := os.OpenFile(req.File, syscall.O_RDONLY, 0700)
41
+		if err != nil {
42
+			respond(err, w)
43
+		}
44
+
45
+		d.mu.Lock()
46
+		d.logs[req.File] = f
47
+		d.mu.Unlock()
48
+
49
+		go io.Copy(ioutil.Discard, f)
50
+		respond(err, w)
51
+	})
52
+
53
+	mux.HandleFunc("/LogDriver.StopLogging", func(w http.ResponseWriter, r *http.Request) {
54
+		var req stopLoggingRequest
55
+		if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
56
+			http.Error(w, err.Error(), http.StatusBadRequest)
57
+			return
58
+		}
59
+
60
+		d.mu.Lock()
61
+		if f := d.logs[req.File]; f != nil {
62
+			f.Close()
63
+		}
64
+		respond(nil, w)
65
+	})
66
+
67
+	mux.HandleFunc("/LogDriver.Capabilities", func(w http.ResponseWriter, r *http.Request) {
68
+		json.NewEncoder(w).Encode(&capabilitiesResponse{
69
+			Cap: struct{ ReadLogs bool }{ReadLogs: false},
70
+		})
71
+	})
72
+}
73
+
74
+type response struct {
75
+	Err string
76
+}
77
+
78
+func respond(err error, w io.Writer) {
79
+	var res response
80
+	if err != nil {
81
+		res.Err = err.Error()
82
+	}
83
+	json.NewEncoder(w).Encode(&res)
84
+}
0 85
new file mode 100644
... ...
@@ -0,0 +1,22 @@
0
+package main
1
+
2
+import (
3
+	"net"
4
+	"net/http"
5
+)
6
+
7
+func main() {
8
+	l, err := net.Listen("unix", "/run/docker/plugins/plugin.sock")
9
+	if err != nil {
10
+		panic(err)
11
+	}
12
+
13
+	mux := http.NewServeMux()
14
+	handle(mux)
15
+
16
+	server := http.Server{
17
+		Addr:    l.Addr().String(),
18
+		Handler: mux,
19
+	}
20
+	server.Serve(l)
21
+}
0 22
new file mode 100644
... ...
@@ -0,0 +1 @@
0
+package main
... ...
@@ -19,6 +19,9 @@ func TestMain(m *testing.M) {
19 19
 		fmt.Println(err)
20 20
 		os.Exit(1)
21 21
 	}
22
+	if testEnv.OSType != "linux" {
23
+		os.Exit(0)
24
+	}
22 25
 	err = environment.EnsureFrozenImagesLinux(testEnv)
23 26
 	if err != nil {
24 27
 		fmt.Println(err)
25 28
new file mode 100644
... ...
@@ -0,0 +1,73 @@
0
+package logging
1
+
2
+import (
3
+	"bytes"
4
+	"testing"
5
+
6
+	"context"
7
+
8
+	"time"
9
+
10
+	"strings"
11
+
12
+	"github.com/docker/docker/api/types"
13
+	"github.com/docker/docker/api/types/container"
14
+	"github.com/docker/docker/pkg/stdcopy"
15
+	"github.com/docker/docker/testutil/daemon"
16
+	"gotest.tools/v3/assert"
17
+)
18
+
19
+// TestReadPluginNoRead tests that reads are supported even if the plugin isn't capable.
20
+func TestReadPluginNoRead(t *testing.T) {
21
+	t.Parallel()
22
+	d := daemon.New(t)
23
+	d.StartWithBusybox(t, "--iptables=false")
24
+	defer d.Stop(t)
25
+
26
+	client, err := d.NewClient()
27
+	assert.Assert(t, err)
28
+	createPlugin(t, client, "test", "discard", asLogDriver)
29
+
30
+	ctx := context.Background()
31
+	defer func() {
32
+		err = client.PluginRemove(ctx, "test", types.PluginRemoveOptions{Force: true})
33
+		assert.Check(t, err)
34
+	}()
35
+
36
+	err = client.PluginEnable(ctx, "test", types.PluginEnableOptions{Timeout: 30})
37
+	assert.Check(t, err)
38
+
39
+	c, err := client.ContainerCreate(ctx,
40
+		&container.Config{
41
+			Image: "busybox",
42
+			Cmd:   []string{"/bin/echo", "hello world"},
43
+		},
44
+		&container.HostConfig{LogConfig: container.LogConfig{Type: "test"}},
45
+		nil,
46
+		"",
47
+	)
48
+	assert.Assert(t, err)
49
+
50
+	err = client.ContainerStart(ctx, c.ID, types.ContainerStartOptions{})
51
+	assert.Assert(t, err)
52
+
53
+	logs, err := client.ContainerLogs(ctx, c.ID, types.ContainerLogsOptions{ShowStdout: true})
54
+	assert.Assert(t, err)
55
+	defer logs.Close()
56
+
57
+	buf := bytes.NewBuffer(nil)
58
+
59
+	errCh := make(chan error)
60
+	go func() {
61
+		_, err := stdcopy.StdCopy(buf, buf, logs)
62
+		errCh <- err
63
+	}()
64
+
65
+	select {
66
+	case <-time.After(60 * time.Second):
67
+		t.Fatal("timeout waiting for IO to complete")
68
+	case err := <-errCh:
69
+		assert.Assert(t, err)
70
+	}
71
+	assert.Assert(t, strings.TrimSpace(buf.String()) == "hello world", buf.Bytes())
72
+}