logs.go 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245
  1. package service
  2. import (
  3. "bytes"
  4. "fmt"
  5. "io"
  6. "strconv"
  7. "strings"
  8. "golang.org/x/net/context"
  9. "github.com/docker/docker/api/types"
  10. "github.com/docker/docker/api/types/swarm"
  11. "github.com/docker/docker/cli"
  12. "github.com/docker/docker/cli/command"
  13. "github.com/docker/docker/cli/command/idresolver"
  14. "github.com/docker/docker/client"
  15. "github.com/docker/docker/pkg/stdcopy"
  16. "github.com/docker/docker/pkg/stringid"
  17. "github.com/spf13/cobra"
  18. )
  19. type logsOptions struct {
  20. noResolve bool
  21. noTrunc bool
  22. noIDs bool
  23. follow bool
  24. since string
  25. timestamps bool
  26. details bool
  27. tail string
  28. service string
  29. }
  30. func newLogsCommand(dockerCli *command.DockerCli) *cobra.Command {
  31. var opts logsOptions
  32. cmd := &cobra.Command{
  33. Use: "logs [OPTIONS] SERVICE",
  34. Short: "Fetch the logs of a service",
  35. Args: cli.ExactArgs(1),
  36. RunE: func(cmd *cobra.Command, args []string) error {
  37. opts.service = args[0]
  38. return runLogs(dockerCli, &opts)
  39. },
  40. Tags: map[string]string{"experimental": ""},
  41. }
  42. flags := cmd.Flags()
  43. flags.BoolVar(&opts.noResolve, "no-resolve", false, "Do not map IDs to Names")
  44. flags.BoolVar(&opts.noTrunc, "no-trunc", false, "Do not truncate output")
  45. flags.BoolVar(&opts.noIDs, "no-ids", false, "Do not include task IDs")
  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. func runLogs(dockerCli *command.DockerCli, opts *logsOptions) error {
  54. ctx := context.Background()
  55. options := types.ContainerLogsOptions{
  56. ShowStdout: true,
  57. ShowStderr: true,
  58. Since: opts.since,
  59. Timestamps: opts.timestamps,
  60. Follow: opts.follow,
  61. Tail: opts.tail,
  62. Details: opts.details,
  63. }
  64. client := dockerCli.Client()
  65. service, _, err := client.ServiceInspectWithRaw(ctx, opts.service)
  66. if err != nil {
  67. return err
  68. }
  69. responseBody, err := client.ServiceLogs(ctx, opts.service, options)
  70. if err != nil {
  71. return err
  72. }
  73. defer responseBody.Close()
  74. var replicas uint64
  75. if service.Spec.Mode.Replicated != nil && service.Spec.Mode.Replicated.Replicas != nil {
  76. replicas = *service.Spec.Mode.Replicated.Replicas
  77. }
  78. padding := len(strconv.FormatUint(replicas, 10))
  79. taskFormatter := newTaskFormatter(client, opts, padding)
  80. stdout := &logWriter{ctx: ctx, opts: opts, f: taskFormatter, w: dockerCli.Out()}
  81. stderr := &logWriter{ctx: ctx, opts: opts, f: taskFormatter, w: dockerCli.Err()}
  82. // TODO(aluzzardi): Do an io.Copy for services with TTY enabled.
  83. _, err = stdcopy.StdCopy(stdout, stderr, responseBody)
  84. return err
  85. }
  86. type taskFormatter struct {
  87. client client.APIClient
  88. opts *logsOptions
  89. padding int
  90. r *idresolver.IDResolver
  91. cache map[logContext]string
  92. }
  93. func newTaskFormatter(client client.APIClient, opts *logsOptions, padding int) *taskFormatter {
  94. return &taskFormatter{
  95. client: client,
  96. opts: opts,
  97. padding: padding,
  98. r: idresolver.New(client, opts.noResolve),
  99. cache: make(map[logContext]string),
  100. }
  101. }
  102. func (f *taskFormatter) format(ctx context.Context, logCtx logContext) (string, error) {
  103. if cached, ok := f.cache[logCtx]; ok {
  104. return cached, nil
  105. }
  106. nodeName, err := f.r.Resolve(ctx, swarm.Node{}, logCtx.nodeID)
  107. if err != nil {
  108. return "", err
  109. }
  110. serviceName, err := f.r.Resolve(ctx, swarm.Service{}, logCtx.serviceID)
  111. if err != nil {
  112. return "", err
  113. }
  114. task, _, err := f.client.TaskInspectWithRaw(ctx, logCtx.taskID)
  115. if err != nil {
  116. return "", err
  117. }
  118. taskName := fmt.Sprintf("%s.%d", serviceName, task.Slot)
  119. if !f.opts.noIDs {
  120. if f.opts.noTrunc {
  121. taskName += fmt.Sprintf(".%s", task.ID)
  122. } else {
  123. taskName += fmt.Sprintf(".%s", stringid.TruncateID(task.ID))
  124. }
  125. }
  126. padding := strings.Repeat(" ", f.padding-len(strconv.FormatInt(int64(task.Slot), 10)))
  127. formatted := fmt.Sprintf("%s@%s%s", taskName, nodeName, padding)
  128. f.cache[logCtx] = formatted
  129. return formatted, nil
  130. }
  131. type logWriter struct {
  132. ctx context.Context
  133. opts *logsOptions
  134. f *taskFormatter
  135. w io.Writer
  136. }
  137. func (lw *logWriter) Write(buf []byte) (int, error) {
  138. contextIndex := 0
  139. numParts := 2
  140. if lw.opts.timestamps {
  141. contextIndex++
  142. numParts++
  143. }
  144. parts := bytes.SplitN(buf, []byte(" "), numParts)
  145. if len(parts) != numParts {
  146. return 0, fmt.Errorf("invalid context in log message: %v", string(buf))
  147. }
  148. logCtx, err := lw.parseContext(string(parts[contextIndex]))
  149. if err != nil {
  150. return 0, err
  151. }
  152. output := []byte{}
  153. for i, part := range parts {
  154. // First part doesn't get space separation.
  155. if i > 0 {
  156. output = append(output, []byte(" ")...)
  157. }
  158. if i == contextIndex {
  159. formatted, err := lw.f.format(lw.ctx, logCtx)
  160. if err != nil {
  161. return 0, err
  162. }
  163. output = append(output, []byte(fmt.Sprintf("%s |", formatted))...)
  164. } else {
  165. output = append(output, part...)
  166. }
  167. }
  168. _, err = lw.w.Write(output)
  169. if err != nil {
  170. return 0, err
  171. }
  172. return len(buf), nil
  173. }
  174. func (lw *logWriter) parseContext(input string) (logContext, error) {
  175. context := make(map[string]string)
  176. components := strings.Split(input, ",")
  177. for _, component := range components {
  178. parts := strings.SplitN(component, "=", 2)
  179. if len(parts) != 2 {
  180. return logContext{}, fmt.Errorf("invalid context: %s", input)
  181. }
  182. context[parts[0]] = parts[1]
  183. }
  184. nodeID, ok := context["com.docker.swarm.node.id"]
  185. if !ok {
  186. return logContext{}, fmt.Errorf("missing node id in context: %s", input)
  187. }
  188. serviceID, ok := context["com.docker.swarm.service.id"]
  189. if !ok {
  190. return logContext{}, fmt.Errorf("missing service id in context: %s", input)
  191. }
  192. taskID, ok := context["com.docker.swarm.task.id"]
  193. if !ok {
  194. return logContext{}, fmt.Errorf("missing task id in context: %s", input)
  195. }
  196. return logContext{
  197. nodeID: nodeID,
  198. serviceID: serviceID,
  199. taskID: taskID,
  200. }, nil
  201. }
  202. type logContext struct {
  203. nodeID string
  204. serviceID string
  205. taskID string
  206. }