Signed-off-by: Andrea Luzzardi <aluzzardi@gmail.com>
| 32 | 33 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,163 @@ |
| 0 |
+package service |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "bytes" |
|
| 4 |
+ "fmt" |
|
| 5 |
+ "io" |
|
| 6 |
+ "strings" |
|
| 7 |
+ |
|
| 8 |
+ "golang.org/x/net/context" |
|
| 9 |
+ |
|
| 10 |
+ "github.com/docker/docker/api/types" |
|
| 11 |
+ "github.com/docker/docker/api/types/swarm" |
|
| 12 |
+ "github.com/docker/docker/cli" |
|
| 13 |
+ "github.com/docker/docker/cli/command" |
|
| 14 |
+ "github.com/docker/docker/cli/command/idresolver" |
|
| 15 |
+ "github.com/docker/docker/pkg/stdcopy" |
|
| 16 |
+ "github.com/spf13/cobra" |
|
| 17 |
+) |
|
| 18 |
+ |
|
| 19 |
+type logsOptions struct {
|
|
| 20 |
+ noResolve bool |
|
| 21 |
+ follow bool |
|
| 22 |
+ since string |
|
| 23 |
+ timestamps bool |
|
| 24 |
+ details bool |
|
| 25 |
+ tail string |
|
| 26 |
+ |
|
| 27 |
+ service string |
|
| 28 |
+} |
|
| 29 |
+ |
|
| 30 |
+func newLogsCommand(dockerCli *command.DockerCli) *cobra.Command {
|
|
| 31 |
+ var opts logsOptions |
|
| 32 |
+ |
|
| 33 |
+ cmd := &cobra.Command{
|
|
| 34 |
+ Use: "logs [OPTIONS] SERVICE", |
|
| 35 |
+ Short: "Fetch the logs of a service", |
|
| 36 |
+ Args: cli.ExactArgs(1), |
|
| 37 |
+ RunE: func(cmd *cobra.Command, args []string) error {
|
|
| 38 |
+ opts.service = args[0] |
|
| 39 |
+ return runLogs(dockerCli, &opts) |
|
| 40 |
+ }, |
|
| 41 |
+ Tags: map[string]string{"experimental": ""},
|
|
| 42 |
+ } |
|
| 43 |
+ |
|
| 44 |
+ flags := cmd.Flags() |
|
| 45 |
+ flags.BoolVar(&opts.noResolve, "no-resolve", false, "Do not map IDs to Names") |
|
| 46 |
+ flags.BoolVarP(&opts.follow, "follow", "f", false, "Follow log output") |
|
| 47 |
+ flags.StringVar(&opts.since, "since", "", "Show logs since timestamp") |
|
| 48 |
+ flags.BoolVarP(&opts.timestamps, "timestamps", "t", false, "Show timestamps") |
|
| 49 |
+ flags.BoolVar(&opts.details, "details", false, "Show extra details provided to logs") |
|
| 50 |
+ flags.StringVar(&opts.tail, "tail", "all", "Number of lines to show from the end of the logs") |
|
| 51 |
+ return cmd |
|
| 52 |
+} |
|
| 53 |
+ |
|
| 54 |
+func runLogs(dockerCli *command.DockerCli, opts *logsOptions) error {
|
|
| 55 |
+ ctx := context.Background() |
|
| 56 |
+ |
|
| 57 |
+ options := types.ContainerLogsOptions{
|
|
| 58 |
+ ShowStdout: true, |
|
| 59 |
+ ShowStderr: true, |
|
| 60 |
+ Since: opts.since, |
|
| 61 |
+ Timestamps: opts.timestamps, |
|
| 62 |
+ Follow: opts.follow, |
|
| 63 |
+ Tail: opts.tail, |
|
| 64 |
+ Details: opts.details, |
|
| 65 |
+ } |
|
| 66 |
+ |
|
| 67 |
+ client := dockerCli.Client() |
|
| 68 |
+ responseBody, err := client.ServiceLogs(ctx, opts.service, options) |
|
| 69 |
+ if err != nil {
|
|
| 70 |
+ return err |
|
| 71 |
+ } |
|
| 72 |
+ defer responseBody.Close() |
|
| 73 |
+ |
|
| 74 |
+ resolver := idresolver.New(client, opts.noResolve) |
|
| 75 |
+ |
|
| 76 |
+ stdout := &logWriter{ctx: ctx, opts: opts, r: resolver, w: dockerCli.Out()}
|
|
| 77 |
+ stderr := &logWriter{ctx: ctx, opts: opts, r: resolver, w: dockerCli.Err()}
|
|
| 78 |
+ |
|
| 79 |
+ // TODO(aluzzardi): Do an io.Copy for services with TTY enabled. |
|
| 80 |
+ _, err = stdcopy.StdCopy(stdout, stderr, responseBody) |
|
| 81 |
+ return err |
|
| 82 |
+} |
|
| 83 |
+ |
|
| 84 |
+type logWriter struct {
|
|
| 85 |
+ ctx context.Context |
|
| 86 |
+ opts *logsOptions |
|
| 87 |
+ r *idresolver.IDResolver |
|
| 88 |
+ w io.Writer |
|
| 89 |
+} |
|
| 90 |
+ |
|
| 91 |
+func (lw *logWriter) Write(buf []byte) (int, error) {
|
|
| 92 |
+ contextIndex := 0 |
|
| 93 |
+ numParts := 2 |
|
| 94 |
+ if lw.opts.timestamps {
|
|
| 95 |
+ contextIndex++ |
|
| 96 |
+ numParts++ |
|
| 97 |
+ } |
|
| 98 |
+ |
|
| 99 |
+ parts := bytes.SplitN(buf, []byte(" "), numParts)
|
|
| 100 |
+ if len(parts) != numParts {
|
|
| 101 |
+ return 0, fmt.Errorf("invalid context in log message: %v", string(buf))
|
|
| 102 |
+ } |
|
| 103 |
+ |
|
| 104 |
+ taskName, nodeName, err := lw.parseContext(string(parts[contextIndex])) |
|
| 105 |
+ if err != nil {
|
|
| 106 |
+ return 0, err |
|
| 107 |
+ } |
|
| 108 |
+ |
|
| 109 |
+ output := []byte{}
|
|
| 110 |
+ for i, part := range parts {
|
|
| 111 |
+ // First part doesn't get space separation. |
|
| 112 |
+ if i > 0 {
|
|
| 113 |
+ output = append(output, []byte(" ")...)
|
|
| 114 |
+ } |
|
| 115 |
+ |
|
| 116 |
+ if i == contextIndex {
|
|
| 117 |
+ // TODO(aluzzardi): Consider constant padding. |
|
| 118 |
+ output = append(output, []byte(fmt.Sprintf("%s@%s |", taskName, nodeName))...)
|
|
| 119 |
+ } else {
|
|
| 120 |
+ output = append(output, part...) |
|
| 121 |
+ } |
|
| 122 |
+ } |
|
| 123 |
+ _, err = lw.w.Write(output) |
|
| 124 |
+ if err != nil {
|
|
| 125 |
+ return 0, err |
|
| 126 |
+ } |
|
| 127 |
+ |
|
| 128 |
+ return len(buf), nil |
|
| 129 |
+} |
|
| 130 |
+ |
|
| 131 |
+func (lw *logWriter) parseContext(input string) (string, string, error) {
|
|
| 132 |
+ context := make(map[string]string) |
|
| 133 |
+ |
|
| 134 |
+ components := strings.Split(input, ",") |
|
| 135 |
+ for _, component := range components {
|
|
| 136 |
+ parts := strings.SplitN(component, "=", 2) |
|
| 137 |
+ if len(parts) != 2 {
|
|
| 138 |
+ return "", "", fmt.Errorf("invalid context: %s", input)
|
|
| 139 |
+ } |
|
| 140 |
+ context[parts[0]] = parts[1] |
|
| 141 |
+ } |
|
| 142 |
+ |
|
| 143 |
+ taskID, ok := context["com.docker.swarm.task.id"] |
|
| 144 |
+ if !ok {
|
|
| 145 |
+ return "", "", fmt.Errorf("missing task id in context: %s", input)
|
|
| 146 |
+ } |
|
| 147 |
+ taskName, err := lw.r.Resolve(lw.ctx, swarm.Task{}, taskID)
|
|
| 148 |
+ if err != nil {
|
|
| 149 |
+ return "", "", err |
|
| 150 |
+ } |
|
| 151 |
+ |
|
| 152 |
+ nodeID, ok := context["com.docker.swarm.node.id"] |
|
| 153 |
+ if !ok {
|
|
| 154 |
+ return "", "", fmt.Errorf("missing node id in context: %s", input)
|
|
| 155 |
+ } |
|
| 156 |
+ nodeName, err := lw.r.Resolve(lw.ctx, swarm.Node{}, nodeID)
|
|
| 157 |
+ if err != nil {
|
|
| 158 |
+ return "", "", err |
|
| 159 |
+ } |
|
| 160 |
+ |
|
| 161 |
+ return taskName, nodeName, nil |
|
| 162 |
+} |
| 0 | 163 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,55 @@ |
| 0 |
+// +build !windows |
|
| 1 |
+ |
|
| 2 |
+package main |
|
| 3 |
+ |
|
| 4 |
+import ( |
|
| 5 |
+ "bufio" |
|
| 6 |
+ "io" |
|
| 7 |
+ "os/exec" |
|
| 8 |
+ "strings" |
|
| 9 |
+ |
|
| 10 |
+ "github.com/docker/docker/pkg/integration/checker" |
|
| 11 |
+ "github.com/go-check/check" |
|
| 12 |
+) |
|
| 13 |
+ |
|
| 14 |
+type logMessage struct {
|
|
| 15 |
+ err error |
|
| 16 |
+ data []byte |
|
| 17 |
+} |
|
| 18 |
+ |
|
| 19 |
+func (s *DockerSwarmSuite) TestServiceLogs(c *check.C) {
|
|
| 20 |
+ testRequires(c, ExperimentalDaemon) |
|
| 21 |
+ |
|
| 22 |
+ d := s.AddDaemon(c, true, true) |
|
| 23 |
+ |
|
| 24 |
+ name := "TestServiceLogs" |
|
| 25 |
+ |
|
| 26 |
+ out, err := d.Cmd("service", "create", "--name", name, "busybox", "sh", "-c", "while true; do echo log test; sleep 1; done")
|
|
| 27 |
+ c.Assert(err, checker.IsNil) |
|
| 28 |
+ c.Assert(strings.TrimSpace(out), checker.Not(checker.Equals), "") |
|
| 29 |
+ |
|
| 30 |
+ // make sure task has been deployed. |
|
| 31 |
+ waitAndAssert(c, defaultReconciliationTimeout, d.checkActiveContainerCount, checker.Equals, 1) |
|
| 32 |
+ |
|
| 33 |
+ args := []string{"service", "logs", "-f", name}
|
|
| 34 |
+ cmd := exec.Command(dockerBinary, d.prependHostArg(args)...) |
|
| 35 |
+ r, w := io.Pipe() |
|
| 36 |
+ cmd.Stdout = w |
|
| 37 |
+ cmd.Stderr = w |
|
| 38 |
+ c.Assert(cmd.Start(), checker.IsNil) |
|
| 39 |
+ |
|
| 40 |
+ // Make sure pipe is written to |
|
| 41 |
+ ch := make(chan *logMessage) |
|
| 42 |
+ go func() {
|
|
| 43 |
+ reader := bufio.NewReader(r) |
|
| 44 |
+ msg := &logMessage{}
|
|
| 45 |
+ msg.data, _, msg.err = reader.ReadLine() |
|
| 46 |
+ ch <- msg |
|
| 47 |
+ }() |
|
| 48 |
+ |
|
| 49 |
+ msg := <-ch |
|
| 50 |
+ c.Assert(msg.err, checker.IsNil) |
|
| 51 |
+ c.Assert(string(msg.data), checker.Contains, "log test") |
|
| 52 |
+ |
|
| 53 |
+ c.Assert(cmd.Process.Kill(), checker.IsNil) |
|
| 54 |
+} |