|
@@ -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
|
|
|
|
+}
|