Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Service logs formatting #31672

Merged
merged 2 commits into from
Mar 13, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 1 addition & 21 deletions cli/command/idresolver/idresolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import (

"github.com/docker/docker/api/types/swarm"
"github.com/docker/docker/client"
"github.com/docker/docker/pkg/stringid"
)

// IDResolver provides ID to Name resolution.
Expand All @@ -27,7 +26,7 @@ func New(client client.APIClient, noResolve bool) *IDResolver {
}

func (r *IDResolver) get(ctx context.Context, t interface{}, id string) (string, error) {
switch t := t.(type) {
switch t.(type) {
case swarm.Node:
node, _, err := r.client.NodeInspectWithRaw(ctx, id)
if err != nil {
Expand All @@ -46,25 +45,6 @@ func (r *IDResolver) get(ctx context.Context, t interface{}, id string) (string,
return id, nil
}
return service.Spec.Annotations.Name, nil
case swarm.Task:
// If the caller passes the full task there's no need to do a lookup.
if t.ID == "" {
var err error

t, _, err = r.client.TaskInspectWithRaw(ctx, id)
if err != nil {
return id, nil
}
}
taskID := stringid.TruncateID(t.ID)
if t.ServiceID == "" {
return taskID, nil
}
service, err := r.Resolve(ctx, swarm.Service{}, t.ServiceID)
if err != nil {
return "", err
}
return fmt.Sprintf("%s.%d.%s", service, t.Slot, taskID), nil
default:
return "", fmt.Errorf("unsupported type")
}
Expand Down
129 changes: 105 additions & 24 deletions cli/command/service/logs.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"bytes"
"fmt"
"io"
"strconv"
"strings"

"golang.org/x/net/context"
Expand All @@ -13,16 +14,19 @@ import (
"github.com/docker/docker/cli"
"github.com/docker/docker/cli/command"
"github.com/docker/docker/cli/command/idresolver"
"github.com/docker/docker/client"
"github.com/docker/docker/pkg/stdcopy"
"github.com/docker/docker/pkg/stringid"
"github.com/spf13/cobra"
)

type logsOptions struct {
noResolve bool
noTrunc bool
noTaskIDs bool
follow bool
since string
timestamps bool
details bool
tail string

service string
Expand All @@ -44,10 +48,11 @@ func newLogsCommand(dockerCli *command.DockerCli) *cobra.Command {

flags := cmd.Flags()
flags.BoolVar(&opts.noResolve, "no-resolve", false, "Do not map IDs to Names")
flags.BoolVar(&opts.noTrunc, "no-trunc", false, "Do not truncate output")
flags.BoolVar(&opts.noTaskIDs, "no-task-ids", false, "Do not include task IDs")
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
}
Expand All @@ -62,30 +67,96 @@ func runLogs(dockerCli *command.DockerCli, opts *logsOptions) error {
Timestamps: opts.timestamps,
Follow: opts.follow,
Tail: opts.tail,
Details: opts.details,
}

client := dockerCli.Client()

service, _, err := client.ServiceInspectWithRaw(ctx, opts.service)
if err != nil {
return err
}

responseBody, err := client.ServiceLogs(ctx, opts.service, options)
if err != nil {
return err
}
defer responseBody.Close()

resolver := idresolver.New(client, opts.noResolve)
var replicas uint64
padding := 1
if service.Spec.Mode.Replicated != nil && service.Spec.Mode.Replicated.Replicas != nil {
// if replicas are initialized, figure out if we need to pad them
replicas = *service.Spec.Mode.Replicated.Replicas
padding = len(strconv.FormatUint(replicas, 10))
}

taskFormatter := newTaskFormatter(client, opts, padding)

stdout := &logWriter{ctx: ctx, opts: opts, r: resolver, w: dockerCli.Out()}
stderr := &logWriter{ctx: ctx, opts: opts, r: resolver, w: dockerCli.Err()}
stdout := &logWriter{ctx: ctx, opts: opts, f: taskFormatter, w: dockerCli.Out()}
stderr := &logWriter{ctx: ctx, opts: opts, f: taskFormatter, w: dockerCli.Err()}

// TODO(aluzzardi): Do an io.Copy for services with TTY enabled.
_, err = stdcopy.StdCopy(stdout, stderr, responseBody)
return err
}

type taskFormatter struct {
client client.APIClient
opts *logsOptions
padding int

r *idresolver.IDResolver
cache map[logContext]string
}

func newTaskFormatter(client client.APIClient, opts *logsOptions, padding int) *taskFormatter {
return &taskFormatter{
client: client,
opts: opts,
padding: padding,
r: idresolver.New(client, opts.noResolve),
cache: make(map[logContext]string),
}
}

func (f *taskFormatter) format(ctx context.Context, logCtx logContext) (string, error) {
if cached, ok := f.cache[logCtx]; ok {
return cached, nil
}

nodeName, err := f.r.Resolve(ctx, swarm.Node{}, logCtx.nodeID)
if err != nil {
return "", err
}

serviceName, err := f.r.Resolve(ctx, swarm.Service{}, logCtx.serviceID)
if err != nil {
return "", err
}

task, _, err := f.client.TaskInspectWithRaw(ctx, logCtx.taskID)
if err != nil {
return "", err
}

taskName := fmt.Sprintf("%s.%d", serviceName, task.Slot)
if !f.opts.noTaskIDs {
if f.opts.noTrunc {
taskName += fmt.Sprintf(".%s", task.ID)
} else {
taskName += fmt.Sprintf(".%s", stringid.TruncateID(task.ID))
}
}
padding := strings.Repeat(" ", f.padding-len(strconv.FormatInt(int64(task.Slot), 10)))
formatted := fmt.Sprintf("%s@%s%s", taskName, nodeName, padding)
f.cache[logCtx] = formatted
return formatted, nil
}

type logWriter struct {
ctx context.Context
opts *logsOptions
r *idresolver.IDResolver
f *taskFormatter
w io.Writer
}

Expand All @@ -102,7 +173,7 @@ func (lw *logWriter) Write(buf []byte) (int, error) {
return 0, fmt.Errorf("invalid context in log message: %v", string(buf))
}

taskName, nodeName, err := lw.parseContext(string(parts[contextIndex]))
logCtx, err := lw.parseContext(string(parts[contextIndex]))
if err != nil {
return 0, err
}
Expand All @@ -115,8 +186,11 @@ func (lw *logWriter) Write(buf []byte) (int, error) {
}

if i == contextIndex {
// TODO(aluzzardi): Consider constant padding.
output = append(output, []byte(fmt.Sprintf("%s@%s |", taskName, nodeName))...)
formatted, err := lw.f.format(lw.ctx, logCtx)
if err != nil {
return 0, err
}
output = append(output, []byte(fmt.Sprintf("%s |", formatted))...)
} else {
output = append(output, part...)
}
Expand All @@ -129,35 +203,42 @@ func (lw *logWriter) Write(buf []byte) (int, error) {
return len(buf), nil
}

func (lw *logWriter) parseContext(input string) (string, string, error) {
func (lw *logWriter) parseContext(input string) (logContext, 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)
return logContext{}, fmt.Errorf("invalid context: %s", input)
}
context[parts[0]] = parts[1]
}

taskID, ok := context["com.docker.swarm.task.id"]
nodeID, ok := context["com.docker.swarm.node.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
return logContext{}, fmt.Errorf("missing node id in context: %s", input)
}

nodeID, ok := context["com.docker.swarm.node.id"]
serviceID, ok := context["com.docker.swarm.service.id"]
if !ok {
return "", "", fmt.Errorf("missing node id in context: %s", input)
return logContext{}, fmt.Errorf("missing service id in context: %s", input)
}
nodeName, err := lw.r.Resolve(lw.ctx, swarm.Node{}, nodeID)
if err != nil {
return "", "", err

taskID, ok := context["com.docker.swarm.task.id"]
if !ok {
return logContext{}, fmt.Errorf("missing task id in context: %s", input)
}

return taskName, nodeName, nil
return logContext{
nodeID: nodeID,
serviceID: serviceID,
taskID: taskID,
}, nil
}

type logContext struct {
nodeID string
serviceID string
taskID string
}