Skip to content

Commit 170be9c

Browse files
authored
Merge pull request moby#32015 from dperny/service-logs-support-task-logs
Add Support for Service Task Logs
2 parents 53c7995 + d330dc3 commit 170be9c

11 files changed

Lines changed: 402 additions & 113 deletions

File tree

api/server/router/swarm/backend.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ type Backend interface {
2121
CreateService(types.ServiceSpec, string) (*basictypes.ServiceCreateResponse, error)
2222
UpdateService(string, uint64, types.ServiceSpec, basictypes.ServiceUpdateOptions) (*basictypes.ServiceUpdateResponse, error)
2323
RemoveService(string) error
24-
ServiceLogs(context.Context, string, *backend.ContainerLogsConfig, chan struct{}) error
24+
ServiceLogs(context.Context, *backend.LogSelector, *backend.ContainerLogsConfig, chan struct{}) error
2525
GetNodes(basictypes.NodeListOptions) ([]types.Node, error)
2626
GetNode(string) (types.Node, error)
2727
UpdateNode(string, uint64, types.NodeSpec) error

api/server/router/swarm/cluster.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ func (sr *swarmRouter) initRoutes() {
4343
router.NewPostRoute("/nodes/{id}/update", sr.updateNode),
4444
router.NewGetRoute("/tasks", sr.getTasks),
4545
router.NewGetRoute("/tasks/{id}", sr.getTask),
46+
router.Experimental(router.Cancellable(router.NewGetRoute("/tasks/{id}/logs", sr.getTaskLogs))),
4647
router.NewGetRoute("/secrets", sr.getSecrets),
4748
router.NewPostRoute("/secrets/create", sr.createSecret),
4849
router.NewDeleteRoute("/secrets/{id}", sr.removeSecret),

api/server/router/swarm/cluster_routes.go

Lines changed: 16 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import (
1313
"github.com/docker/docker/api/types/backend"
1414
"github.com/docker/docker/api/types/filters"
1515
types "github.com/docker/docker/api/types/swarm"
16-
"github.com/docker/docker/pkg/stdcopy"
1716
"golang.org/x/net/context"
1817
)
1918

@@ -215,54 +214,28 @@ func (sr *swarmRouter) removeService(ctx context.Context, w http.ResponseWriter,
215214
return nil
216215
}
217216

218-
func (sr *swarmRouter) getServiceLogs(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
217+
func (sr *swarmRouter) getTaskLogs(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
219218
if err := httputils.ParseForm(r); err != nil {
220219
return err
221220
}
222221

223-
// Args are validated before the stream starts because when it starts we're
224-
// sending HTTP 200 by writing an empty chunk of data to tell the client that
225-
// daemon is going to stream. By sending this initial HTTP 200 we can't report
226-
// any error after the stream starts (i.e. container not found, wrong parameters)
227-
// with the appropriate status code.
228-
stdout, stderr := httputils.BoolValue(r, "stdout"), httputils.BoolValue(r, "stderr")
229-
if !(stdout || stderr) {
230-
return fmt.Errorf("Bad parameters: you must choose at least one stream")
231-
}
232-
233-
serviceName := vars["id"]
234-
logsConfig := &backend.ContainerLogsConfig{
235-
ContainerLogsOptions: basictypes.ContainerLogsOptions{
236-
Follow: httputils.BoolValue(r, "follow"),
237-
Timestamps: httputils.BoolValue(r, "timestamps"),
238-
Since: r.Form.Get("since"),
239-
Tail: r.Form.Get("tail"),
240-
ShowStdout: stdout,
241-
ShowStderr: stderr,
242-
Details: httputils.BoolValue(r, "details"),
243-
},
244-
OutStream: w,
245-
}
246-
247-
if logsConfig.Details {
248-
return fmt.Errorf("Bad parameters: details is not currently supported")
249-
}
250-
251-
chStarted := make(chan struct{})
252-
if err := sr.backend.ServiceLogs(ctx, serviceName, logsConfig, chStarted); err != nil {
253-
select {
254-
case <-chStarted:
255-
// The client may be expecting all of the data we're sending to
256-
// be multiplexed, so send it through OutStream, which will
257-
// have been set up to handle that if needed.
258-
stdwriter := stdcopy.NewStdWriter(w, stdcopy.Systemerr)
259-
fmt.Fprintf(stdwriter, "Error grabbing service logs: %v\n", err)
260-
default:
261-
return err
262-
}
222+
// make a selector to pass to the helper function
223+
selector := &backend.LogSelector{
224+
Tasks: []string{vars["id"]},
263225
}
226+
return sr.swarmLogs(ctx, w, r, selector)
227+
}
264228

265-
return nil
229+
func (sr *swarmRouter) getServiceLogs(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
230+
if err := httputils.ParseForm(r); err != nil {
231+
return err
232+
}
233+
234+
// make a selector to pass to the helper function
235+
selector := &backend.LogSelector{
236+
Services: []string{vars["id"]},
237+
}
238+
return sr.swarmLogs(ctx, w, r, selector)
266239
}
267240

268241
func (sr *swarmRouter) getNodes(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {

api/server/router/swarm/helpers.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package swarm
2+
3+
import (
4+
"fmt"
5+
"net/http"
6+
7+
"github.com/docker/docker/api/server/httputils"
8+
basictypes "github.com/docker/docker/api/types"
9+
"github.com/docker/docker/api/types/backend"
10+
"github.com/docker/docker/pkg/stdcopy"
11+
"golang.org/x/net/context"
12+
)
13+
14+
// swarmLogs takes an http response, request, and selector, and writes the logs
15+
// specified by the selector to the response
16+
func (sr *swarmRouter) swarmLogs(ctx context.Context, w http.ResponseWriter, r *http.Request, selector *backend.LogSelector) error {
17+
// Args are validated before the stream starts because when it starts we're
18+
// sending HTTP 200 by writing an empty chunk of data to tell the client that
19+
// daemon is going to stream. By sending this initial HTTP 200 we can't report
20+
// any error after the stream starts (i.e. container not found, wrong parameters)
21+
// with the appropriate status code.
22+
stdout, stderr := httputils.BoolValue(r, "stdout"), httputils.BoolValue(r, "stderr")
23+
if !(stdout || stderr) {
24+
return fmt.Errorf("Bad parameters: you must choose at least one stream")
25+
}
26+
27+
logsConfig := &backend.ContainerLogsConfig{
28+
ContainerLogsOptions: basictypes.ContainerLogsOptions{
29+
Follow: httputils.BoolValue(r, "follow"),
30+
Timestamps: httputils.BoolValue(r, "timestamps"),
31+
Since: r.Form.Get("since"),
32+
Tail: r.Form.Get("tail"),
33+
ShowStdout: stdout,
34+
ShowStderr: stderr,
35+
Details: httputils.BoolValue(r, "details"),
36+
},
37+
OutStream: w,
38+
}
39+
40+
chStarted := make(chan struct{})
41+
if err := sr.backend.ServiceLogs(ctx, selector, logsConfig, chStarted); err != nil {
42+
select {
43+
case <-chStarted:
44+
// The client may be expecting all of the data we're sending to
45+
// be multiplexed, so send it through OutStream, which will
46+
// have been set up to handle that if needed.
47+
stdwriter := stdcopy.NewStdWriter(w, stdcopy.Systemerr)
48+
fmt.Fprintf(stdwriter, "Error grabbing service logs: %v\n", err)
49+
default:
50+
return err
51+
}
52+
}
53+
54+
return nil
55+
}

api/swagger.yaml

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7948,6 +7948,86 @@ paths:
79487948
required: true
79497949
type: "string"
79507950
tags: ["Task"]
7951+
/tasks/{id}/logs:
7952+
get:
7953+
summary: "Get task logs"
7954+
description: |
7955+
Get `stdout` and `stderr` logs from a task.
7956+
7957+
**Note**: This endpoint works only for services with the `json-file` or `journald` logging drivers.
7958+
operationId: "TaskLogs"
7959+
produces:
7960+
- "application/vnd.docker.raw-stream"
7961+
- "application/json"
7962+
responses:
7963+
101:
7964+
description: "logs returned as a stream"
7965+
schema:
7966+
type: "string"
7967+
format: "binary"
7968+
200:
7969+
description: "logs returned as a string in response body"
7970+
schema:
7971+
type: "string"
7972+
404:
7973+
description: "no such task"
7974+
schema:
7975+
$ref: "#/definitions/ErrorResponse"
7976+
examples:
7977+
application/json:
7978+
message: "No such task: c2ada9df5af8"
7979+
500:
7980+
description: "server error"
7981+
schema:
7982+
$ref: "#/definitions/ErrorResponse"
7983+
503:
7984+
description: "node is not part of a swarm"
7985+
schema:
7986+
$ref: "#/definitions/ErrorResponse"
7987+
parameters:
7988+
- name: "id"
7989+
in: "path"
7990+
required: true
7991+
description: "ID of the task"
7992+
type: "string"
7993+
- name: "details"
7994+
in: "query"
7995+
description: "Show extra details provided to logs."
7996+
type: "boolean"
7997+
default: false
7998+
- name: "follow"
7999+
in: "query"
8000+
description: |
8001+
Return the logs as a stream.
8002+
8003+
This will return a `101` HTTP response with a `Connection: upgrade` header, then hijack the HTTP connection to send raw output. For more information about hijacking and the stream format, [see the documentation for the attach endpoint](#operation/ContainerAttach).
8004+
type: "boolean"
8005+
default: false
8006+
- name: "stdout"
8007+
in: "query"
8008+
description: "Return logs from `stdout`"
8009+
type: "boolean"
8010+
default: false
8011+
- name: "stderr"
8012+
in: "query"
8013+
description: "Return logs from `stderr`"
8014+
type: "boolean"
8015+
default: false
8016+
- name: "since"
8017+
in: "query"
8018+
description: "Only return logs since this time, as a UNIX timestamp"
8019+
type: "integer"
8020+
default: 0
8021+
- name: "timestamps"
8022+
in: "query"
8023+
description: "Add timestamps to every log line"
8024+
type: "boolean"
8025+
default: false
8026+
- name: "tail"
8027+
in: "query"
8028+
description: "Only return this number of log lines from the end of the logs. Specify as an integer or `all` to output all log lines."
8029+
type: "string"
8030+
default: "all"
79518031
/secrets:
79528032
get:
79538033
summary: "List secrets"

api/types/backend/backend.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,16 @@ type ContainerLogsConfig struct {
3232
OutStream io.Writer
3333
}
3434

35+
// LogSelector is a list of services and tasks that should be returned as part
36+
// of a log stream. It is similar to swarmapi.LogSelector, with the difference
37+
// that the names don't have to be resolved to IDs; this is mostly to avoid
38+
// accidents later where a swarmapi LogSelector might have been incorrectly
39+
// used verbatim (and to avoid the handler having to import swarmapi types)
40+
type LogSelector struct {
41+
Services []string
42+
Tasks []string
43+
}
44+
3545
// ContainerStatsConfig holds information for configuring the runtime
3646
// behavior of a backend.ContainerStats() call.
3747
type ContainerStatsConfig struct {

cli/command/service/logs.go

Lines changed: 48 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,14 @@ type logsOptions struct {
3030
timestamps bool
3131
tail string
3232

33-
service string
33+
target string
3434
}
3535

36+
// TODO(dperny) the whole CLI for this is kind of a mess IMHOIRL and it needs
37+
// to be refactored agressively. There may be changes to the implementation of
38+
// details, which will be need to be reflected in this code. The refactoring
39+
// should be put off until we make those changes, tho, because I think the
40+
// decisions made WRT details will impact the design of the CLI.
3641
func newLogsCommand(dockerCli *command.DockerCli) *cobra.Command {
3742
var opts logsOptions
3843

@@ -41,16 +46,16 @@ func newLogsCommand(dockerCli *command.DockerCli) *cobra.Command {
4146
Short: "Fetch the logs of a service",
4247
Args: cli.ExactArgs(1),
4348
RunE: func(cmd *cobra.Command, args []string) error {
44-
opts.service = args[0]
49+
opts.target = args[0]
4550
return runLogs(dockerCli, &opts)
4651
},
4752
Tags: map[string]string{"experimental": ""},
4853
}
4954

5055
flags := cmd.Flags()
51-
flags.BoolVar(&opts.noResolve, "no-resolve", false, "Do not map IDs to Names")
56+
flags.BoolVar(&opts.noResolve, "no-resolve", false, "Do not map IDs to Names in output")
5257
flags.BoolVar(&opts.noTrunc, "no-trunc", false, "Do not truncate output")
53-
flags.BoolVar(&opts.noTaskIDs, "no-task-ids", false, "Do not include task IDs")
58+
flags.BoolVar(&opts.noTaskIDs, "no-task-ids", false, "Do not include task IDs in output")
5459
flags.BoolVarP(&opts.follow, "follow", "f", false, "Follow log output")
5560
flags.StringVar(&opts.since, "since", "", "Show logs since timestamp (e.g. 2013-01-02T13:23:37) or relative (e.g. 42m for 42 minutes)")
5661
flags.BoolVarP(&opts.timestamps, "timestamps", "t", false, "Show timestamps")
@@ -70,28 +75,44 @@ func runLogs(dockerCli *command.DockerCli, opts *logsOptions) error {
7075
Tail: opts.tail,
7176
}
7277

73-
client := dockerCli.Client()
78+
cli := dockerCli.Client()
7479

75-
service, _, err := client.ServiceInspectWithRaw(ctx, opts.service)
76-
if err != nil {
77-
return err
78-
}
80+
var (
81+
maxLength = 1
82+
responseBody io.ReadCloser
83+
)
7984

80-
responseBody, err := client.ServiceLogs(ctx, opts.service, options)
85+
service, _, err := cli.ServiceInspectWithRaw(ctx, opts.target)
8186
if err != nil {
82-
return err
87+
// if it's any error other than service not found, it's Real
88+
if !client.IsErrServiceNotFound(err) {
89+
return err
90+
}
91+
task, _, err := cli.TaskInspectWithRaw(ctx, opts.target)
92+
if err != nil {
93+
if client.IsErrTaskNotFound(err) {
94+
// if the task ALSO isn't found, rewrite the error to be clear
95+
// that we looked for services AND tasks
96+
err = fmt.Errorf("No such task or service")
97+
}
98+
return err
99+
}
100+
maxLength = getMaxLength(task.Slot)
101+
responseBody, err = cli.TaskLogs(ctx, opts.target, options)
102+
} else {
103+
responseBody, err = cli.ServiceLogs(ctx, opts.target, options)
104+
if err != nil {
105+
return err
106+
}
107+
if service.Spec.Mode.Replicated != nil && service.Spec.Mode.Replicated.Replicas != nil {
108+
// if replicas are initialized, figure out if we need to pad them
109+
replicas := *service.Spec.Mode.Replicated.Replicas
110+
maxLength = getMaxLength(int(replicas))
111+
}
83112
}
84113
defer responseBody.Close()
85114

86-
var replicas uint64
87-
padding := 1
88-
if service.Spec.Mode.Replicated != nil && service.Spec.Mode.Replicated.Replicas != nil {
89-
// if replicas are initialized, figure out if we need to pad them
90-
replicas = *service.Spec.Mode.Replicated.Replicas
91-
padding = len(strconv.FormatUint(replicas, 10))
92-
}
93-
94-
taskFormatter := newTaskFormatter(client, opts, padding)
115+
taskFormatter := newTaskFormatter(cli, opts, maxLength)
95116

96117
stdout := &logWriter{ctx: ctx, opts: opts, f: taskFormatter, w: dockerCli.Out()}
97118
stderr := &logWriter{ctx: ctx, opts: opts, f: taskFormatter, w: dockerCli.Err()}
@@ -101,6 +122,11 @@ func runLogs(dockerCli *command.DockerCli, opts *logsOptions) error {
101122
return err
102123
}
103124

125+
// getMaxLength gets the maximum length of the number in base 10
126+
func getMaxLength(i int) int {
127+
return len(strconv.FormatInt(int64(i), 10))
128+
}
129+
104130
type taskFormatter struct {
105131
client client.APIClient
106132
opts *logsOptions
@@ -148,7 +174,8 @@ func (f *taskFormatter) format(ctx context.Context, logCtx logContext) (string,
148174
taskName += fmt.Sprintf(".%s", stringid.TruncateID(task.ID))
149175
}
150176
}
151-
padding := strings.Repeat(" ", f.padding-len(strconv.FormatInt(int64(task.Slot), 10)))
177+
178+
padding := strings.Repeat(" ", f.padding-getMaxLength(task.Slot))
152179
formatted := fmt.Sprintf("%s@%s%s", taskName, nodeName, padding)
153180
f.cache[logCtx] = formatted
154181
return formatted, nil

client/interface.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ type ServiceAPIClient interface {
128128
ServiceRemove(ctx context.Context, serviceID string) error
129129
ServiceUpdate(ctx context.Context, serviceID string, version swarm.Version, service swarm.ServiceSpec, options types.ServiceUpdateOptions) (types.ServiceUpdateResponse, error)
130130
ServiceLogs(ctx context.Context, serviceID string, options types.ContainerLogsOptions) (io.ReadCloser, error)
131+
TaskLogs(ctx context.Context, taskID string, options types.ContainerLogsOptions) (io.ReadCloser, error)
131132
TaskInspectWithRaw(ctx context.Context, taskID string) (swarm.Task, []byte, error)
132133
TaskList(ctx context.Context, options types.TaskListOptions) ([]swarm.Task, error)
133134
}

0 commit comments

Comments
 (0)