go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/buildbucket/cli/log.go (about) 1 // Copyright 2019 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 "fmt" 20 "io" 21 "os" 22 "time" 23 24 "github.com/maruel/subcommands" 25 "google.golang.org/genproto/protobuf/field_mask" 26 "google.golang.org/grpc/codes" 27 "google.golang.org/grpc/status" 28 29 "go.chromium.org/luci/buildbucket/protoutil" 30 "go.chromium.org/luci/common/cli" 31 "go.chromium.org/luci/common/data/stringset" 32 "go.chromium.org/luci/common/sync/parallel" 33 "go.chromium.org/luci/grpc/prpc" 34 35 "go.chromium.org/luci/logdog/client/coordinator" 36 "go.chromium.org/luci/logdog/common/fetcher" 37 "go.chromium.org/luci/logdog/common/types" 38 39 pb "go.chromium.org/luci/buildbucket/proto" 40 logpb "go.chromium.org/luci/logdog/api/logpb" 41 ) 42 43 func cmdLog(p Params) *subcommands.Command { 44 return &subcommands.Command{ 45 UsageLine: `log [flags] <BUILD> <STEP> [<LOG>...]`, 46 ShortDesc: "prints step logs", 47 LongDesc: doc(` 48 Prints step logs. 49 50 Argument BUILD can be an int64 build id or a string 51 <project>/<bucket>/<builder>/<build_number>, e.g. chromium/ci/linux-rel/1 52 53 Argument STEP is a build step name, e.g. "bot_update". 54 Use | as parent-child separator, e.g. "parent|child". 55 56 Arguments LOG is one or more log names of the STEP. They will be multiplexed 57 by time. Defaults to stdout and stderr. 58 Log "stderr" is printed to stderr. 59 `), 60 CommandRun: func() subcommands.CommandRun { 61 r := &logRun{} 62 r.RegisterDefaultFlags(p) 63 return r 64 }, 65 } 66 } 67 68 type logRun struct { 69 baseCommandRun 70 71 // args 72 73 build string 74 getBuild *pb.GetBuildRequest 75 step string 76 logs stringset.Set 77 } 78 79 func (r *logRun) Run(a subcommands.Application, args []string, env subcommands.Env) int { 80 ctx := cli.GetContext(a, r, env) 81 if err := r.initClients(ctx, nil); err != nil { 82 return r.done(ctx, err) 83 } 84 85 if err := r.parseArgs(args); err != nil { 86 return r.done(ctx, err) 87 } 88 89 // Find the step. 90 steps, err := r.getSteps(ctx) 91 if err != nil { 92 return r.done(ctx, err) 93 } 94 var step *pb.Step 95 for _, s := range steps { 96 if s.Name == r.step { 97 step = s 98 break 99 } 100 } 101 if step == nil { 102 fmt.Fprintf(os.Stderr, "step %q not found in build %q. Available steps:\n", r.step, r.build) 103 for _, s := range steps { 104 fmt.Fprintf(os.Stderr, " %s\n", s.Name) 105 } 106 return 1 107 } 108 109 // Find the logs. 110 logByName := map[string]*pb.Log{} 111 for _, l := range step.Logs { 112 logByName[l.Name] = l 113 } 114 var logs []*pb.Log 115 if len(r.logs) == 0 { 116 logs = r.defaultLogs(logByName) 117 } else { 118 logs = make([]*pb.Log, 0, len(r.logs)) 119 for name := range r.logs { 120 l, ok := logByName[name] 121 if !ok { 122 fmt.Fprintf(os.Stderr, "log %q not found in step %q of build %q. Available logs:\n", name, r.step, r.build) 123 for _, l := range step.Logs { 124 fmt.Fprintf(os.Stderr, " %s\n", l.Name) 125 } 126 return 1 127 } 128 logs = append(logs, l) 129 } 130 } 131 132 return r.done(ctx, r.printLogs(ctx, logs)) 133 } 134 135 func (r *logRun) parseArgs(args []string) error { 136 if len(args) < 2 { 137 return fmt.Errorf("usage: bb log <BUILD> <STEP> [LOG...]") 138 } 139 140 r.build = args[0] 141 r.step = args[1] 142 r.logs = stringset.NewFromSlice(args[2:]...) 143 var err error 144 r.getBuild, err = protoutil.ParseGetBuildRequest(r.build) 145 return err 146 } 147 148 func (r *logRun) defaultLogs(available map[string]*pb.Log) []*pb.Log { 149 logs := make([]*pb.Log, 0, 2) 150 if log, ok := available["stdout"]; ok { 151 logs = append(logs, log) 152 } 153 if log, ok := available["stderr"]; ok { 154 logs = append(logs, log) 155 } 156 return logs 157 } 158 159 // getSteps fetches steps of the build. 160 func (r *logRun) getSteps(ctx context.Context) ([]*pb.Step, error) { 161 r.getBuild.Fields = &field_mask.FieldMask{Paths: []string{"steps"}} 162 build, err := r.buildsClient.GetBuild(ctx, r.getBuild, prpc.ExpectedCode(codes.NotFound)) 163 switch status.Code(err) { 164 case codes.OK: 165 return build.Steps, nil 166 case codes.NotFound: 167 return nil, fmt.Errorf("build not found") 168 default: 169 return nil, err 170 } 171 } 172 173 // printLogs fetches and prints the logs to io.Stdout/io.Stderr. 174 // Entries from different logs are multiplexed by timestamps. 175 func (r *logRun) printLogs(ctx context.Context, logs []*pb.Log) error { 176 if len(logs) == 0 { 177 return nil 178 } 179 180 // Parse Log URLs. 181 addrs := make([]*types.StreamAddr, len(logs)) 182 for i, l := range logs { 183 var err error 184 addrs[i], err = types.ParseURL(l.Url) 185 if err != nil { 186 return fmt.Errorf("log %q has unsupported URL %q", l.Name, l.Url) 187 } 188 189 if addrs[0].Host != addrs[i].Host { 190 // unrealistic 191 return fmt.Errorf("multiple different logdog hosts are not supported") 192 } 193 } 194 195 // Create a client. 196 client := coordinator.NewClient(&prpc.Client{ 197 C: r.httpClient, 198 Host: addrs[0].Host, 199 }) 200 201 // Fetch descriptors and create fetchers. 202 fetchers := make([]*fetcher.Fetcher, len(addrs)) 203 m := logMultiplexer{Chans: make([]logChan, len(addrs))} 204 err := parallel.FanOutIn(func(work chan<- func() error) { 205 for i, addr := range addrs { 206 i := i 207 stream := client.Stream(addr.Project, addr.Path) 208 fetchers[i] = stream.Fetcher(ctx, nil) 209 work <- func() error { 210 s, err := stream.State(ctx) 211 if err != nil { 212 return err 213 } 214 if s.Desc.StreamType != logpb.StreamType_TEXT { 215 return fmt.Errorf("log %q is not a text stream; only text streams are currently supported", logs[i].Name) 216 } 217 m.Chans[i].start = s.Desc.Timestamp.AsTime() 218 return err 219 } 220 } 221 }) 222 if err != nil { 223 return err 224 } 225 226 // Start fetching. 227 for i := range m.Chans { 228 i := i 229 c := make(chan logAndErr, 256) 230 m.Chans[i].c = c 231 go func() { 232 defer close(c) 233 for { 234 log, err := fetchers[i].NextLogEntry() 235 c <- logAndErr{log, err} 236 if err != nil { 237 break 238 } 239 } 240 }() 241 } 242 243 stdout, stderr := newStdioPrinters(r.noColor) 244 for { 245 chanIndex, entry, err := m.Next() 246 out := stdout 247 switch { 248 case err == io.EOF: 249 return nil 250 case err != nil: 251 return err 252 case logs[chanIndex].Name == "stderr": 253 out = stderr 254 } 255 256 for _, line := range entry.GetText().GetLines() { 257 out.f("%s%s", line.Value, line.Delimiter) 258 } 259 if out.Err != nil { 260 return out.Err 261 } 262 } 263 } 264 265 type logChan struct { 266 start time.Time // base time for log entries 267 c chan logAndErr // channel of log entries 268 } 269 270 type logAndErr struct { 271 *logpb.LogEntry 272 err error 273 } 274 275 // logMultiplexer receives from Chans and multiplexes log entries by timestamp. 276 type logMultiplexer struct { 277 Chans []logChan 278 279 // internal state 280 281 heads []logAndErr 282 } 283 284 // Next returns the next log entry. 285 // Returns io.EOF if there is no next log entry. 286 func (m *logMultiplexer) Next() (chanIndex int, log *logpb.LogEntry, err error) { 287 if m.heads == nil { 288 // initialization 289 m.heads = make([]logAndErr, len(m.Chans)) 290 for i := range m.heads { 291 m.heads[i] = <-m.Chans[i].c 292 } 293 } 294 295 // Choose the earliest one. 296 // Note: using heap is overkill here. 297 // Most of the time we deal with only two logs. 298 earliest := -1 299 earliestTime := time.Time{} 300 for i, h := range m.heads { 301 if h.err == io.EOF { 302 continue 303 } 304 if h.err != nil { 305 return 0, nil, h.err 306 } 307 308 curTime := m.Chans[i].start 309 if h.TimeOffset != nil { 310 if err := h.TimeOffset.CheckValid(); err != nil { 311 return 0, nil, err 312 } 313 offset := h.TimeOffset.AsDuration() 314 curTime = curTime.Add(offset) 315 } 316 317 isEarlier := earliest == -1 || 318 curTime.Before(earliestTime) || 319 (curTime.Equal(earliestTime) && h.PrefixIndex < m.heads[earliest].PrefixIndex) 320 if isEarlier { 321 earliest = i 322 earliestTime = curTime 323 } 324 } 325 if earliest == -1 { 326 // all finished 327 return 0, nil, io.EOF 328 } 329 330 // Call f and replace the entry with the freshest one from the same 331 // channel. 332 earliestEntry := m.heads[earliest].LogEntry 333 m.heads[earliest] = <-m.Chans[earliest].c 334 return earliest, earliestEntry, nil 335 }