From b4ca6ebb098b78da7e4a697232ac9eaa4ddc568c Mon Sep 17 00:00:00 2001 From: Drew Erny Date: Tue, 21 Mar 2017 11:35:55 -0700 Subject: [PATCH] Add support for task and arbitrary combo logs Refactored the API to more easily accept new endpoints. Added REST, client, and CLI endpoints for getting logs from a specific task. All that is needed after this commit to enable arbitrary service log selectors is a REST endpoint and handler. Task logs can be retrieved by putting in a task ID at the CLI instead of a service ID. Signed-off-by: Drew Erny --- command/service/logs.go | 69 ++++++++++++++++++++++++++++------------- 1 file changed, 48 insertions(+), 21 deletions(-) diff --git a/command/service/logs.go b/command/service/logs.go index 1bf5723ae0..da2374f9dd 100644 --- a/command/service/logs.go +++ b/command/service/logs.go @@ -30,9 +30,14 @@ type logsOptions struct { timestamps bool tail string - service string + target string } +// TODO(dperny) the whole CLI for this is kind of a mess IMHOIRL and it needs +// to be refactored agressively. There may be changes to the implementation of +// details, which will be need to be reflected in this code. The refactoring +// should be put off until we make those changes, tho, because I think the +// decisions made WRT details will impact the design of the CLI. func newLogsCommand(dockerCli *command.DockerCli) *cobra.Command { var opts logsOptions @@ -41,16 +46,16 @@ func newLogsCommand(dockerCli *command.DockerCli) *cobra.Command { Short: "Fetch the logs of a service", Args: cli.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - opts.service = args[0] + opts.target = args[0] return runLogs(dockerCli, &opts) }, Tags: map[string]string{"experimental": ""}, } flags := cmd.Flags() - flags.BoolVar(&opts.noResolve, "no-resolve", false, "Do not map IDs to Names") + flags.BoolVar(&opts.noResolve, "no-resolve", false, "Do not map IDs to Names in output") flags.BoolVar(&opts.noTrunc, "no-trunc", false, "Do not truncate output") - flags.BoolVar(&opts.noTaskIDs, "no-task-ids", false, "Do not include task IDs") + flags.BoolVar(&opts.noTaskIDs, "no-task-ids", false, "Do not include task IDs in output") flags.BoolVarP(&opts.follow, "follow", "f", false, "Follow log output") flags.StringVar(&opts.since, "since", "", "Show logs since timestamp (e.g. 2013-01-02T13:23:37) or relative (e.g. 42m for 42 minutes)") flags.BoolVarP(&opts.timestamps, "timestamps", "t", false, "Show timestamps") @@ -70,28 +75,44 @@ func runLogs(dockerCli *command.DockerCli, opts *logsOptions) error { Tail: opts.tail, } - client := dockerCli.Client() + cli := dockerCli.Client() - service, _, err := client.ServiceInspectWithRaw(ctx, opts.service) - if err != nil { - return err - } + var ( + maxLength = 1 + responseBody io.ReadCloser + ) - responseBody, err := client.ServiceLogs(ctx, opts.service, options) + service, _, err := cli.ServiceInspectWithRaw(ctx, opts.target) if err != nil { - return err + // if it's any error other than service not found, it's Real + if !client.IsErrServiceNotFound(err) { + return err + } + task, _, err := cli.TaskInspectWithRaw(ctx, opts.target) + if err != nil { + if client.IsErrTaskNotFound(err) { + // if the task ALSO isn't found, rewrite the error to be clear + // that we looked for services AND tasks + err = fmt.Errorf("No such task or service") + } + return err + } + maxLength = getMaxLength(task.Slot) + responseBody, err = cli.TaskLogs(ctx, opts.target, options) + } else { + responseBody, err = cli.ServiceLogs(ctx, opts.target, options) + if err != nil { + return err + } + if service.Spec.Mode.Replicated != nil && service.Spec.Mode.Replicated.Replicas != nil { + // if replicas are initialized, figure out if we need to pad them + replicas := *service.Spec.Mode.Replicated.Replicas + maxLength = getMaxLength(int(replicas)) + } } defer responseBody.Close() - var replicas uint64 - padding := 1 - if service.Spec.Mode.Replicated != nil && service.Spec.Mode.Replicated.Replicas != nil { - // if replicas are initialized, figure out if we need to pad them - replicas = *service.Spec.Mode.Replicated.Replicas - padding = len(strconv.FormatUint(replicas, 10)) - } - - taskFormatter := newTaskFormatter(client, opts, padding) + taskFormatter := newTaskFormatter(cli, opts, maxLength) stdout := &logWriter{ctx: ctx, opts: opts, f: taskFormatter, w: dockerCli.Out()} stderr := &logWriter{ctx: ctx, opts: opts, f: taskFormatter, w: dockerCli.Err()} @@ -101,6 +122,11 @@ func runLogs(dockerCli *command.DockerCli, opts *logsOptions) error { return err } +// getMaxLength gets the maximum length of the number in base 10 +func getMaxLength(i int) int { + return len(strconv.FormatInt(int64(i), 10)) +} + type taskFormatter struct { client client.APIClient opts *logsOptions @@ -148,7 +174,8 @@ func (f *taskFormatter) format(ctx context.Context, logCtx logContext) (string, taskName += fmt.Sprintf(".%s", stringid.TruncateID(task.ID)) } } - padding := strings.Repeat(" ", f.padding-len(strconv.FormatInt(int64(task.Slot), 10))) + + padding := strings.Repeat(" ", f.padding-getMaxLength(task.Slot)) formatted := fmt.Sprintf("%s@%s%s", taskName, nodeName, padding) f.cache[logCtx] = formatted return formatted, nil