Files
docker-cli/cli/command/service/logs.go
Sebastiaan van Stijn 78c54646c3 cli: disable file-completion by default
This uses the DefaultShellCompDirective feature which was added
in cobra to override the default (which would complete to use
files for commands and flags).

Note that we set "cobra.NoFileCompletions" for many commands, which
is redundant with this change, so we could remove as well.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-09-10 11:52:32 +02:00

349 lines
10 KiB
Go

package service
import (
"bytes"
"context"
"errors"
"fmt"
"io"
"sort"
"strconv"
"strings"
"github.com/containerd/errdefs"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/formatter"
"github.com/docker/cli/cli/command/idresolver"
"github.com/docker/cli/internal/logdetails"
"github.com/moby/moby/api/pkg/stdcopy"
"github.com/moby/moby/api/types/swarm"
"github.com/moby/moby/client"
"github.com/spf13/cobra"
)
type logsOptions struct {
noResolve bool
noTrunc bool
noTaskIDs bool
follow bool
since string
timestamps bool
tail string
details bool
raw bool
target string
}
func newLogsCommand(dockerCLI command.Cli) *cobra.Command {
var opts logsOptions
cmd := &cobra.Command{
Use: "logs [OPTIONS] SERVICE|TASK",
Short: "Fetch the logs of a service or task",
Args: cli.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
opts.target = args[0]
return runLogs(cmd.Context(), dockerCLI, &opts)
},
Annotations: map[string]string{"version": "1.29"},
ValidArgsFunction: completeServiceNames(dockerCLI),
DisableFlagsInUseLine: true,
}
flags := cmd.Flags()
// options specific to service logs
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.raw, "raw", false, "Do not neatly format logs")
flags.SetAnnotation("raw", "version", []string{"1.30"})
flags.BoolVar(&opts.noTaskIDs, "no-task-ids", false, "Do not include task IDs in output")
// options identical to container logs
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:37Z") or relative (e.g. "42m" for 42 minutes)`)
flags.BoolVarP(&opts.timestamps, "timestamps", "t", false, "Show timestamps")
flags.BoolVar(&opts.details, "details", false, "Show extra details provided to logs")
flags.SetAnnotation("details", "version", []string{"1.30"})
flags.StringVarP(&opts.tail, "tail", "n", "all", "Number of lines to show from the end of the logs")
return cmd
}
func runLogs(ctx context.Context, dockerCli command.Cli, opts *logsOptions) error {
apiClient := dockerCli.Client()
var (
maxLength = 1
responseBody io.ReadCloser
tty bool
// logfunc is used to delay the call to logs so that we can do some
// processing before we actually get the logs
logfunc func(context.Context, string, client.ContainerLogsOptions) (io.ReadCloser, error)
)
service, _, err := apiClient.ServiceInspectWithRaw(ctx, opts.target, client.ServiceInspectOptions{})
if err != nil {
// if it's any error other than service not found, it's Real
if !errdefs.IsNotFound(err) {
return err
}
task, _, err := apiClient.TaskInspectWithRaw(ctx, opts.target)
if err != nil {
if errdefs.IsNotFound(err) {
// if the task isn't found, rewrite the error to be clear
// that we looked for services AND tasks and found none
err = fmt.Errorf("no such task or service: %v", opts.target)
}
return err
}
tty = task.Spec.ContainerSpec.TTY
maxLength = getMaxLength(task.Slot)
// use the TaskLogs api function
logfunc = apiClient.TaskLogs
} else {
// use ServiceLogs api function
logfunc = apiClient.ServiceLogs
tty = service.Spec.TaskTemplate.ContainerSpec.TTY
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))
}
}
// we can't prettify tty logs. tell the user that this is the case.
// this is why we assign the logs function to a variable and delay calling
// it. we want to check this before we make the call and checking twice in
// each branch is even sloppier than this CLI disaster already is
if tty && !opts.raw {
return errors.New("tty service logs only supported with --raw")
}
// now get the logs
responseBody, err = logfunc(ctx, opts.target, client.ContainerLogsOptions{
ShowStdout: true,
ShowStderr: true,
Since: opts.since,
Timestamps: opts.timestamps,
Follow: opts.follow,
Tail: opts.tail,
// get the details if we request it OR if we're not doing raw mode
// (we need them for the context to pretty print)
Details: opts.details || !opts.raw,
})
if err != nil {
return err
}
defer responseBody.Close()
// tty logs get straight copied. they're not muxed with stdcopy
if tty {
_, err = io.Copy(dockerCli.Out(), responseBody)
return err
}
// otherwise, logs are multiplexed. if we're doing pretty printing, also
// create a task formatter.
var stdout, stderr io.Writer
stdout = dockerCli.Out()
stderr = dockerCli.Err()
if !opts.raw {
taskFormatter := newTaskFormatter(apiClient, opts, maxLength)
stdout = &logWriter{ctx: ctx, opts: opts, f: taskFormatter, w: stdout}
stderr = &logWriter{ctx: ctx, opts: opts, f: taskFormatter, w: stderr}
}
_, err = stdcopy.StdCopy(stdout, stderr, responseBody)
return err
}
// getMaxLength gets the maximum length of the number in base 10
func getMaxLength(i int) int {
return len(strconv.Itoa(i))
}
type taskFormatter struct {
client client.APIClient
opts *logsOptions
padding int
r *idresolver.IDResolver
// cache saves a pre-cooked logContext formatted string based on a
// logcontext object, so we don't have to resolve names every time
cache map[logContext]string
}
func newTaskFormatter(apiClient client.APIClient, opts *logsOptions, padding int) *taskFormatter {
return &taskFormatter{
client: apiClient,
opts: opts,
padding: padding,
r: idresolver.New(apiClient, opts.noResolve),
cache: make(map[logContext]string),
}
}
func (f *taskFormatter) format(ctx context.Context, logCtx logContext) (string, error) {
if cached, ok := f.cache[logCtx]; ok {
return cached, nil
}
nodeName, err := f.r.Resolve(ctx, swarm.Node{}, logCtx.nodeID)
if err != nil {
return "", err
}
serviceName, err := f.r.Resolve(ctx, swarm.Service{}, logCtx.serviceID)
if err != nil {
return "", err
}
task, _, err := f.client.TaskInspectWithRaw(ctx, logCtx.taskID)
if err != nil {
return "", err
}
taskName := fmt.Sprintf("%s.%d", serviceName, task.Slot)
if !f.opts.noTaskIDs {
if f.opts.noTrunc {
taskName += "." + task.ID
} else {
taskName += "." + formatter.TruncateID(task.ID)
}
}
paddingCount := f.padding - getMaxLength(task.Slot)
padding := ""
if paddingCount > 0 {
padding = strings.Repeat(" ", paddingCount)
}
formatted := taskName + "@" + nodeName + padding
f.cache[logCtx] = formatted
return formatted, nil
}
type logWriter struct {
ctx context.Context
opts *logsOptions
f *taskFormatter
w io.Writer
}
func (lw *logWriter) Write(buf []byte) (int, error) {
// this works but ONLY because stdcopy calls write a whole line at a time.
// if this ends up horribly broken or panics, check to see if stdcopy has
// reneged on that assumption. (@god forgive me)
// also this only works because the logs format is, like, barely parsable.
// if something changes in the logs format, this is gonna break
// there should always be at least 2 parts: details and message. if there
// is no timestamp, details will be first (index 0) when we split on
// spaces. if there is a timestamp, details will be 2nd (`index 1)
detailsIndex := 0
numParts := 2
if lw.opts.timestamps {
detailsIndex++
numParts++
}
// break up the log line into parts.
parts := bytes.SplitN(buf, []byte(" "), numParts)
if len(parts) != numParts {
return 0, fmt.Errorf("invalid context in log message: %v", string(buf))
}
// parse the details out
details, err := logdetails.Parse(string(parts[detailsIndex]))
if err != nil {
return 0, err
}
// and then create a context from the details
// this removes the context-specific details from the details map, so we
// can more easily print the details later
logCtx, err := parseContext(details)
if err != nil {
return 0, err
}
output := []byte{}
// if we included timestamps, add them to the front
if lw.opts.timestamps {
output = append(output, parts[0]...)
output = append(output, ' ')
}
// add the context, nice and formatted
formatted, err := lw.f.format(lw.ctx, logCtx)
if err != nil {
return 0, err
}
output = append(output, []byte(formatted+" | ")...)
// if the user asked for details, add them to be log message
if lw.opts.details {
// ugh i hate this it's basically a dupe of api/server/httputils/write_log_stream.go:stringAttrs()
// ok but we're gonna do it a bit different
// there are optimizations that can be made here. for starters, i'd
// suggest caching the details keys. then, we can maybe draw maps and
// slices from a pool to avoid alloc overhead on them. idk if it's
// worth the time yet.
// first we need a slice
d := make([]string, 0, len(details))
// then let's add all the pairs
for k := range details {
d = append(d, k+"="+details[k])
}
// then sort em
sort.Strings(d)
// then join and append
output = append(output, []byte(strings.Join(d, ","))...)
output = append(output, ' ')
}
// add the log message itself, finally
output = append(output, parts[detailsIndex+1]...)
_, err = lw.w.Write(output)
if err != nil {
return 0, err
}
return len(buf), nil
}
// parseContext returns a log context and REMOVES the context from the details map
func parseContext(details map[string]string) (logContext, error) {
nodeID, ok := details["com.docker.swarm.node.id"]
if !ok {
return logContext{}, fmt.Errorf("missing node id in details: %v", details)
}
delete(details, "com.docker.swarm.node.id")
serviceID, ok := details["com.docker.swarm.service.id"]
if !ok {
return logContext{}, fmt.Errorf("missing service id in details: %v", details)
}
delete(details, "com.docker.swarm.service.id")
taskID, ok := details["com.docker.swarm.task.id"]
if !ok {
return logContext{}, fmt.Errorf("missing task id in details: %s", details)
}
delete(details, "com.docker.swarm.task.id")
return logContext{
nodeID: nodeID,
serviceID: serviceID,
taskID: taskID,
}, nil
}
type logContext struct {
nodeID string
serviceID string
taskID string
}