github.1git.de/docker/cli@v26.1.3+incompatible/cli/command/service/logs.go (about) 1 package service 2 3 import ( 4 "bytes" 5 "context" 6 "fmt" 7 "io" 8 "sort" 9 "strconv" 10 "strings" 11 12 "github.com/docker/cli/cli" 13 "github.com/docker/cli/cli/command" 14 "github.com/docker/cli/cli/command/idresolver" 15 "github.com/docker/cli/service/logs" 16 "github.com/docker/docker/api/types" 17 "github.com/docker/docker/api/types/container" 18 "github.com/docker/docker/api/types/swarm" 19 "github.com/docker/docker/client" 20 "github.com/docker/docker/errdefs" 21 "github.com/docker/docker/pkg/stdcopy" 22 "github.com/docker/docker/pkg/stringid" 23 "github.com/pkg/errors" 24 "github.com/spf13/cobra" 25 ) 26 27 type logsOptions struct { 28 noResolve bool 29 noTrunc bool 30 noTaskIDs bool 31 follow bool 32 since string 33 timestamps bool 34 tail string 35 details bool 36 raw bool 37 38 target string 39 } 40 41 func newLogsCommand(dockerCli command.Cli) *cobra.Command { 42 var opts logsOptions 43 44 cmd := &cobra.Command{ 45 Use: "logs [OPTIONS] SERVICE|TASK", 46 Short: "Fetch the logs of a service or task", 47 Args: cli.ExactArgs(1), 48 RunE: func(cmd *cobra.Command, args []string) error { 49 opts.target = args[0] 50 return runLogs(cmd.Context(), dockerCli, &opts) 51 }, 52 Annotations: map[string]string{"version": "1.29"}, 53 ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { 54 return CompletionFn(dockerCli)(cmd, args, toComplete) 55 }, 56 } 57 58 flags := cmd.Flags() 59 // options specific to service logs 60 flags.BoolVar(&opts.noResolve, "no-resolve", false, "Do not map IDs to Names in output") 61 flags.BoolVar(&opts.noTrunc, "no-trunc", false, "Do not truncate output") 62 flags.BoolVar(&opts.raw, "raw", false, "Do not neatly format logs") 63 flags.SetAnnotation("raw", "version", []string{"1.30"}) 64 flags.BoolVar(&opts.noTaskIDs, "no-task-ids", false, "Do not include task IDs in output") 65 // options identical to container logs 66 flags.BoolVarP(&opts.follow, "follow", "f", false, "Follow log output") 67 flags.StringVar(&opts.since, "since", "", `Show logs since timestamp (e.g. "2013-01-02T13:23:37Z") or relative (e.g. "42m" for 42 minutes)`) 68 flags.BoolVarP(&opts.timestamps, "timestamps", "t", false, "Show timestamps") 69 flags.BoolVar(&opts.details, "details", false, "Show extra details provided to logs") 70 flags.SetAnnotation("details", "version", []string{"1.30"}) 71 flags.StringVarP(&opts.tail, "tail", "n", "all", "Number of lines to show from the end of the logs") 72 return cmd 73 } 74 75 func runLogs(ctx context.Context, dockerCli command.Cli, opts *logsOptions) error { 76 apiClient := dockerCli.Client() 77 78 var ( 79 maxLength = 1 80 responseBody io.ReadCloser 81 tty bool 82 // logfunc is used to delay the call to logs so that we can do some 83 // processing before we actually get the logs 84 logfunc func(context.Context, string, container.LogsOptions) (io.ReadCloser, error) 85 ) 86 87 service, _, err := apiClient.ServiceInspectWithRaw(ctx, opts.target, types.ServiceInspectOptions{}) 88 if err != nil { 89 // if it's any error other than service not found, it's Real 90 if !errdefs.IsNotFound(err) { 91 return err 92 } 93 task, _, err := apiClient.TaskInspectWithRaw(ctx, opts.target) 94 if err != nil { 95 if errdefs.IsNotFound(err) { 96 // if the task isn't found, rewrite the error to be clear 97 // that we looked for services AND tasks and found none 98 err = fmt.Errorf("no such task or service: %v", opts.target) 99 } 100 return err 101 } 102 103 tty = task.Spec.ContainerSpec.TTY 104 maxLength = getMaxLength(task.Slot) 105 106 // use the TaskLogs api function 107 logfunc = apiClient.TaskLogs 108 } else { 109 // use ServiceLogs api function 110 logfunc = apiClient.ServiceLogs 111 tty = service.Spec.TaskTemplate.ContainerSpec.TTY 112 if service.Spec.Mode.Replicated != nil && service.Spec.Mode.Replicated.Replicas != nil { 113 // if replicas are initialized, figure out if we need to pad them 114 replicas := *service.Spec.Mode.Replicated.Replicas 115 maxLength = getMaxLength(int(replicas)) 116 } 117 } 118 119 // we can't prettify tty logs. tell the user that this is the case. 120 // this is why we assign the logs function to a variable and delay calling 121 // it. we want to check this before we make the call and checking twice in 122 // each branch is even sloppier than this CLI disaster already is 123 if tty && !opts.raw { 124 return errors.New("tty service logs only supported with --raw") 125 } 126 127 // now get the logs 128 responseBody, err = logfunc(ctx, opts.target, container.LogsOptions{ 129 ShowStdout: true, 130 ShowStderr: true, 131 Since: opts.since, 132 Timestamps: opts.timestamps, 133 Follow: opts.follow, 134 Tail: opts.tail, 135 // get the details if we request it OR if we're not doing raw mode 136 // (we need them for the context to pretty print) 137 Details: opts.details || !opts.raw, 138 }) 139 if err != nil { 140 return err 141 } 142 defer responseBody.Close() 143 144 // tty logs get straight copied. they're not muxed with stdcopy 145 if tty { 146 _, err = io.Copy(dockerCli.Out(), responseBody) 147 return err 148 } 149 150 // otherwise, logs are multiplexed. if we're doing pretty printing, also 151 // create a task formatter. 152 var stdout, stderr io.Writer 153 stdout = dockerCli.Out() 154 stderr = dockerCli.Err() 155 if !opts.raw { 156 taskFormatter := newTaskFormatter(apiClient, opts, maxLength) 157 158 stdout = &logWriter{ctx: ctx, opts: opts, f: taskFormatter, w: stdout} 159 stderr = &logWriter{ctx: ctx, opts: opts, f: taskFormatter, w: stderr} 160 } 161 162 _, err = stdcopy.StdCopy(stdout, stderr, responseBody) 163 return err 164 } 165 166 // getMaxLength gets the maximum length of the number in base 10 167 func getMaxLength(i int) int { 168 return len(strconv.Itoa(i)) 169 } 170 171 type taskFormatter struct { 172 client client.APIClient 173 opts *logsOptions 174 padding int 175 176 r *idresolver.IDResolver 177 // cache saves a pre-cooked logContext formatted string based on a 178 // logcontext object, so we don't have to resolve names every time 179 cache map[logContext]string 180 } 181 182 func newTaskFormatter(apiClient client.APIClient, opts *logsOptions, padding int) *taskFormatter { 183 return &taskFormatter{ 184 client: apiClient, 185 opts: opts, 186 padding: padding, 187 r: idresolver.New(apiClient, opts.noResolve), 188 cache: make(map[logContext]string), 189 } 190 } 191 192 func (f *taskFormatter) format(ctx context.Context, logCtx logContext) (string, error) { 193 if cached, ok := f.cache[logCtx]; ok { 194 return cached, nil 195 } 196 197 nodeName, err := f.r.Resolve(ctx, swarm.Node{}, logCtx.nodeID) 198 if err != nil { 199 return "", err 200 } 201 202 serviceName, err := f.r.Resolve(ctx, swarm.Service{}, logCtx.serviceID) 203 if err != nil { 204 return "", err 205 } 206 207 task, _, err := f.client.TaskInspectWithRaw(ctx, logCtx.taskID) 208 if err != nil { 209 return "", err 210 } 211 212 taskName := fmt.Sprintf("%s.%d", serviceName, task.Slot) 213 if !f.opts.noTaskIDs { 214 if f.opts.noTrunc { 215 taskName += fmt.Sprintf(".%s", task.ID) 216 } else { 217 taskName += fmt.Sprintf(".%s", stringid.TruncateID(task.ID)) 218 } 219 } 220 221 paddingCount := f.padding - getMaxLength(task.Slot) 222 padding := "" 223 if paddingCount > 0 { 224 padding = strings.Repeat(" ", paddingCount) 225 } 226 formatted := taskName + "@" + nodeName + padding 227 f.cache[logCtx] = formatted 228 return formatted, nil 229 } 230 231 type logWriter struct { 232 ctx context.Context 233 opts *logsOptions 234 f *taskFormatter 235 w io.Writer 236 } 237 238 func (lw *logWriter) Write(buf []byte) (int, error) { 239 // this works but ONLY because stdcopy calls write a whole line at a time. 240 // if this ends up horribly broken or panics, check to see if stdcopy has 241 // reneged on that assumption. (@god forgive me) 242 // also this only works because the logs format is, like, barely parsable. 243 // if something changes in the logs format, this is gonna break 244 245 // there should always be at least 2 parts: details and message. if there 246 // is no timestamp, details will be first (index 0) when we split on 247 // spaces. if there is a timestamp, details will be 2nd (`index 1) 248 detailsIndex := 0 249 numParts := 2 250 if lw.opts.timestamps { 251 detailsIndex++ 252 numParts++ 253 } 254 255 // break up the log line into parts. 256 parts := bytes.SplitN(buf, []byte(" "), numParts) 257 if len(parts) != numParts { 258 return 0, errors.Errorf("invalid context in log message: %v", string(buf)) 259 } 260 // parse the details out 261 details, err := logs.ParseLogDetails(string(parts[detailsIndex])) 262 if err != nil { 263 return 0, err 264 } 265 // and then create a context from the details 266 // this removes the context-specific details from the details map, so we 267 // can more easily print the details later 268 logCtx, err := lw.parseContext(details) 269 if err != nil { 270 return 0, err 271 } 272 273 output := []byte{} 274 // if we included timestamps, add them to the front 275 if lw.opts.timestamps { 276 output = append(output, parts[0]...) 277 output = append(output, ' ') 278 } 279 // add the context, nice and formatted 280 formatted, err := lw.f.format(lw.ctx, logCtx) 281 if err != nil { 282 return 0, err 283 } 284 output = append(output, []byte(formatted+" | ")...) 285 // if the user asked for details, add them to be log message 286 if lw.opts.details { 287 // ugh i hate this it's basically a dupe of api/server/httputils/write_log_stream.go:stringAttrs() 288 // ok but we're gonna do it a bit different 289 290 // there are optimizations that can be made here. for starters, i'd 291 // suggest caching the details keys. then, we can maybe draw maps and 292 // slices from a pool to avoid alloc overhead on them. idk if it's 293 // worth the time yet. 294 295 // first we need a slice 296 d := make([]string, 0, len(details)) 297 // then let's add all the pairs 298 for k := range details { 299 d = append(d, k+"="+details[k]) 300 } 301 // then sort em 302 sort.Strings(d) 303 // then join and append 304 output = append(output, []byte(strings.Join(d, ","))...) 305 output = append(output, ' ') 306 } 307 308 // add the log message itself, finally 309 output = append(output, parts[detailsIndex+1]...) 310 311 _, err = lw.w.Write(output) 312 if err != nil { 313 return 0, err 314 } 315 316 return len(buf), nil 317 } 318 319 // parseContext returns a log context and REMOVES the context from the details map 320 func (lw *logWriter) parseContext(details map[string]string) (logContext, error) { 321 nodeID, ok := details["com.docker.swarm.node.id"] 322 if !ok { 323 return logContext{}, errors.Errorf("missing node id in details: %v", details) 324 } 325 delete(details, "com.docker.swarm.node.id") 326 327 serviceID, ok := details["com.docker.swarm.service.id"] 328 if !ok { 329 return logContext{}, errors.Errorf("missing service id in details: %v", details) 330 } 331 delete(details, "com.docker.swarm.service.id") 332 333 taskID, ok := details["com.docker.swarm.task.id"] 334 if !ok { 335 return logContext{}, errors.Errorf("missing task id in details: %s", details) 336 } 337 delete(details, "com.docker.swarm.task.id") 338 339 return logContext{ 340 nodeID: nodeID, 341 serviceID: serviceID, 342 taskID: taskID, 343 }, nil 344 } 345 346 type logContext struct { 347 nodeID string 348 serviceID string 349 taskID string 350 }