cli: docker service logs support
Signed-off-by: Andrea Luzzardi <aluzzardi@gmail.com>
This commit is contained in:
parent
819d0159bb
commit
c7995fdc77
3 changed files with 219 additions and 0 deletions
|
@ -26,6 +26,7 @@ func NewServiceCommand(dockerCli *command.DockerCli) *cobra.Command {
|
|||
newRemoveCommand(dockerCli),
|
||||
newScaleCommand(dockerCli),
|
||||
newUpdateCommand(dockerCli),
|
||||
newLogsCommand(dockerCli),
|
||||
)
|
||||
return cmd
|
||||
}
|
||||
|
|
163
cli/command/service/logs.go
Normal file
163
cli/command/service/logs.go
Normal file
|
@ -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
integration-cli/docker_cli_service_logs_experimental_test.go
Normal file
55
integration-cli/docker_cli_service_logs_experimental_test.go
Normal file
|
@ -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)
|
||||
}
|
Loading…
Reference in a new issue