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