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