go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/logdog/client/cli/subcommandCat.go (about) 1 // Copyright 2015 The LUCI Authors. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package cli 16 17 import ( 18 "context" 19 "io" 20 "os" 21 "strconv" 22 "strings" 23 "time" 24 25 "go.chromium.org/luci/common/errors" 26 "go.chromium.org/luci/common/flag/flagenum" 27 log "go.chromium.org/luci/common/logging" 28 "go.chromium.org/luci/logdog/api/logpb" 29 "go.chromium.org/luci/logdog/client/coordinator" 30 "go.chromium.org/luci/logdog/common/fetcher" 31 "go.chromium.org/luci/logdog/common/renderer" 32 "go.chromium.org/luci/logdog/common/types" 33 annopb "go.chromium.org/luci/luciexe/legacy/annotee/proto" 34 35 "github.com/golang/protobuf/proto" 36 "github.com/maruel/subcommands" 37 ) 38 39 type timestampsFlag string 40 41 const ( 42 timestampsOff timestampsFlag = "" 43 timestampsLocal timestampsFlag = "local" 44 timestampsUTC timestampsFlag = "utc" 45 ) 46 47 func (t *timestampsFlag) Set(v string) error { return timestampFlagEnum.FlagSet(t, v) } 48 func (t *timestampsFlag) String() string { return timestampFlagEnum.FlagString(t) } 49 50 var timestampFlagEnum = flagenum.Enum{ 51 "": timestampsOff, 52 "local": timestampsLocal, 53 "utc": timestampsUTC, 54 } 55 56 type catCommandRun struct { 57 subcommands.CommandRunBase 58 59 index int64 60 count int64 61 buffer int 62 fetchSize int 63 fetchBytes int 64 raw bool 65 66 timestamps timestampsFlag 67 showStreamIndex bool 68 } 69 70 func newCatCommand() *subcommands.Command { 71 return &subcommands.Command{ 72 UsageLine: "cat", 73 ShortDesc: "Write log stream to STDOUT.", 74 CommandRun: func() subcommands.CommandRun { 75 cmd := &catCommandRun{} 76 77 cmd.Flags.Int64Var(&cmd.index, "index", 0, "Starting index.") 78 cmd.Flags.Int64Var(&cmd.count, "count", 0, "The number of log entries to fetch.") 79 cmd.Flags.Var(&cmd.timestamps, "timestamps", 80 "When rendering text logs, prefix them with their timestamps. Options are: "+timestampFlagEnum.Choices()) 81 cmd.Flags.BoolVar(&cmd.showStreamIndex, "show-stream-index", false, 82 "When rendering text logs, show their stream index.") 83 cmd.Flags.IntVar(&cmd.buffer, "buffer", 64, 84 "The size of the read buffer. A smaller buffer will more responsive while streaming, whereas "+ 85 "a larger buffer will have higher throughput.") 86 cmd.Flags.IntVar(&cmd.fetchSize, "fetch-size", 0, "Constrains the number of log entries to fetch per request.") 87 cmd.Flags.IntVar(&cmd.fetchBytes, "fetch-bytes", 0, "Constrains the number of bytes to fetch per request.") 88 cmd.Flags.BoolVar(&cmd.raw, "raw", false, 89 "Reproduce original log stream, instead of attempting to render for humans.") 90 return cmd 91 }, 92 } 93 } 94 95 func (cmd *catCommandRun) Run(scApp subcommands.Application, args []string, _ subcommands.Env) int { 96 a := scApp.(*application) 97 98 if len(args) == 0 { 99 log.Errorf(a, "At least one log path must be supplied.") 100 return 1 101 } 102 103 // Validate and construct our cat addresses. 104 addrs := make([]*types.StreamAddr, len(args)) 105 for i, arg := range args { 106 // If the address parses as a URL, use it directly. 107 var err error 108 if addrs[i], err = types.ParseURL(arg); err == nil { 109 continue 110 } 111 112 // User-friendly: trim any leading or trailing slashes from the path. 113 project, path, _, err := a.splitPath(arg) 114 if err != nil { 115 log.WithError(err).Errorf(a, "Invalid path specifier.") 116 return 1 117 } 118 119 addr := types.StreamAddr{Project: project, Path: types.StreamPath(path)} 120 if err := addr.Path.Validate(); err != nil { 121 log.Fields{ 122 log.ErrorKey: err, 123 "index": i, 124 "project": addr.Project, 125 "path": addr.Path, 126 }.Errorf(a, "Invalid command-line stream path.") 127 return 1 128 } 129 130 if addr.Host, err = a.resolveHost(""); err != nil { 131 err = errors.Annotate(err, "failed to resolve host: %q", addr.Host).Err() 132 errors.Log(a, err) 133 return 1 134 } 135 136 addrs[i] = &addr 137 } 138 if cmd.buffer <= 0 { 139 log.Fields{ 140 "value": cmd.buffer, 141 }.Errorf(a, "Buffer size must be >0.") 142 } 143 144 coords := make(map[string]*coordinator.Client, len(addrs)) 145 for _, addr := range addrs { 146 if _, ok := coords[addr.Host]; ok { 147 continue 148 } 149 150 var err error 151 if coords[addr.Host], err = a.coordinatorClient(addr.Host); err != nil { 152 err = errors.Annotate(err, "failed to create Coordinator client for %q", addr.Host).Err() 153 154 errors.Log(a, err) 155 return 1 156 } 157 } 158 159 tctx, _ := a.timeoutCtx(a) 160 for i, addr := range addrs { 161 if err := cmd.catPath(tctx, coords[addr.Host], addr); err != nil { 162 log.Fields{ 163 log.ErrorKey: err, 164 "project": addr.Project, 165 "path": addr.Path, 166 "index": i, 167 }.Errorf(a, "Failed to fetch log stream.") 168 169 if err == context.DeadlineExceeded { 170 return 2 171 } 172 return 1 173 } 174 } 175 176 return 0 177 } 178 179 func (cmd *catCommandRun) catPath(c context.Context, coord *coordinator.Client, addr *types.StreamAddr) error { 180 // Pull stream information. 181 f := coord.Stream(addr.Project, addr.Path).Fetcher(c, &fetcher.Options{ 182 Index: types.MessageIndex(cmd.index), 183 Count: cmd.count, 184 BufferCount: cmd.fetchSize, 185 BufferBytes: int64(cmd.fetchBytes), 186 }) 187 188 rend := renderer.Renderer{ 189 Source: f, 190 Raw: cmd.raw, 191 TextPrefix: func(le *logpb.LogEntry, line *logpb.Text_Line) string { 192 desc := f.Descriptor() 193 if desc == nil { 194 log.Errorf(c, "Failed to get text prefix descriptor.") 195 return "" 196 } 197 return cmd.getTextPrefix(desc, le) 198 }, 199 DatagramWriter: func(w io.Writer, dg []byte) bool { 200 desc := f.Descriptor() 201 if desc == nil { 202 log.Errorf(c, "Failed to get stream descriptor.") 203 return false 204 } 205 return getDatagramWriter(c, desc)(w, dg) 206 }, 207 } 208 if _, err := io.CopyBuffer(os.Stdout, &rend, make([]byte, cmd.buffer)); err != nil { 209 return err 210 } 211 return nil 212 } 213 214 func (cmd *catCommandRun) getTextPrefix(desc *logpb.LogStreamDescriptor, le *logpb.LogEntry) string { 215 var parts []string 216 if cmd.timestamps != timestampsOff { 217 ts := desc.Timestamp.AsTime() 218 ts = ts.Add(le.TimeOffset.AsDuration()) 219 switch cmd.timestamps { 220 case timestampsLocal: 221 parts = append(parts, ts.Local().Format(time.StampMilli)) 222 223 case timestampsUTC: 224 parts = append(parts, ts.UTC().Format(time.StampMilli)) 225 } 226 } 227 228 if cmd.showStreamIndex { 229 parts = append(parts, strconv.FormatUint(le.StreamIndex, 10)) 230 } 231 if len(parts) == 0 { 232 return "" 233 } 234 return strings.Join(parts, " ") + "| " 235 } 236 237 // getDatagramWriter returns a datagram writer function that can be used as a 238 // Renderer's DatagramWriter. The writer is bound to desc. 239 func getDatagramWriter(c context.Context, desc *logpb.LogStreamDescriptor) renderer.DatagramWriter { 240 241 return func(w io.Writer, dg []byte) bool { 242 var pb proto.Message 243 switch desc.ContentType { 244 case annopb.ContentTypeAnnotations: 245 mp := annopb.Step{} 246 if err := proto.Unmarshal(dg, &mp); err != nil { 247 log.WithError(err).Errorf(c, "Failed to unmarshal datagram data.") 248 return false 249 } 250 pb = &mp 251 252 default: 253 return false 254 } 255 256 if err := proto.MarshalText(w, pb); err != nil { 257 log.WithError(err).Errorf(c, "Failed to marshal datagram as text.") 258 return false 259 } 260 261 return true 262 } 263 }