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>
| ... | ... |
@@ -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 |
+} |
| 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 |
+} |