Ver Fonte

cli: docker service logs support

Signed-off-by: Andrea Luzzardi <aluzzardi@gmail.com>
Andrea Luzzardi há 8 anos atrás
pai
commit
c7995fdc77

+ 1 - 0
cli/command/service/cmd.go

@@ -26,6 +26,7 @@ func NewServiceCommand(dockerCli *command.DockerCli) *cobra.Command {
 		newRemoveCommand(dockerCli),
 		newScaleCommand(dockerCli),
 		newUpdateCommand(dockerCli),
+		newLogsCommand(dockerCli),
 	)
 	return cmd
 }

+ 163 - 0
cli/command/service/logs.go

@@ -0,0 +1,163 @@
+package service
+
+import (
+	"bytes"
+	"fmt"
+	"io"
+	"strings"
+
+	"golang.org/x/net/context"
+
+	"github.com/docker/docker/api/types"
+	"github.com/docker/docker/api/types/swarm"
+	"github.com/docker/docker/cli"
+	"github.com/docker/docker/cli/command"
+	"github.com/docker/docker/cli/command/idresolver"
+	"github.com/docker/docker/pkg/stdcopy"
+	"github.com/spf13/cobra"
+)
+
+type logsOptions struct {
+	noResolve  bool
+	follow     bool
+	since      string
+	timestamps bool
+	details    bool
+	tail       string
+
+	service string
+}
+
+func newLogsCommand(dockerCli *command.DockerCli) *cobra.Command {
+	var opts logsOptions
+
+	cmd := &cobra.Command{
+		Use:   "logs [OPTIONS] SERVICE",
+		Short: "Fetch the logs of a service",
+		Args:  cli.ExactArgs(1),
+		RunE: func(cmd *cobra.Command, args []string) error {
+			opts.service = 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.BoolVarP(&opts.follow, "follow", "f", false, "Follow log output")
+	flags.StringVar(&opts.since, "since", "", "Show logs since timestamp")
+	flags.BoolVarP(&opts.timestamps, "timestamps", "t", false, "Show timestamps")
+	flags.BoolVar(&opts.details, "details", false, "Show extra details provided to logs")
+	flags.StringVar(&opts.tail, "tail", "all", "Number of lines to show from the end of the logs")
+	return cmd
+}
+
+func runLogs(dockerCli *command.DockerCli, opts *logsOptions) error {
+	ctx := context.Background()
+
+	options := types.ContainerLogsOptions{
+		ShowStdout: true,
+		ShowStderr: true,
+		Since:      opts.since,
+		Timestamps: opts.timestamps,
+		Follow:     opts.follow,
+		Tail:       opts.tail,
+		Details:    opts.details,
+	}
+
+	client := dockerCli.Client()
+	responseBody, err := client.ServiceLogs(ctx, opts.service, options)
+	if err != nil {
+		return err
+	}
+	defer responseBody.Close()
+
+	resolver := idresolver.New(client, opts.noResolve)
+
+	stdout := &logWriter{ctx: ctx, opts: opts, r: resolver, w: dockerCli.Out()}
+	stderr := &logWriter{ctx: ctx, opts: opts, r: resolver, w: dockerCli.Err()}
+
+	// TODO(aluzzardi): Do an io.Copy for services with TTY enabled.
+	_, err = stdcopy.StdCopy(stdout, stderr, responseBody)
+	return err
+}
+
+type logWriter struct {
+	ctx  context.Context
+	opts *logsOptions
+	r    *idresolver.IDResolver
+	w    io.Writer
+}
+
+func (lw *logWriter) Write(buf []byte) (int, error) {
+	contextIndex := 0
+	numParts := 2
+	if lw.opts.timestamps {
+		contextIndex++
+		numParts++
+	}
+
+	parts := bytes.SplitN(buf, []byte(" "), numParts)
+	if len(parts) != numParts {
+		return 0, fmt.Errorf("invalid context in log message: %v", string(buf))
+	}
+
+	taskName, nodeName, err := lw.parseContext(string(parts[contextIndex]))
+	if err != nil {
+		return 0, err
+	}
+
+	output := []byte{}
+	for i, part := range parts {
+		// First part doesn't get space separation.
+		if i > 0 {
+			output = append(output, []byte(" ")...)
+		}
+
+		if i == contextIndex {
+			// TODO(aluzzardi): Consider constant padding.
+			output = append(output, []byte(fmt.Sprintf("%s@%s    |", taskName, nodeName))...)
+		} else {
+			output = append(output, part...)
+		}
+	}
+	_, err = lw.w.Write(output)
+	if err != nil {
+		return 0, err
+	}
+
+	return len(buf), nil
+}
+
+func (lw *logWriter) parseContext(input string) (string, string, error) {
+	context := make(map[string]string)
+
+	components := strings.Split(input, ",")
+	for _, component := range components {
+		parts := strings.SplitN(component, "=", 2)
+		if len(parts) != 2 {
+			return "", "", fmt.Errorf("invalid context: %s", input)
+		}
+		context[parts[0]] = parts[1]
+	}
+
+	taskID, ok := context["com.docker.swarm.task.id"]
+	if !ok {
+		return "", "", fmt.Errorf("missing task id in context: %s", input)
+	}
+	taskName, err := lw.r.Resolve(lw.ctx, swarm.Task{}, taskID)
+	if err != nil {
+		return "", "", err
+	}
+
+	nodeID, ok := context["com.docker.swarm.node.id"]
+	if !ok {
+		return "", "", fmt.Errorf("missing node id in context: %s", input)
+	}
+	nodeName, err := lw.r.Resolve(lw.ctx, swarm.Node{}, nodeID)
+	if err != nil {
+		return "", "", err
+	}
+
+	return taskName, nodeName, nil
+}

+ 55 - 0
integration-cli/docker_cli_service_logs_experimental_test.go

@@ -0,0 +1,55 @@
+// +build !windows
+
+package main
+
+import (
+	"bufio"
+	"io"
+	"os/exec"
+	"strings"
+
+	"github.com/docker/docker/pkg/integration/checker"
+	"github.com/go-check/check"
+)
+
+type logMessage struct {
+	err  error
+	data []byte
+}
+
+func (s *DockerSwarmSuite) TestServiceLogs(c *check.C) {
+	testRequires(c, ExperimentalDaemon)
+
+	d := s.AddDaemon(c, true, true)
+
+	name := "TestServiceLogs"
+
+	out, err := d.Cmd("service", "create", "--name", name, "busybox", "sh", "-c", "while true; do echo log test; sleep 1; done")
+	c.Assert(err, checker.IsNil)
+	c.Assert(strings.TrimSpace(out), checker.Not(checker.Equals), "")
+
+	// make sure task has been deployed.
+	waitAndAssert(c, defaultReconciliationTimeout, d.checkActiveContainerCount, checker.Equals, 1)
+
+	args := []string{"service", "logs", "-f", name}
+	cmd := exec.Command(dockerBinary, d.prependHostArg(args)...)
+	r, w := io.Pipe()
+	cmd.Stdout = w
+	cmd.Stderr = w
+	c.Assert(cmd.Start(), checker.IsNil)
+
+	// Make sure pipe is written to
+	ch := make(chan *logMessage)
+	go func() {
+		reader := bufio.NewReader(r)
+		msg := &logMessage{}
+		msg.data, _, msg.err = reader.ReadLine()
+		ch <- msg
+	}()
+
+	msg := <-ch
+	c.Assert(msg.err, checker.IsNil)
+	c.Assert(string(msg.data), checker.Contains, "log test")
+
+	c.Assert(cmd.Process.Kill(), checker.IsNil)
+}