Browse code

Merge pull request #32462 from dperny/service-logs-general-availability

Remove experimental from service logs

Victor Vieux authored on 2017/04/11 10:30:20
Showing 7 changed files
... ...
@@ -36,14 +36,14 @@ func (sr *swarmRouter) initRoutes() {
36 36
 		router.NewPostRoute("/services/create", sr.createService),
37 37
 		router.NewPostRoute("/services/{id}/update", sr.updateService),
38 38
 		router.NewDeleteRoute("/services/{id}", sr.removeService),
39
-		router.Experimental(router.Cancellable(router.NewGetRoute("/services/{id}/logs", sr.getServiceLogs))),
39
+		router.Cancellable(router.NewGetRoute("/services/{id}/logs", sr.getServiceLogs)),
40 40
 		router.NewGetRoute("/nodes", sr.getNodes),
41 41
 		router.NewGetRoute("/nodes/{id}", sr.getNode),
42 42
 		router.NewDeleteRoute("/nodes/{id}", sr.removeNode),
43 43
 		router.NewPostRoute("/nodes/{id}/update", sr.updateNode),
44 44
 		router.NewGetRoute("/tasks", sr.getTasks),
45 45
 		router.NewGetRoute("/tasks/{id}", sr.getTask),
46
-		router.Experimental(router.Cancellable(router.NewGetRoute("/tasks/{id}/logs", sr.getTaskLogs))),
46
+		router.Cancellable(router.NewGetRoute("/tasks/{id}/logs", sr.getTaskLogs)),
47 47
 		router.NewGetRoute("/secrets", sr.getSecrets),
48 48
 		router.NewPostRoute("/secrets/create", sr.createSecret),
49 49
 		router.NewDeleteRoute("/secrets/{id}", sr.removeSecret),
... ...
@@ -7729,12 +7729,12 @@ paths:
7729 7729
           schema:
7730 7730
             type: "string"
7731 7731
         404:
7732
-          description: "no such container"
7732
+          description: "no such service"
7733 7733
           schema:
7734 7734
             $ref: "#/definitions/ErrorResponse"
7735 7735
           examples:
7736 7736
             application/json:
7737
-              message: "No such container: c2ada9df5af8"
7737
+              message: "No such service: c2ada9df5af8"
7738 7738
         500:
7739 7739
           description: "server error"
7740 7740
           schema:
... ...
@@ -7747,11 +7747,11 @@ paths:
7747 7747
         - name: "id"
7748 7748
           in: "path"
7749 7749
           required: true
7750
-          description: "ID or name of the container"
7750
+          description: "ID or name of the service"
7751 7751
           type: "string"
7752 7752
         - name: "details"
7753 7753
           in: "query"
7754
-          description: "Show extra details provided to logs."
7754
+          description: "Show service context and extra details provided to logs."
7755 7755
           type: "boolean"
7756 7756
           default: false
7757 7757
         - name: "follow"
... ...
@@ -8008,7 +8008,7 @@ paths:
8008 8008
           type: "string"
8009 8009
         - name: "details"
8010 8010
           in: "query"
8011
-          description: "Show extra details provided to logs."
8011
+          description: "Show task context and extra details provided to logs."
8012 8012
           type: "boolean"
8013 8013
           default: false
8014 8014
         - name: "follow"
... ...
@@ -42,20 +42,22 @@ func newLogsCommand(dockerCli *command.DockerCli) *cobra.Command {
42 42
 	var opts logsOptions
43 43
 
44 44
 	cmd := &cobra.Command{
45
-		Use:   "logs [OPTIONS] SERVICE",
46
-		Short: "Fetch the logs of a service",
45
+		Use:   "logs [OPTIONS] SERVICE|TASK",
46
+		Short: "Fetch the logs of a service or task",
47 47
 		Args:  cli.ExactArgs(1),
48 48
 		RunE: func(cmd *cobra.Command, args []string) error {
49 49
 			opts.target = args[0]
50 50
 			return runLogs(dockerCli, &opts)
51 51
 		},
52
-		Tags: map[string]string{"experimental": ""},
52
+		Tags: map[string]string{"version": "1.29"},
53 53
 	}
54 54
 
55 55
 	flags := cmd.Flags()
56
+	// options specific to service logs
56 57
 	flags.BoolVar(&opts.noResolve, "no-resolve", false, "Do not map IDs to Names in output")
57 58
 	flags.BoolVar(&opts.noTrunc, "no-trunc", false, "Do not truncate output")
58 59
 	flags.BoolVar(&opts.noTaskIDs, "no-task-ids", false, "Do not include task IDs in output")
60
+	// options identical to container logs
59 61
 	flags.BoolVarP(&opts.follow, "follow", "f", false, "Follow log output")
60 62
 	flags.StringVar(&opts.since, "since", "", "Show logs since timestamp (e.g. 2013-01-02T13:23:37) or relative (e.g. 42m for 42 minutes)")
61 63
 	flags.BoolVarP(&opts.timestamps, "timestamps", "t", false, "Show timestamps")
... ...
@@ -94,6 +96,8 @@ func runLogs(dockerCli *command.DockerCli, opts *logsOptions) error {
94 94
 		tty = task.Spec.ContainerSpec.TTY
95 95
 		// TODO(dperny) hot fix until we get a nice details system squared away,
96 96
 		// ignores details (including task context) if we have a TTY log
97
+		// if we don't do this, we'll vomit the huge context verbatim into the
98
+		// TTY log lines and that's Undesirable.
97 99
 		if tty {
98 100
 			options.Details = false
99 101
 		}
... ...
@@ -1,8 +1,7 @@
1 1
 ---
2
-title: "service logs (experimental)"
2
+title: "service logs"
3 3
 description: "The service logs command description and usage"
4
-keywords: "service, logs"
5
-advisory: "experimental"
4
+keywords: "service, task, logs"
6 5
 ---
7 6
 
8 7
 <!-- This file is maintained within the docker/docker Github
... ...
@@ -17,14 +16,16 @@ advisory: "experimental"
17 17
 # service logs
18 18
 
19 19
 ```Markdown
20
-Usage:  docker service logs [OPTIONS] SERVICE
20
+Usage:  docker service logs [OPTIONS] SERVICE|TASK
21 21
 
22
-Fetch the logs of a service
22
+Fetch the logs of a service or task
23 23
 
24 24
 Options:
25
-      --details        Show extra details provided to logs
26 25
   -f, --follow         Follow log output
27 26
       --help           Print usage
27
+      --no-resolve     Do not map IDs to Names in output
28
+      --no-task-ids    Do not include task IDs in output
29
+      --no-trunc        Do not truncate output
28 30
       --since string   Show logs since timestamp
29 31
       --tail string    Number of lines to show from the end of the logs (default "all")
30 32
   -t, --timestamps     Show timestamps
... ...
@@ -34,6 +35,11 @@ Options:
34 34
 
35 35
 The `docker service logs` command batch-retrieves logs present at the time of execution.
36 36
 
37
+The `docker service logs` command can be used with either the name or ID of a
38
+service, or with the ID of a task. If a service is passed, it will display logs
39
+for all of the containers in that service. If a task is passed, it will only
40
+display logs from that particular task.
41
+
37 42
 > **Note**: This command is only functional for services that are started with
38 43
 > the `json-file` or `journald` logging driver.
39 44
 
... ...
@@ -40,7 +40,6 @@ Metrics (Prometheus) output for basic container, image, and daemon operations.
40 40
 
41 41
  * The top-level [docker deploy](../docs/reference/commandline/deploy.md) command. The
42 42
    `docker stack deploy` command is **not** experimental.
43
- * [`docker service logs` command](../docs/reference/commandline/service_logs.md)
44 43
  * [`--squash` option to `docker build` command](../docs/reference/commandline/build.md##squash-an-images-layers---squash-experimental-only)
45 44
  * [External graphdriver plugins](../docs/extend/plugins_graphdriver.md)
46 45
  * [Ipvlan Network Drivers](vlan-networks.md)
47 46
deleted file mode 100644
... ...
@@ -1,296 +0,0 @@
1
-// +build !windows
2
-
3
-package main
4
-
5
-import (
6
-	"bufio"
7
-	"fmt"
8
-	"io"
9
-	"os/exec"
10
-	"strings"
11
-	"time"
12
-
13
-	"github.com/docker/docker/integration-cli/checker"
14
-	"github.com/docker/docker/integration-cli/daemon"
15
-	icmd "github.com/docker/docker/pkg/testutil/cmd"
16
-	"github.com/go-check/check"
17
-)
18
-
19
-type logMessage struct {
20
-	err  error
21
-	data []byte
22
-}
23
-
24
-func (s *DockerSwarmSuite) TestServiceLogs(c *check.C) {
25
-	testRequires(c, ExperimentalDaemon)
26
-
27
-	d := s.AddDaemon(c, true, true)
28
-
29
-	// we have multiple services here for detecting the goroutine issue #28915
30
-	services := map[string]string{
31
-		"TestServiceLogs1": "hello1",
32
-		"TestServiceLogs2": "hello2",
33
-	}
34
-
35
-	for name, message := range services {
36
-		out, err := d.Cmd("service", "create", "--name", name, "busybox",
37
-			"sh", "-c", fmt.Sprintf("echo %s; tail -f /dev/null", message))
38
-		c.Assert(err, checker.IsNil)
39
-		c.Assert(strings.TrimSpace(out), checker.Not(checker.Equals), "")
40
-	}
41
-
42
-	// make sure task has been deployed.
43
-	waitAndAssert(c, defaultReconciliationTimeout,
44
-		d.CheckRunningTaskImages, checker.DeepEquals,
45
-		map[string]int{"busybox": len(services)})
46
-
47
-	for name, message := range services {
48
-		out, err := d.Cmd("service", "logs", name)
49
-		c.Assert(err, checker.IsNil)
50
-		c.Logf("log for %q: %q", name, out)
51
-		c.Assert(out, checker.Contains, message)
52
-	}
53
-}
54
-
55
-// countLogLines returns a closure that can be used with waitAndAssert to
56
-// verify that a minimum number of expected container log messages have been
57
-// output.
58
-func countLogLines(d *daemon.Swarm, name string) func(*check.C) (interface{}, check.CommentInterface) {
59
-	return func(c *check.C) (interface{}, check.CommentInterface) {
60
-		result := icmd.RunCmd(d.Command("service", "logs", "-t", name))
61
-		result.Assert(c, icmd.Expected{})
62
-		lines := strings.Split(strings.TrimSpace(result.Stdout()), "\n")
63
-		return len(lines), check.Commentf("output, %q", string(result.Stdout()))
64
-	}
65
-}
66
-
67
-func (s *DockerSwarmSuite) TestServiceLogsCompleteness(c *check.C) {
68
-	testRequires(c, ExperimentalDaemon)
69
-	d := s.AddDaemon(c, true, true)
70
-
71
-	name := "TestServiceLogsCompleteness"
72
-
73
-	// make a service that prints 6 lines
74
-	out, err := d.Cmd("service", "create", "--name", name, "busybox", "sh", "-c", "for line in $(seq 0 5); do echo log test $line; done; sleep 100000")
75
-	c.Assert(err, checker.IsNil)
76
-	c.Assert(strings.TrimSpace(out), checker.Not(checker.Equals), "")
77
-
78
-	// make sure task has been deployed.
79
-	waitAndAssert(c, defaultReconciliationTimeout, d.CheckActiveContainerCount, checker.Equals, 1)
80
-	// and make sure we have all the log lines
81
-	waitAndAssert(c, defaultReconciliationTimeout, countLogLines(d, name), checker.Equals, 6)
82
-
83
-	out, err = d.Cmd("service", "logs", name)
84
-	c.Assert(err, checker.IsNil)
85
-	lines := strings.Split(strings.TrimSpace(out), "\n")
86
-
87
-	// i have heard anecdotal reports that logs may come back from the engine
88
-	// mis-ordered. if this tests fails, consider the possibility that that
89
-	// might be occurring
90
-	for i, line := range lines {
91
-		c.Assert(line, checker.Contains, fmt.Sprintf("log test %v", i))
92
-	}
93
-}
94
-
95
-func (s *DockerSwarmSuite) TestServiceLogsTail(c *check.C) {
96
-	testRequires(c, ExperimentalDaemon)
97
-	d := s.AddDaemon(c, true, true)
98
-
99
-	name := "TestServiceLogsTail"
100
-
101
-	// make a service that prints 6 lines
102
-	out, err := d.Cmd("service", "create", "--name", name, "busybox", "sh", "-c", "for line in $(seq 1 6); do echo log test $line; done; sleep 100000")
103
-	c.Assert(err, checker.IsNil)
104
-	c.Assert(strings.TrimSpace(out), checker.Not(checker.Equals), "")
105
-
106
-	// make sure task has been deployed.
107
-	waitAndAssert(c, defaultReconciliationTimeout, d.CheckActiveContainerCount, checker.Equals, 1)
108
-	waitAndAssert(c, defaultReconciliationTimeout, countLogLines(d, name), checker.Equals, 6)
109
-
110
-	out, err = d.Cmd("service", "logs", "--tail=2", name)
111
-	c.Assert(err, checker.IsNil)
112
-	lines := strings.Split(strings.TrimSpace(out), "\n")
113
-
114
-	for i, line := range lines {
115
-		// doing i+5 is hacky but not too fragile, it's good enough. if it flakes something else is wrong
116
-		c.Assert(line, checker.Contains, fmt.Sprintf("log test %v", i+5))
117
-	}
118
-}
119
-
120
-func (s *DockerSwarmSuite) TestServiceLogsSince(c *check.C) {
121
-	// See DockerSuite.TestLogsSince, which is where this comes from
122
-	testRequires(c, ExperimentalDaemon)
123
-	d := s.AddDaemon(c, true, true)
124
-
125
-	name := "TestServiceLogsSince"
126
-
127
-	out, err := d.Cmd("service", "create", "--name", name, "busybox", "sh", "-c", "for i in $(seq 1 3); do sleep .1; echo log$i; done; sleep 10000000")
128
-	c.Assert(err, checker.IsNil)
129
-	c.Assert(strings.TrimSpace(out), checker.Not(checker.Equals), "")
130
-	waitAndAssert(c, defaultReconciliationTimeout, d.CheckActiveContainerCount, checker.Equals, 1)
131
-	// wait a sec for the logs to come in
132
-	waitAndAssert(c, defaultReconciliationTimeout, countLogLines(d, name), checker.Equals, 3)
133
-
134
-	out, err = d.Cmd("service", "logs", "-t", name)
135
-	c.Assert(err, checker.IsNil)
136
-
137
-	log2Line := strings.Split(strings.Split(out, "\n")[1], " ")
138
-	t, err := time.Parse(time.RFC3339Nano, log2Line[0]) // timestamp log2 is written
139
-	c.Assert(err, checker.IsNil)
140
-	u := t.Add(50 * time.Millisecond) // add .05s so log1 & log2 don't show up
141
-	since := u.Format(time.RFC3339Nano)
142
-
143
-	out, err = d.Cmd("service", "logs", "-t", fmt.Sprintf("--since=%v", since), name)
144
-	c.Assert(err, checker.IsNil)
145
-
146
-	unexpected := []string{"log1", "log2"}
147
-	expected := []string{"log3"}
148
-	for _, v := range unexpected {
149
-		c.Assert(out, checker.Not(checker.Contains), v, check.Commentf("unexpected log message returned, since=%v", u))
150
-	}
151
-	for _, v := range expected {
152
-		c.Assert(out, checker.Contains, v, check.Commentf("expected log message %v, was not present, since=%v", u))
153
-	}
154
-}
155
-
156
-func (s *DockerSwarmSuite) TestServiceLogsFollow(c *check.C) {
157
-	testRequires(c, ExperimentalDaemon)
158
-
159
-	d := s.AddDaemon(c, true, true)
160
-
161
-	name := "TestServiceLogsFollow"
162
-
163
-	out, err := d.Cmd("service", "create", "--name", name, "busybox", "sh", "-c", "while true; do echo log test; sleep 0.1; done")
164
-	c.Assert(err, checker.IsNil)
165
-	c.Assert(strings.TrimSpace(out), checker.Not(checker.Equals), "")
166
-
167
-	// make sure task has been deployed.
168
-	waitAndAssert(c, defaultReconciliationTimeout, d.CheckActiveContainerCount, checker.Equals, 1)
169
-
170
-	args := []string{"service", "logs", "-f", name}
171
-	cmd := exec.Command(dockerBinary, d.PrependHostArg(args)...)
172
-	r, w := io.Pipe()
173
-	cmd.Stdout = w
174
-	cmd.Stderr = w
175
-	c.Assert(cmd.Start(), checker.IsNil)
176
-
177
-	// Make sure pipe is written to
178
-	ch := make(chan *logMessage)
179
-	done := make(chan struct{})
180
-	go func() {
181
-		reader := bufio.NewReader(r)
182
-		for {
183
-			msg := &logMessage{}
184
-			msg.data, _, msg.err = reader.ReadLine()
185
-			select {
186
-			case ch <- msg:
187
-			case <-done:
188
-				return
189
-			}
190
-		}
191
-	}()
192
-
193
-	for i := 0; i < 3; i++ {
194
-		msg := <-ch
195
-		c.Assert(msg.err, checker.IsNil)
196
-		c.Assert(string(msg.data), checker.Contains, "log test")
197
-	}
198
-	close(done)
199
-
200
-	c.Assert(cmd.Process.Kill(), checker.IsNil)
201
-}
202
-
203
-func (s *DockerSwarmSuite) TestServiceLogsTaskLogs(c *check.C) {
204
-	testRequires(c, ExperimentalDaemon)
205
-
206
-	d := s.AddDaemon(c, true, true)
207
-
208
-	name := "TestServicelogsTaskLogs"
209
-	replicas := 2
210
-
211
-	result := icmd.RunCmd(d.Command(
212
-		// create a service with the name
213
-		"service", "create", "--name", name,
214
-		// which has some number of replicas
215
-		fmt.Sprintf("--replicas=%v", replicas),
216
-		// which has this the task id as an environment variable templated in
217
-		"--env", "TASK={{.Task.ID}}",
218
-		// and runs this command to print exaclty 6 logs lines
219
-		"busybox", "sh", "-c", "for line in $(seq 0 5); do echo $TASK log test $line; done; sleep 100000",
220
-	))
221
-	result.Assert(c, icmd.Expected{})
222
-	// ^^ verify that we get no error
223
-	// then verify that we have an id in stdout
224
-	id := strings.TrimSpace(result.Stdout())
225
-	c.Assert(id, checker.Not(checker.Equals), "")
226
-	// so, right here, we're basically inspecting by id and returning only
227
-	// the ID. if they don't match, the service doesn't exist.
228
-	result = icmd.RunCmd(d.Command("service", "inspect", "--format=\"{{.ID}}\"", id))
229
-	result.Assert(c, icmd.Expected{Out: id})
230
-
231
-	// make sure task has been deployed.
232
-	waitAndAssert(c, defaultReconciliationTimeout, d.CheckActiveContainerCount, checker.Equals, replicas)
233
-	waitAndAssert(c, defaultReconciliationTimeout, countLogLines(d, name), checker.Equals, 6*replicas)
234
-
235
-	// get the task ids
236
-	result = icmd.RunCmd(d.Command("service", "ps", "-q", name))
237
-	result.Assert(c, icmd.Expected{})
238
-	// make sure we have two tasks
239
-	taskIDs := strings.Split(strings.TrimSpace(result.Stdout()), "\n")
240
-	c.Assert(taskIDs, checker.HasLen, replicas)
241
-
242
-	for _, taskID := range taskIDs {
243
-		c.Logf("checking task %v", taskID)
244
-		result := icmd.RunCmd(d.Command("service", "logs", taskID))
245
-		result.Assert(c, icmd.Expected{})
246
-		lines := strings.Split(strings.TrimSpace(result.Stdout()), "\n")
247
-
248
-		c.Logf("checking messages for %v", taskID)
249
-		for i, line := range lines {
250
-			// make sure the message is in order
251
-			c.Assert(line, checker.Contains, fmt.Sprintf("log test %v", i))
252
-			// make sure it contains the task id
253
-			c.Assert(line, checker.Contains, taskID)
254
-		}
255
-	}
256
-}
257
-
258
-func (s *DockerSwarmSuite) TestServiceLogsTTY(c *check.C) {
259
-	testRequires(c, ExperimentalDaemon)
260
-
261
-	d := s.AddDaemon(c, true, true)
262
-
263
-	name := "TestServiceLogsTTY"
264
-
265
-	result := icmd.RunCmd(d.Command(
266
-		// create a service
267
-		"service", "create",
268
-		// name it $name
269
-		"--name", name,
270
-		// use a TTY
271
-		"-t",
272
-		// busybox image, shell string
273
-		"busybox", "sh", "-c",
274
-		// echo to stdout and stderr
275
-		"echo out; (echo err 1>&2); sleep 10000",
276
-	))
277
-
278
-	result.Assert(c, icmd.Expected{})
279
-	id := strings.TrimSpace(result.Stdout())
280
-	c.Assert(id, checker.Not(checker.Equals), "")
281
-	// so, right here, we're basically inspecting by id and returning only
282
-	// the ID. if they don't match, the service doesn't exist.
283
-	result = icmd.RunCmd(d.Command("service", "inspect", "--format=\"{{.ID}}\"", id))
284
-	result.Assert(c, icmd.Expected{Out: id})
285
-
286
-	// make sure task has been deployed.
287
-	waitAndAssert(c, defaultReconciliationTimeout, d.CheckActiveContainerCount, checker.Equals, 1)
288
-	// and make sure we have all the log lines
289
-	waitAndAssert(c, defaultReconciliationTimeout, countLogLines(d, name), checker.Equals, 2)
290
-
291
-	cmd := d.Command("service", "logs", name)
292
-	result = icmd.RunCmd(cmd)
293
-	// for some reason there is carriage return in the output. i think this is
294
-	// just expected.
295
-	c.Assert(result, icmd.Matches, icmd.Expected{Out: "out\r\nerr\r\n"})
296
-}
297 1
new file mode 100644
... ...
@@ -0,0 +1,285 @@
0
+// +build !windows
1
+
2
+package main
3
+
4
+import (
5
+	"bufio"
6
+	"fmt"
7
+	"io"
8
+	"os/exec"
9
+	"strings"
10
+	"time"
11
+
12
+	"github.com/docker/docker/integration-cli/checker"
13
+	"github.com/docker/docker/integration-cli/daemon"
14
+	icmd "github.com/docker/docker/pkg/testutil/cmd"
15
+	"github.com/go-check/check"
16
+)
17
+
18
+type logMessage struct {
19
+	err  error
20
+	data []byte
21
+}
22
+
23
+func (s *DockerSwarmSuite) TestServiceLogs(c *check.C) {
24
+	d := s.AddDaemon(c, true, true)
25
+
26
+	// we have multiple services here for detecting the goroutine issue #28915
27
+	services := map[string]string{
28
+		"TestServiceLogs1": "hello1",
29
+		"TestServiceLogs2": "hello2",
30
+	}
31
+
32
+	for name, message := range services {
33
+		out, err := d.Cmd("service", "create", "--name", name, "busybox",
34
+			"sh", "-c", fmt.Sprintf("echo %s; tail -f /dev/null", message))
35
+		c.Assert(err, checker.IsNil)
36
+		c.Assert(strings.TrimSpace(out), checker.Not(checker.Equals), "")
37
+	}
38
+
39
+	// make sure task has been deployed.
40
+	waitAndAssert(c, defaultReconciliationTimeout,
41
+		d.CheckRunningTaskImages, checker.DeepEquals,
42
+		map[string]int{"busybox": len(services)})
43
+
44
+	for name, message := range services {
45
+		out, err := d.Cmd("service", "logs", name)
46
+		c.Assert(err, checker.IsNil)
47
+		c.Logf("log for %q: %q", name, out)
48
+		c.Assert(out, checker.Contains, message)
49
+	}
50
+}
51
+
52
+// countLogLines returns a closure that can be used with waitAndAssert to
53
+// verify that a minimum number of expected container log messages have been
54
+// output.
55
+func countLogLines(d *daemon.Swarm, name string) func(*check.C) (interface{}, check.CommentInterface) {
56
+	return func(c *check.C) (interface{}, check.CommentInterface) {
57
+		result := icmd.RunCmd(d.Command("service", "logs", "-t", name))
58
+		result.Assert(c, icmd.Expected{})
59
+		lines := strings.Split(strings.TrimSpace(result.Stdout()), "\n")
60
+		return len(lines), check.Commentf("output, %q", string(result.Stdout()))
61
+	}
62
+}
63
+
64
+func (s *DockerSwarmSuite) TestServiceLogsCompleteness(c *check.C) {
65
+	d := s.AddDaemon(c, true, true)
66
+
67
+	name := "TestServiceLogsCompleteness"
68
+
69
+	// make a service that prints 6 lines
70
+	out, err := d.Cmd("service", "create", "--name", name, "busybox", "sh", "-c", "for line in $(seq 0 5); do echo log test $line; done; sleep 100000")
71
+	c.Assert(err, checker.IsNil)
72
+	c.Assert(strings.TrimSpace(out), checker.Not(checker.Equals), "")
73
+
74
+	// make sure task has been deployed.
75
+	waitAndAssert(c, defaultReconciliationTimeout, d.CheckActiveContainerCount, checker.Equals, 1)
76
+	// and make sure we have all the log lines
77
+	waitAndAssert(c, defaultReconciliationTimeout, countLogLines(d, name), checker.Equals, 6)
78
+
79
+	out, err = d.Cmd("service", "logs", name)
80
+	c.Assert(err, checker.IsNil)
81
+	lines := strings.Split(strings.TrimSpace(out), "\n")
82
+
83
+	// i have heard anecdotal reports that logs may come back from the engine
84
+	// mis-ordered. if this tests fails, consider the possibility that that
85
+	// might be occurring
86
+	for i, line := range lines {
87
+		c.Assert(line, checker.Contains, fmt.Sprintf("log test %v", i))
88
+	}
89
+}
90
+
91
+func (s *DockerSwarmSuite) TestServiceLogsTail(c *check.C) {
92
+	d := s.AddDaemon(c, true, true)
93
+
94
+	name := "TestServiceLogsTail"
95
+
96
+	// make a service that prints 6 lines
97
+	out, err := d.Cmd("service", "create", "--name", name, "busybox", "sh", "-c", "for line in $(seq 1 6); do echo log test $line; done; sleep 100000")
98
+	c.Assert(err, checker.IsNil)
99
+	c.Assert(strings.TrimSpace(out), checker.Not(checker.Equals), "")
100
+
101
+	// make sure task has been deployed.
102
+	waitAndAssert(c, defaultReconciliationTimeout, d.CheckActiveContainerCount, checker.Equals, 1)
103
+	waitAndAssert(c, defaultReconciliationTimeout, countLogLines(d, name), checker.Equals, 6)
104
+
105
+	out, err = d.Cmd("service", "logs", "--tail=2", name)
106
+	c.Assert(err, checker.IsNil)
107
+	lines := strings.Split(strings.TrimSpace(out), "\n")
108
+
109
+	for i, line := range lines {
110
+		// doing i+5 is hacky but not too fragile, it's good enough. if it flakes something else is wrong
111
+		c.Assert(line, checker.Contains, fmt.Sprintf("log test %v", i+5))
112
+	}
113
+}
114
+
115
+func (s *DockerSwarmSuite) TestServiceLogsSince(c *check.C) {
116
+	// See DockerSuite.TestLogsSince, which is where this comes from
117
+	d := s.AddDaemon(c, true, true)
118
+
119
+	name := "TestServiceLogsSince"
120
+
121
+	out, err := d.Cmd("service", "create", "--name", name, "busybox", "sh", "-c", "for i in $(seq 1 3); do sleep .1; echo log$i; done; sleep 10000000")
122
+	c.Assert(err, checker.IsNil)
123
+	c.Assert(strings.TrimSpace(out), checker.Not(checker.Equals), "")
124
+	waitAndAssert(c, defaultReconciliationTimeout, d.CheckActiveContainerCount, checker.Equals, 1)
125
+	// wait a sec for the logs to come in
126
+	waitAndAssert(c, defaultReconciliationTimeout, countLogLines(d, name), checker.Equals, 3)
127
+
128
+	out, err = d.Cmd("service", "logs", "-t", name)
129
+	c.Assert(err, checker.IsNil)
130
+
131
+	log2Line := strings.Split(strings.Split(out, "\n")[1], " ")
132
+	t, err := time.Parse(time.RFC3339Nano, log2Line[0]) // timestamp log2 is written
133
+	c.Assert(err, checker.IsNil)
134
+	u := t.Add(50 * time.Millisecond) // add .05s so log1 & log2 don't show up
135
+	since := u.Format(time.RFC3339Nano)
136
+
137
+	out, err = d.Cmd("service", "logs", "-t", fmt.Sprintf("--since=%v", since), name)
138
+	c.Assert(err, checker.IsNil)
139
+
140
+	unexpected := []string{"log1", "log2"}
141
+	expected := []string{"log3"}
142
+	for _, v := range unexpected {
143
+		c.Assert(out, checker.Not(checker.Contains), v, check.Commentf("unexpected log message returned, since=%v", u))
144
+	}
145
+	for _, v := range expected {
146
+		c.Assert(out, checker.Contains, v, check.Commentf("expected log message %v, was not present, since=%v", u))
147
+	}
148
+}
149
+
150
+func (s *DockerSwarmSuite) TestServiceLogsFollow(c *check.C) {
151
+	d := s.AddDaemon(c, true, true)
152
+
153
+	name := "TestServiceLogsFollow"
154
+
155
+	out, err := d.Cmd("service", "create", "--name", name, "busybox", "sh", "-c", "while true; do echo log test; sleep 0.1; done")
156
+	c.Assert(err, checker.IsNil)
157
+	c.Assert(strings.TrimSpace(out), checker.Not(checker.Equals), "")
158
+
159
+	// make sure task has been deployed.
160
+	waitAndAssert(c, defaultReconciliationTimeout, d.CheckActiveContainerCount, checker.Equals, 1)
161
+
162
+	args := []string{"service", "logs", "-f", name}
163
+	cmd := exec.Command(dockerBinary, d.PrependHostArg(args)...)
164
+	r, w := io.Pipe()
165
+	cmd.Stdout = w
166
+	cmd.Stderr = w
167
+	c.Assert(cmd.Start(), checker.IsNil)
168
+
169
+	// Make sure pipe is written to
170
+	ch := make(chan *logMessage)
171
+	done := make(chan struct{})
172
+	go func() {
173
+		reader := bufio.NewReader(r)
174
+		for {
175
+			msg := &logMessage{}
176
+			msg.data, _, msg.err = reader.ReadLine()
177
+			select {
178
+			case ch <- msg:
179
+			case <-done:
180
+				return
181
+			}
182
+		}
183
+	}()
184
+
185
+	for i := 0; i < 3; i++ {
186
+		msg := <-ch
187
+		c.Assert(msg.err, checker.IsNil)
188
+		c.Assert(string(msg.data), checker.Contains, "log test")
189
+	}
190
+	close(done)
191
+
192
+	c.Assert(cmd.Process.Kill(), checker.IsNil)
193
+}
194
+
195
+func (s *DockerSwarmSuite) TestServiceLogsTaskLogs(c *check.C) {
196
+	d := s.AddDaemon(c, true, true)
197
+
198
+	name := "TestServicelogsTaskLogs"
199
+	replicas := 2
200
+
201
+	result := icmd.RunCmd(d.Command(
202
+		// create a service with the name
203
+		"service", "create", "--name", name,
204
+		// which has some number of replicas
205
+		fmt.Sprintf("--replicas=%v", replicas),
206
+		// which has this the task id as an environment variable templated in
207
+		"--env", "TASK={{.Task.ID}}",
208
+		// and runs this command to print exaclty 6 logs lines
209
+		"busybox", "sh", "-c", "for line in $(seq 0 5); do echo $TASK log test $line; done; sleep 100000",
210
+	))
211
+	result.Assert(c, icmd.Expected{})
212
+	// ^^ verify that we get no error
213
+	// then verify that we have an id in stdout
214
+	id := strings.TrimSpace(result.Stdout())
215
+	c.Assert(id, checker.Not(checker.Equals), "")
216
+	// so, right here, we're basically inspecting by id and returning only
217
+	// the ID. if they don't match, the service doesn't exist.
218
+	result = icmd.RunCmd(d.Command("service", "inspect", "--format=\"{{.ID}}\"", id))
219
+	result.Assert(c, icmd.Expected{Out: id})
220
+
221
+	// make sure task has been deployed.
222
+	waitAndAssert(c, defaultReconciliationTimeout, d.CheckActiveContainerCount, checker.Equals, replicas)
223
+	waitAndAssert(c, defaultReconciliationTimeout, countLogLines(d, name), checker.Equals, 6*replicas)
224
+
225
+	// get the task ids
226
+	result = icmd.RunCmd(d.Command("service", "ps", "-q", name))
227
+	result.Assert(c, icmd.Expected{})
228
+	// make sure we have two tasks
229
+	taskIDs := strings.Split(strings.TrimSpace(result.Stdout()), "\n")
230
+	c.Assert(taskIDs, checker.HasLen, replicas)
231
+
232
+	for _, taskID := range taskIDs {
233
+		c.Logf("checking task %v", taskID)
234
+		result := icmd.RunCmd(d.Command("service", "logs", taskID))
235
+		result.Assert(c, icmd.Expected{})
236
+		lines := strings.Split(strings.TrimSpace(result.Stdout()), "\n")
237
+
238
+		c.Logf("checking messages for %v", taskID)
239
+		for i, line := range lines {
240
+			// make sure the message is in order
241
+			c.Assert(line, checker.Contains, fmt.Sprintf("log test %v", i))
242
+			// make sure it contains the task id
243
+			c.Assert(line, checker.Contains, taskID)
244
+		}
245
+	}
246
+}
247
+
248
+func (s *DockerSwarmSuite) TestServiceLogsTTY(c *check.C) {
249
+	d := s.AddDaemon(c, true, true)
250
+
251
+	name := "TestServiceLogsTTY"
252
+
253
+	result := icmd.RunCmd(d.Command(
254
+		// create a service
255
+		"service", "create",
256
+		// name it $name
257
+		"--name", name,
258
+		// use a TTY
259
+		"-t",
260
+		// busybox image, shell string
261
+		"busybox", "sh", "-c",
262
+		// echo to stdout and stderr
263
+		"echo out; (echo err 1>&2); sleep 10000",
264
+	))
265
+
266
+	result.Assert(c, icmd.Expected{})
267
+	id := strings.TrimSpace(result.Stdout())
268
+	c.Assert(id, checker.Not(checker.Equals), "")
269
+	// so, right here, we're basically inspecting by id and returning only
270
+	// the ID. if they don't match, the service doesn't exist.
271
+	result = icmd.RunCmd(d.Command("service", "inspect", "--format=\"{{.ID}}\"", id))
272
+	result.Assert(c, icmd.Expected{Out: id})
273
+
274
+	// make sure task has been deployed.
275
+	waitAndAssert(c, defaultReconciliationTimeout, d.CheckActiveContainerCount, checker.Equals, 1)
276
+	// and make sure we have all the log lines
277
+	waitAndAssert(c, defaultReconciliationTimeout, countLogLines(d, name), checker.Equals, 2)
278
+
279
+	cmd := d.Command("service", "logs", name)
280
+	result = icmd.RunCmd(cmd)
281
+	// for some reason there is carriage return in the output. i think this is
282
+	// just expected.
283
+	c.Assert(result, icmd.Matches, icmd.Expected{Out: "out\r\nerr\r\n"})
284
+}