logs.go 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294
  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/pkg/errors"
  18. "github.com/spf13/cobra"
  19. )
  20. type logsOptions struct {
  21. noResolve bool
  22. noTrunc bool
  23. noTaskIDs bool
  24. follow bool
  25. since string
  26. timestamps bool
  27. tail string
  28. target string
  29. }
  30. // TODO(dperny) the whole CLI for this is kind of a mess IMHOIRL and it needs
  31. // to be refactored agressively. There may be changes to the implementation of
  32. // details, which will be need to be reflected in this code. The refactoring
  33. // should be put off until we make those changes, tho, because I think the
  34. // decisions made WRT details will impact the design of the CLI.
  35. func newLogsCommand(dockerCli *command.DockerCli) *cobra.Command {
  36. var opts logsOptions
  37. cmd := &cobra.Command{
  38. Use: "logs [OPTIONS] SERVICE",
  39. Short: "Fetch the logs of a service",
  40. Args: cli.ExactArgs(1),
  41. RunE: func(cmd *cobra.Command, args []string) error {
  42. opts.target = args[0]
  43. return runLogs(dockerCli, &opts)
  44. },
  45. Tags: map[string]string{"experimental": ""},
  46. }
  47. flags := cmd.Flags()
  48. flags.BoolVar(&opts.noResolve, "no-resolve", false, "Do not map IDs to Names in output")
  49. flags.BoolVar(&opts.noTrunc, "no-trunc", false, "Do not truncate output")
  50. flags.BoolVar(&opts.noTaskIDs, "no-task-ids", false, "Do not include task IDs in output")
  51. flags.BoolVarP(&opts.follow, "follow", "f", false, "Follow log output")
  52. flags.StringVar(&opts.since, "since", "", "Show logs since timestamp (e.g. 2013-01-02T13:23:37) or relative (e.g. 42m for 42 minutes)")
  53. flags.BoolVarP(&opts.timestamps, "timestamps", "t", false, "Show timestamps")
  54. flags.StringVar(&opts.tail, "tail", "all", "Number of lines to show from the end of the logs")
  55. return cmd
  56. }
  57. func runLogs(dockerCli *command.DockerCli, opts *logsOptions) error {
  58. ctx := context.Background()
  59. options := types.ContainerLogsOptions{
  60. ShowStdout: true,
  61. ShowStderr: true,
  62. Since: opts.since,
  63. Timestamps: opts.timestamps,
  64. Follow: opts.follow,
  65. Tail: opts.tail,
  66. Details: true,
  67. }
  68. cli := dockerCli.Client()
  69. var (
  70. maxLength = 1
  71. responseBody io.ReadCloser
  72. tty bool
  73. )
  74. service, _, err := cli.ServiceInspectWithRaw(ctx, opts.target)
  75. if err != nil {
  76. // if it's any error other than service not found, it's Real
  77. if !client.IsErrServiceNotFound(err) {
  78. return err
  79. }
  80. task, _, err := cli.TaskInspectWithRaw(ctx, opts.target)
  81. tty = task.Spec.ContainerSpec.TTY
  82. // TODO(dperny) hot fix until we get a nice details system squared away,
  83. // ignores details (including task context) if we have a TTY log
  84. if tty {
  85. options.Details = false
  86. }
  87. responseBody, err = cli.TaskLogs(ctx, opts.target, options)
  88. if err != nil {
  89. if client.IsErrTaskNotFound(err) {
  90. // if the task ALSO isn't found, rewrite the error to be clear
  91. // that we looked for services AND tasks
  92. err = fmt.Errorf("No such task or service")
  93. }
  94. return err
  95. }
  96. maxLength = getMaxLength(task.Slot)
  97. responseBody, err = cli.TaskLogs(ctx, opts.target, options)
  98. } else {
  99. tty = service.Spec.TaskTemplate.ContainerSpec.TTY
  100. // TODO(dperny) hot fix until we get a nice details system squared away,
  101. // ignores details (including task context) if we have a TTY log
  102. if tty {
  103. options.Details = false
  104. }
  105. responseBody, err = cli.ServiceLogs(ctx, opts.target, options)
  106. if err != nil {
  107. return err
  108. }
  109. if service.Spec.Mode.Replicated != nil && service.Spec.Mode.Replicated.Replicas != nil {
  110. // if replicas are initialized, figure out if we need to pad them
  111. replicas := *service.Spec.Mode.Replicated.Replicas
  112. maxLength = getMaxLength(int(replicas))
  113. }
  114. }
  115. defer responseBody.Close()
  116. if tty {
  117. _, err = io.Copy(dockerCli.Out(), responseBody)
  118. return err
  119. }
  120. taskFormatter := newTaskFormatter(cli, opts, maxLength)
  121. stdout := &logWriter{ctx: ctx, opts: opts, f: taskFormatter, w: dockerCli.Out()}
  122. stderr := &logWriter{ctx: ctx, opts: opts, f: taskFormatter, w: dockerCli.Err()}
  123. // TODO(aluzzardi): Do an io.Copy for services with TTY enabled.
  124. _, err = stdcopy.StdCopy(stdout, stderr, responseBody)
  125. return err
  126. }
  127. // getMaxLength gets the maximum length of the number in base 10
  128. func getMaxLength(i int) int {
  129. return len(strconv.FormatInt(int64(i), 10))
  130. }
  131. type taskFormatter struct {
  132. client client.APIClient
  133. opts *logsOptions
  134. padding int
  135. r *idresolver.IDResolver
  136. cache map[logContext]string
  137. }
  138. func newTaskFormatter(client client.APIClient, opts *logsOptions, padding int) *taskFormatter {
  139. return &taskFormatter{
  140. client: client,
  141. opts: opts,
  142. padding: padding,
  143. r: idresolver.New(client, opts.noResolve),
  144. cache: make(map[logContext]string),
  145. }
  146. }
  147. func (f *taskFormatter) format(ctx context.Context, logCtx logContext) (string, error) {
  148. if cached, ok := f.cache[logCtx]; ok {
  149. return cached, nil
  150. }
  151. nodeName, err := f.r.Resolve(ctx, swarm.Node{}, logCtx.nodeID)
  152. if err != nil {
  153. return "", err
  154. }
  155. serviceName, err := f.r.Resolve(ctx, swarm.Service{}, logCtx.serviceID)
  156. if err != nil {
  157. return "", err
  158. }
  159. task, _, err := f.client.TaskInspectWithRaw(ctx, logCtx.taskID)
  160. if err != nil {
  161. return "", err
  162. }
  163. taskName := fmt.Sprintf("%s.%d", serviceName, task.Slot)
  164. if !f.opts.noTaskIDs {
  165. if f.opts.noTrunc {
  166. taskName += fmt.Sprintf(".%s", task.ID)
  167. } else {
  168. taskName += fmt.Sprintf(".%s", stringid.TruncateID(task.ID))
  169. }
  170. }
  171. padding := strings.Repeat(" ", f.padding-getMaxLength(task.Slot))
  172. formatted := fmt.Sprintf("%s@%s%s", taskName, nodeName, padding)
  173. f.cache[logCtx] = formatted
  174. return formatted, nil
  175. }
  176. type logWriter struct {
  177. ctx context.Context
  178. opts *logsOptions
  179. f *taskFormatter
  180. w io.Writer
  181. }
  182. func (lw *logWriter) Write(buf []byte) (int, error) {
  183. contextIndex := 0
  184. numParts := 2
  185. if lw.opts.timestamps {
  186. contextIndex++
  187. numParts++
  188. }
  189. parts := bytes.SplitN(buf, []byte(" "), numParts)
  190. if len(parts) != numParts {
  191. return 0, errors.Errorf("invalid context in log message: %v", string(buf))
  192. }
  193. logCtx, err := lw.parseContext(string(parts[contextIndex]))
  194. if err != nil {
  195. return 0, err
  196. }
  197. output := []byte{}
  198. for i, part := range parts {
  199. // First part doesn't get space separation.
  200. if i > 0 {
  201. output = append(output, []byte(" ")...)
  202. }
  203. if i == contextIndex {
  204. formatted, err := lw.f.format(lw.ctx, logCtx)
  205. if err != nil {
  206. return 0, err
  207. }
  208. output = append(output, []byte(fmt.Sprintf("%s |", formatted))...)
  209. } else {
  210. output = append(output, part...)
  211. }
  212. }
  213. _, err = lw.w.Write(output)
  214. if err != nil {
  215. return 0, err
  216. }
  217. return len(buf), nil
  218. }
  219. func (lw *logWriter) parseContext(input string) (logContext, error) {
  220. context := make(map[string]string)
  221. components := strings.Split(input, ",")
  222. for _, component := range components {
  223. parts := strings.SplitN(component, "=", 2)
  224. if len(parts) != 2 {
  225. return logContext{}, errors.Errorf("invalid context: %s", input)
  226. }
  227. context[parts[0]] = parts[1]
  228. }
  229. nodeID, ok := context["com.docker.swarm.node.id"]
  230. if !ok {
  231. return logContext{}, errors.Errorf("missing node id in context: %s", input)
  232. }
  233. serviceID, ok := context["com.docker.swarm.service.id"]
  234. if !ok {
  235. return logContext{}, errors.Errorf("missing service id in context: %s", input)
  236. }
  237. taskID, ok := context["com.docker.swarm.task.id"]
  238. if !ok {
  239. return logContext{}, errors.Errorf("missing task id in context: %s", input)
  240. }
  241. return logContext{
  242. nodeID: nodeID,
  243. serviceID: serviceID,
  244. taskID: taskID,
  245. }, nil
  246. }
  247. type logContext struct {
  248. nodeID string
  249. serviceID string
  250. taskID string
  251. }