go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/logdog/client/cli/subcommandLatest.go (about) 1 // Copyright 2016 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 "time" 22 23 "go.chromium.org/luci/common/clock" 24 "go.chromium.org/luci/common/errors" 25 log "go.chromium.org/luci/common/logging" 26 "go.chromium.org/luci/logdog/api/logpb" 27 "go.chromium.org/luci/logdog/client/coordinator" 28 "go.chromium.org/luci/logdog/common/renderer" 29 "go.chromium.org/luci/logdog/common/types" 30 31 "github.com/maruel/subcommands" 32 ) 33 34 type latestCommandRun struct { 35 subcommands.CommandRunBase 36 37 raw bool 38 } 39 40 func newLatestCommand() *subcommands.Command { 41 return &subcommands.Command{ 42 UsageLine: "latest [options] stream", 43 ShortDesc: "Write the latest full log record in a stream to STDOUT.", 44 LongDesc: "Write the latest full log record in a stream to STDOUT. If the stream " + 45 "doesn't have any log entries, will block until a log entry is available.", 46 CommandRun: func() subcommands.CommandRun { 47 cmd := &latestCommandRun{} 48 49 cmd.Flags.BoolVar(&cmd.raw, "raw", false, 50 "Reproduce original log stream, instead of attempting to render for humans.") 51 return cmd 52 }, 53 } 54 } 55 56 func (cmd *latestCommandRun) Run(scApp subcommands.Application, args []string, _ subcommands.Env) int { 57 a := scApp.(*application) 58 59 // User-friendly: trim any leading or trailing slashes from the path. 60 if len(args) != 1 { 61 log.Errorf(a, "Exactly one argument, the stream path, must be supplied.") 62 return 1 63 } 64 65 var addr *types.StreamAddr 66 var err error 67 if addr, err = types.ParseURL(args[0]); err != nil { 68 // Not a log stream address. 69 project, path, _, err := a.splitPath(args[0]) 70 if err != nil { 71 log.WithError(err).Errorf(a, "Invalid path specifier.") 72 return 1 73 } 74 75 addr = &types.StreamAddr{Project: project, Path: types.StreamPath(path)} 76 if err := addr.Path.Validate(); err != nil { 77 log.Fields{ 78 log.ErrorKey: err, 79 "project": addr.Project, 80 "path": addr.Path, 81 }.Errorf(a, "Invalid command-line stream path.") 82 return 1 83 } 84 } 85 86 coord, err := a.coordinatorClient(addr.Host) 87 if err != nil { 88 errors.Log(a, errors.Annotate(err, "failed to create Coordinator client").Err()) 89 return 1 90 } 91 92 stream := coord.Stream(addr.Project, addr.Path) 93 94 tctx, _ := a.timeoutCtx(a) 95 le, st, err := cmd.getTailEntry(tctx, stream) 96 if err != nil { 97 log.Fields{ 98 log.ErrorKey: err, 99 "project": addr.Project, 100 "path": addr.Path, 101 }.Errorf(a, "Failed to load latest record.") 102 103 if err == context.DeadlineExceeded { 104 return 2 105 } 106 return 1 107 } 108 109 // Render the entry. 110 r := renderer.Renderer{ 111 Source: &renderer.StaticSource{le}, 112 Raw: cmd.raw, 113 DatagramWriter: getDatagramWriter(a, &st.Desc), 114 } 115 if _, err := io.Copy(os.Stdout, &r); err != nil { 116 log.WithError(err).Errorf(a, "failed to write to output") 117 return 1 118 } 119 120 return 0 121 } 122 123 func (cmd *latestCommandRun) getTailEntry(c context.Context, s *coordinator.Stream) ( 124 *logpb.LogEntry, *coordinator.LogStream, error) { 125 126 // Loop until we either hard fail or succeed. 127 var st coordinator.LogStream 128 129 delayTimer := clock.NewTimer(c) 130 defer delayTimer.Stop() 131 for { 132 ls, err := s.Tail(c, coordinator.Complete(), coordinator.WithState(&st)) 133 134 // TODO(iannucci,dnj): use retry module + transient tags instead 135 delayTimer.Reset(5 * time.Second) 136 switch { 137 case err == nil: 138 return ls, &st, nil 139 140 case err == coordinator.ErrNoSuchStream, ls == nil: 141 log.WithError(err).Warningf(c, "No log entries, sleeping and retry.") 142 143 if ar := <-delayTimer.GetC(); ar.Incomplete() { 144 // Timer stopped prematurely. 145 return nil, nil, ar.Err 146 } 147 148 default: 149 return nil, nil, err 150 } 151 } 152 }