github.1git.de/docker/cli@v26.1.3+incompatible/cli/command/service/logs.go (about)

     1  package service
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"fmt"
     7  	"io"
     8  	"sort"
     9  	"strconv"
    10  	"strings"
    11  
    12  	"github.com/docker/cli/cli"
    13  	"github.com/docker/cli/cli/command"
    14  	"github.com/docker/cli/cli/command/idresolver"
    15  	"github.com/docker/cli/service/logs"
    16  	"github.com/docker/docker/api/types"
    17  	"github.com/docker/docker/api/types/container"
    18  	"github.com/docker/docker/api/types/swarm"
    19  	"github.com/docker/docker/client"
    20  	"github.com/docker/docker/errdefs"
    21  	"github.com/docker/docker/pkg/stdcopy"
    22  	"github.com/docker/docker/pkg/stringid"
    23  	"github.com/pkg/errors"
    24  	"github.com/spf13/cobra"
    25  )
    26  
    27  type logsOptions struct {
    28  	noResolve  bool
    29  	noTrunc    bool
    30  	noTaskIDs  bool
    31  	follow     bool
    32  	since      string
    33  	timestamps bool
    34  	tail       string
    35  	details    bool
    36  	raw        bool
    37  
    38  	target string
    39  }
    40  
    41  func newLogsCommand(dockerCli command.Cli) *cobra.Command {
    42  	var opts logsOptions
    43  
    44  	cmd := &cobra.Command{
    45  		Use:   "logs [OPTIONS] SERVICE|TASK",
    46  		Short: "Fetch the logs of a service or task",
    47  		Args:  cli.ExactArgs(1),
    48  		RunE: func(cmd *cobra.Command, args []string) error {
    49  			opts.target = args[0]
    50  			return runLogs(cmd.Context(), dockerCli, &opts)
    51  		},
    52  		Annotations: map[string]string{"version": "1.29"},
    53  		ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
    54  			return CompletionFn(dockerCli)(cmd, args, toComplete)
    55  		},
    56  	}
    57  
    58  	flags := cmd.Flags()
    59  	// options specific to service logs
    60  	flags.BoolVar(&opts.noResolve, "no-resolve", false, "Do not map IDs to Names in output")
    61  	flags.BoolVar(&opts.noTrunc, "no-trunc", false, "Do not truncate output")
    62  	flags.BoolVar(&opts.raw, "raw", false, "Do not neatly format logs")
    63  	flags.SetAnnotation("raw", "version", []string{"1.30"})
    64  	flags.BoolVar(&opts.noTaskIDs, "no-task-ids", false, "Do not include task IDs in output")
    65  	// options identical to container logs
    66  	flags.BoolVarP(&opts.follow, "follow", "f", false, "Follow log output")
    67  	flags.StringVar(&opts.since, "since", "", `Show logs since timestamp (e.g. "2013-01-02T13:23:37Z") or relative (e.g. "42m" for 42 minutes)`)
    68  	flags.BoolVarP(&opts.timestamps, "timestamps", "t", false, "Show timestamps")
    69  	flags.BoolVar(&opts.details, "details", false, "Show extra details provided to logs")
    70  	flags.SetAnnotation("details", "version", []string{"1.30"})
    71  	flags.StringVarP(&opts.tail, "tail", "n", "all", "Number of lines to show from the end of the logs")
    72  	return cmd
    73  }
    74  
    75  func runLogs(ctx context.Context, dockerCli command.Cli, opts *logsOptions) error {
    76  	apiClient := dockerCli.Client()
    77  
    78  	var (
    79  		maxLength    = 1
    80  		responseBody io.ReadCloser
    81  		tty          bool
    82  		// logfunc is used to delay the call to logs so that we can do some
    83  		// processing before we actually get the logs
    84  		logfunc func(context.Context, string, container.LogsOptions) (io.ReadCloser, error)
    85  	)
    86  
    87  	service, _, err := apiClient.ServiceInspectWithRaw(ctx, opts.target, types.ServiceInspectOptions{})
    88  	if err != nil {
    89  		// if it's any error other than service not found, it's Real
    90  		if !errdefs.IsNotFound(err) {
    91  			return err
    92  		}
    93  		task, _, err := apiClient.TaskInspectWithRaw(ctx, opts.target)
    94  		if err != nil {
    95  			if errdefs.IsNotFound(err) {
    96  				// if the task isn't found, rewrite the error to be clear
    97  				// that we looked for services AND tasks and found none
    98  				err = fmt.Errorf("no such task or service: %v", opts.target)
    99  			}
   100  			return err
   101  		}
   102  
   103  		tty = task.Spec.ContainerSpec.TTY
   104  		maxLength = getMaxLength(task.Slot)
   105  
   106  		// use the TaskLogs api function
   107  		logfunc = apiClient.TaskLogs
   108  	} else {
   109  		// use ServiceLogs api function
   110  		logfunc = apiClient.ServiceLogs
   111  		tty = service.Spec.TaskTemplate.ContainerSpec.TTY
   112  		if service.Spec.Mode.Replicated != nil && service.Spec.Mode.Replicated.Replicas != nil {
   113  			// if replicas are initialized, figure out if we need to pad them
   114  			replicas := *service.Spec.Mode.Replicated.Replicas
   115  			maxLength = getMaxLength(int(replicas))
   116  		}
   117  	}
   118  
   119  	// we can't prettify tty logs. tell the user that this is the case.
   120  	// this is why we assign the logs function to a variable and delay calling
   121  	// it. we want to check this before we make the call and checking twice in
   122  	// each branch is even sloppier than this CLI disaster already is
   123  	if tty && !opts.raw {
   124  		return errors.New("tty service logs only supported with --raw")
   125  	}
   126  
   127  	// now get the logs
   128  	responseBody, err = logfunc(ctx, opts.target, container.LogsOptions{
   129  		ShowStdout: true,
   130  		ShowStderr: true,
   131  		Since:      opts.since,
   132  		Timestamps: opts.timestamps,
   133  		Follow:     opts.follow,
   134  		Tail:       opts.tail,
   135  		// get the details if we request it OR if we're not doing raw mode
   136  		// (we need them for the context to pretty print)
   137  		Details: opts.details || !opts.raw,
   138  	})
   139  	if err != nil {
   140  		return err
   141  	}
   142  	defer responseBody.Close()
   143  
   144  	// tty logs get straight copied. they're not muxed with stdcopy
   145  	if tty {
   146  		_, err = io.Copy(dockerCli.Out(), responseBody)
   147  		return err
   148  	}
   149  
   150  	// otherwise, logs are multiplexed. if we're doing pretty printing, also
   151  	// create a task formatter.
   152  	var stdout, stderr io.Writer
   153  	stdout = dockerCli.Out()
   154  	stderr = dockerCli.Err()
   155  	if !opts.raw {
   156  		taskFormatter := newTaskFormatter(apiClient, opts, maxLength)
   157  
   158  		stdout = &logWriter{ctx: ctx, opts: opts, f: taskFormatter, w: stdout}
   159  		stderr = &logWriter{ctx: ctx, opts: opts, f: taskFormatter, w: stderr}
   160  	}
   161  
   162  	_, err = stdcopy.StdCopy(stdout, stderr, responseBody)
   163  	return err
   164  }
   165  
   166  // getMaxLength gets the maximum length of the number in base 10
   167  func getMaxLength(i int) int {
   168  	return len(strconv.Itoa(i))
   169  }
   170  
   171  type taskFormatter struct {
   172  	client  client.APIClient
   173  	opts    *logsOptions
   174  	padding int
   175  
   176  	r *idresolver.IDResolver
   177  	// cache saves a pre-cooked logContext formatted string based on a
   178  	// logcontext object, so we don't have to resolve names every time
   179  	cache map[logContext]string
   180  }
   181  
   182  func newTaskFormatter(apiClient client.APIClient, opts *logsOptions, padding int) *taskFormatter {
   183  	return &taskFormatter{
   184  		client:  apiClient,
   185  		opts:    opts,
   186  		padding: padding,
   187  		r:       idresolver.New(apiClient, opts.noResolve),
   188  		cache:   make(map[logContext]string),
   189  	}
   190  }
   191  
   192  func (f *taskFormatter) format(ctx context.Context, logCtx logContext) (string, error) {
   193  	if cached, ok := f.cache[logCtx]; ok {
   194  		return cached, nil
   195  	}
   196  
   197  	nodeName, err := f.r.Resolve(ctx, swarm.Node{}, logCtx.nodeID)
   198  	if err != nil {
   199  		return "", err
   200  	}
   201  
   202  	serviceName, err := f.r.Resolve(ctx, swarm.Service{}, logCtx.serviceID)
   203  	if err != nil {
   204  		return "", err
   205  	}
   206  
   207  	task, _, err := f.client.TaskInspectWithRaw(ctx, logCtx.taskID)
   208  	if err != nil {
   209  		return "", err
   210  	}
   211  
   212  	taskName := fmt.Sprintf("%s.%d", serviceName, task.Slot)
   213  	if !f.opts.noTaskIDs {
   214  		if f.opts.noTrunc {
   215  			taskName += fmt.Sprintf(".%s", task.ID)
   216  		} else {
   217  			taskName += fmt.Sprintf(".%s", stringid.TruncateID(task.ID))
   218  		}
   219  	}
   220  
   221  	paddingCount := f.padding - getMaxLength(task.Slot)
   222  	padding := ""
   223  	if paddingCount > 0 {
   224  		padding = strings.Repeat(" ", paddingCount)
   225  	}
   226  	formatted := taskName + "@" + nodeName + padding
   227  	f.cache[logCtx] = formatted
   228  	return formatted, nil
   229  }
   230  
   231  type logWriter struct {
   232  	ctx  context.Context
   233  	opts *logsOptions
   234  	f    *taskFormatter
   235  	w    io.Writer
   236  }
   237  
   238  func (lw *logWriter) Write(buf []byte) (int, error) {
   239  	// this works but ONLY because stdcopy calls write a whole line at a time.
   240  	// if this ends up horribly broken or panics, check to see if stdcopy has
   241  	// reneged on that assumption. (@god forgive me)
   242  	// also this only works because the logs format is, like, barely parsable.
   243  	// if something changes in the logs format, this is gonna break
   244  
   245  	// there should always be at least 2 parts: details and message. if there
   246  	// is no timestamp, details will be first (index 0) when we split on
   247  	// spaces. if there is a timestamp, details will be 2nd (`index 1)
   248  	detailsIndex := 0
   249  	numParts := 2
   250  	if lw.opts.timestamps {
   251  		detailsIndex++
   252  		numParts++
   253  	}
   254  
   255  	// break up the log line into parts.
   256  	parts := bytes.SplitN(buf, []byte(" "), numParts)
   257  	if len(parts) != numParts {
   258  		return 0, errors.Errorf("invalid context in log message: %v", string(buf))
   259  	}
   260  	// parse the details out
   261  	details, err := logs.ParseLogDetails(string(parts[detailsIndex]))
   262  	if err != nil {
   263  		return 0, err
   264  	}
   265  	// and then create a context from the details
   266  	// this removes the context-specific details from the details map, so we
   267  	// can more easily print the details later
   268  	logCtx, err := lw.parseContext(details)
   269  	if err != nil {
   270  		return 0, err
   271  	}
   272  
   273  	output := []byte{}
   274  	// if we included timestamps, add them to the front
   275  	if lw.opts.timestamps {
   276  		output = append(output, parts[0]...)
   277  		output = append(output, ' ')
   278  	}
   279  	// add the context, nice and formatted
   280  	formatted, err := lw.f.format(lw.ctx, logCtx)
   281  	if err != nil {
   282  		return 0, err
   283  	}
   284  	output = append(output, []byte(formatted+"    | ")...)
   285  	// if the user asked for details, add them to be log message
   286  	if lw.opts.details {
   287  		// ugh i hate this it's basically a dupe of api/server/httputils/write_log_stream.go:stringAttrs()
   288  		// ok but we're gonna do it a bit different
   289  
   290  		// there are optimizations that can be made here. for starters, i'd
   291  		// suggest caching the details keys. then, we can maybe draw maps and
   292  		// slices from a pool to avoid alloc overhead on them. idk if it's
   293  		// worth the time yet.
   294  
   295  		// first we need a slice
   296  		d := make([]string, 0, len(details))
   297  		// then let's add all the pairs
   298  		for k := range details {
   299  			d = append(d, k+"="+details[k])
   300  		}
   301  		// then sort em
   302  		sort.Strings(d)
   303  		// then join and append
   304  		output = append(output, []byte(strings.Join(d, ","))...)
   305  		output = append(output, ' ')
   306  	}
   307  
   308  	// add the log message itself, finally
   309  	output = append(output, parts[detailsIndex+1]...)
   310  
   311  	_, err = lw.w.Write(output)
   312  	if err != nil {
   313  		return 0, err
   314  	}
   315  
   316  	return len(buf), nil
   317  }
   318  
   319  // parseContext returns a log context and REMOVES the context from the details map
   320  func (lw *logWriter) parseContext(details map[string]string) (logContext, error) {
   321  	nodeID, ok := details["com.docker.swarm.node.id"]
   322  	if !ok {
   323  		return logContext{}, errors.Errorf("missing node id in details: %v", details)
   324  	}
   325  	delete(details, "com.docker.swarm.node.id")
   326  
   327  	serviceID, ok := details["com.docker.swarm.service.id"]
   328  	if !ok {
   329  		return logContext{}, errors.Errorf("missing service id in details: %v", details)
   330  	}
   331  	delete(details, "com.docker.swarm.service.id")
   332  
   333  	taskID, ok := details["com.docker.swarm.task.id"]
   334  	if !ok {
   335  		return logContext{}, errors.Errorf("missing task id in details: %s", details)
   336  	}
   337  	delete(details, "com.docker.swarm.task.id")
   338  
   339  	return logContext{
   340  		nodeID:    nodeID,
   341  		serviceID: serviceID,
   342  		taskID:    taskID,
   343  	}, nil
   344  }
   345  
   346  type logContext struct {
   347  	nodeID    string
   348  	serviceID string
   349  	taskID    string
   350  }