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