Browse code

cli: docker service logs support

Signed-off-by: Andrea Luzzardi <aluzzardi@gmail.com>

Andrea Luzzardi authored on 2016/10/26 17:19:32
Showing 3 changed files
... ...
@@ -26,6 +26,7 @@ func NewServiceCommand(dockerCli *command.DockerCli) *cobra.Command {
26 26
 		newRemoveCommand(dockerCli),
27 27
 		newScaleCommand(dockerCli),
28 28
 		newUpdateCommand(dockerCli),
29
+		newLogsCommand(dockerCli),
29 30
 	)
30 31
 	return cmd
31 32
 }
32 33
new file mode 100644
... ...
@@ -0,0 +1,163 @@
0
+package service
1
+
2
+import (
3
+	"bytes"
4
+	"fmt"
5
+	"io"
6
+	"strings"
7
+
8
+	"golang.org/x/net/context"
9
+
10
+	"github.com/docker/docker/api/types"
11
+	"github.com/docker/docker/api/types/swarm"
12
+	"github.com/docker/docker/cli"
13
+	"github.com/docker/docker/cli/command"
14
+	"github.com/docker/docker/cli/command/idresolver"
15
+	"github.com/docker/docker/pkg/stdcopy"
16
+	"github.com/spf13/cobra"
17
+)
18
+
19
+type logsOptions struct {
20
+	noResolve  bool
21
+	follow     bool
22
+	since      string
23
+	timestamps bool
24
+	details    bool
25
+	tail       string
26
+
27
+	service string
28
+}
29
+
30
+func newLogsCommand(dockerCli *command.DockerCli) *cobra.Command {
31
+	var opts logsOptions
32
+
33
+	cmd := &cobra.Command{
34
+		Use:   "logs [OPTIONS] SERVICE",
35
+		Short: "Fetch the logs of a service",
36
+		Args:  cli.ExactArgs(1),
37
+		RunE: func(cmd *cobra.Command, args []string) error {
38
+			opts.service = args[0]
39
+			return runLogs(dockerCli, &opts)
40
+		},
41
+		Tags: map[string]string{"experimental": ""},
42
+	}
43
+
44
+	flags := cmd.Flags()
45
+	flags.BoolVar(&opts.noResolve, "no-resolve", false, "Do not map IDs to Names")
46
+	flags.BoolVarP(&opts.follow, "follow", "f", false, "Follow log output")
47
+	flags.StringVar(&opts.since, "since", "", "Show logs since timestamp")
48
+	flags.BoolVarP(&opts.timestamps, "timestamps", "t", false, "Show timestamps")
49
+	flags.BoolVar(&opts.details, "details", false, "Show extra details provided to logs")
50
+	flags.StringVar(&opts.tail, "tail", "all", "Number of lines to show from the end of the logs")
51
+	return cmd
52
+}
53
+
54
+func runLogs(dockerCli *command.DockerCli, opts *logsOptions) error {
55
+	ctx := context.Background()
56
+
57
+	options := types.ContainerLogsOptions{
58
+		ShowStdout: true,
59
+		ShowStderr: true,
60
+		Since:      opts.since,
61
+		Timestamps: opts.timestamps,
62
+		Follow:     opts.follow,
63
+		Tail:       opts.tail,
64
+		Details:    opts.details,
65
+	}
66
+
67
+	client := dockerCli.Client()
68
+	responseBody, err := client.ServiceLogs(ctx, opts.service, options)
69
+	if err != nil {
70
+		return err
71
+	}
72
+	defer responseBody.Close()
73
+
74
+	resolver := idresolver.New(client, opts.noResolve)
75
+
76
+	stdout := &logWriter{ctx: ctx, opts: opts, r: resolver, w: dockerCli.Out()}
77
+	stderr := &logWriter{ctx: ctx, opts: opts, r: resolver, w: dockerCli.Err()}
78
+
79
+	// TODO(aluzzardi): Do an io.Copy for services with TTY enabled.
80
+	_, err = stdcopy.StdCopy(stdout, stderr, responseBody)
81
+	return err
82
+}
83
+
84
+type logWriter struct {
85
+	ctx  context.Context
86
+	opts *logsOptions
87
+	r    *idresolver.IDResolver
88
+	w    io.Writer
89
+}
90
+
91
+func (lw *logWriter) Write(buf []byte) (int, error) {
92
+	contextIndex := 0
93
+	numParts := 2
94
+	if lw.opts.timestamps {
95
+		contextIndex++
96
+		numParts++
97
+	}
98
+
99
+	parts := bytes.SplitN(buf, []byte(" "), numParts)
100
+	if len(parts) != numParts {
101
+		return 0, fmt.Errorf("invalid context in log message: %v", string(buf))
102
+	}
103
+
104
+	taskName, nodeName, err := lw.parseContext(string(parts[contextIndex]))
105
+	if err != nil {
106
+		return 0, err
107
+	}
108
+
109
+	output := []byte{}
110
+	for i, part := range parts {
111
+		// First part doesn't get space separation.
112
+		if i > 0 {
113
+			output = append(output, []byte(" ")...)
114
+		}
115
+
116
+		if i == contextIndex {
117
+			// TODO(aluzzardi): Consider constant padding.
118
+			output = append(output, []byte(fmt.Sprintf("%s@%s    |", taskName, nodeName))...)
119
+		} else {
120
+			output = append(output, part...)
121
+		}
122
+	}
123
+	_, err = lw.w.Write(output)
124
+	if err != nil {
125
+		return 0, err
126
+	}
127
+
128
+	return len(buf), nil
129
+}
130
+
131
+func (lw *logWriter) parseContext(input string) (string, string, error) {
132
+	context := make(map[string]string)
133
+
134
+	components := strings.Split(input, ",")
135
+	for _, component := range components {
136
+		parts := strings.SplitN(component, "=", 2)
137
+		if len(parts) != 2 {
138
+			return "", "", fmt.Errorf("invalid context: %s", input)
139
+		}
140
+		context[parts[0]] = parts[1]
141
+	}
142
+
143
+	taskID, ok := context["com.docker.swarm.task.id"]
144
+	if !ok {
145
+		return "", "", fmt.Errorf("missing task id in context: %s", input)
146
+	}
147
+	taskName, err := lw.r.Resolve(lw.ctx, swarm.Task{}, taskID)
148
+	if err != nil {
149
+		return "", "", err
150
+	}
151
+
152
+	nodeID, ok := context["com.docker.swarm.node.id"]
153
+	if !ok {
154
+		return "", "", fmt.Errorf("missing node id in context: %s", input)
155
+	}
156
+	nodeName, err := lw.r.Resolve(lw.ctx, swarm.Node{}, nodeID)
157
+	if err != nil {
158
+		return "", "", err
159
+	}
160
+
161
+	return taskName, nodeName, nil
162
+}
0 163
new file mode 100644
... ...
@@ -0,0 +1,55 @@
0
+// +build !windows
1
+
2
+package main
3
+
4
+import (
5
+	"bufio"
6
+	"io"
7
+	"os/exec"
8
+	"strings"
9
+
10
+	"github.com/docker/docker/pkg/integration/checker"
11
+	"github.com/go-check/check"
12
+)
13
+
14
+type logMessage struct {
15
+	err  error
16
+	data []byte
17
+}
18
+
19
+func (s *DockerSwarmSuite) TestServiceLogs(c *check.C) {
20
+	testRequires(c, ExperimentalDaemon)
21
+
22
+	d := s.AddDaemon(c, true, true)
23
+
24
+	name := "TestServiceLogs"
25
+
26
+	out, err := d.Cmd("service", "create", "--name", name, "busybox", "sh", "-c", "while true; do echo log test; sleep 1; done")
27
+	c.Assert(err, checker.IsNil)
28
+	c.Assert(strings.TrimSpace(out), checker.Not(checker.Equals), "")
29
+
30
+	// make sure task has been deployed.
31
+	waitAndAssert(c, defaultReconciliationTimeout, d.checkActiveContainerCount, checker.Equals, 1)
32
+
33
+	args := []string{"service", "logs", "-f", name}
34
+	cmd := exec.Command(dockerBinary, d.prependHostArg(args)...)
35
+	r, w := io.Pipe()
36
+	cmd.Stdout = w
37
+	cmd.Stderr = w
38
+	c.Assert(cmd.Start(), checker.IsNil)
39
+
40
+	// Make sure pipe is written to
41
+	ch := make(chan *logMessage)
42
+	go func() {
43
+		reader := bufio.NewReader(r)
44
+		msg := &logMessage{}
45
+		msg.data, _, msg.err = reader.ReadLine()
46
+		ch <- msg
47
+	}()
48
+
49
+	msg := <-ch
50
+	c.Assert(msg.err, checker.IsNil)
51
+	c.Assert(string(msg.data), checker.Contains, "log test")
52
+
53
+	c.Assert(cmd.Process.Kill(), checker.IsNil)
54
+}