go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/logdog/client/cli/subcommandQuery.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 "bufio" 19 "context" 20 "encoding/json" 21 "io" 22 "os" 23 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/butlerlib/streamproto" 28 "go.chromium.org/luci/logdog/client/coordinator" 29 30 "github.com/maruel/subcommands" 31 ) 32 33 const ( 34 // defaultQueryResults is the default number of query results to return. 35 defaultQueryResults = 200 36 ) 37 38 type queryCommandRun struct { 39 subcommands.CommandRunBase 40 41 path string 42 contentType string 43 tags streamproto.TagMap 44 results int 45 purged trinaryValue 46 47 json bool 48 out string 49 } 50 51 func newQueryCommand() *subcommands.Command { 52 return &subcommands.Command{ 53 UsageLine: "query -path ... [OPTIONS]", 54 ShortDesc: "Query for log streams.", 55 LongDesc: "" + 56 "Returns log stream paths that match a given path pattern\n" + 57 "\n" + 58 "An input path must be of the form 'full/path/prefix/+/stream/name'\n" + 59 "'stream/name' portion can contain glob-style '*' and '**' operators.", 60 CommandRun: func() subcommands.CommandRun { 61 cmd := &queryCommandRun{} 62 63 fs := cmd.GetFlags() 64 fs.StringVar(&cmd.path, "path", "", "Filter logs matching this path (may include globbing).") 65 fs.StringVar(&cmd.contentType, "contentType", "", "Limit results to a content type.") 66 fs.Var(&cmd.tags, "tag", "Filter logs containing this tag (key[=value]).") 67 fs.Var(&cmd.purged, "purged", "Include purged streams in the result. This requires administrative privileges.") 68 fs.IntVar(&cmd.results, "results", defaultQueryResults, 69 "The maximum number of results to return. If 0, no limit will be applied.") 70 fs.BoolVar(&cmd.json, "json", false, "Output JSON state instead of log stream names.") 71 fs.StringVar(&cmd.out, "out", "-", "Path to query result output. Use '-' for STDOUT (default).") 72 73 return cmd 74 }, 75 } 76 } 77 78 func (cmd *queryCommandRun) Run(scApp subcommands.Application, args []string, _ subcommands.Env) int { 79 a := scApp.(*application) 80 81 // User-friendly: trim any leading or trailing slashes from the path. 82 project, path, unified, err := a.splitPath(cmd.path) 83 if err != nil { 84 log.WithError(err).Errorf(a, "Invalid path specifier.") 85 return 1 86 } 87 88 coord, err := a.coordinatorClient("") 89 if err != nil { 90 errors.Log(a, errors.Annotate(err, "could not create Coordinator client").Err()) 91 return 1 92 } 93 94 // Open our output file, if necessary. 95 w := io.Writer(nil) 96 switch cmd.out { 97 case "-": 98 w = os.Stdout 99 default: 100 f, err := os.OpenFile(cmd.out, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0643) 101 if err != nil { 102 log.Fields{ 103 log.ErrorKey: err, 104 "path": cmd.out, 105 }.Errorf(a, "Failed to open output file for writing.") 106 return 1 107 } 108 defer f.Close() 109 w = f 110 } 111 112 bw := bufio.NewWriter(w) 113 defer bw.Flush() 114 115 o := queryOutput(nil) 116 if cmd.json { 117 o = &jsonQueryOutput{ 118 Writer: bw, 119 } 120 } else { 121 o = &pathQueryOutput{ 122 Writer: bw, 123 unified: unified, 124 } 125 } 126 127 qo := coordinator.QueryOptions{ 128 ContentType: cmd.contentType, 129 State: cmd.json, 130 Purged: cmd.purged.Trinary(), 131 } 132 count := 0 133 log.Debugf(a, "Issuing query...") 134 135 tctx, _ := a.timeoutCtx(a) 136 ierr := error(nil) 137 err = coord.Query(tctx, project, path, qo, func(s *coordinator.LogStream) bool { 138 if err := o.emit(s); err != nil { 139 ierr = err 140 return false 141 } 142 143 count++ 144 return !(cmd.results > 0 && count >= cmd.results) 145 }) 146 if err == nil { 147 // Propagate internal error. 148 err = ierr 149 } 150 if err != nil { 151 log.Fields{ 152 log.ErrorKey: err, 153 "count": count, 154 }.Errorf(a, "Query failed.") 155 156 if err == context.DeadlineExceeded { 157 return 2 158 } 159 return 1 160 } 161 log.Fields{ 162 "count": count, 163 }.Infof(a, "Query sequence completed.") 164 165 // (Terminate output stream) 166 if err := o.end(); err != nil { 167 log.Fields{ 168 log.ErrorKey: err, 169 }.Errorf(a, "Failed to end output stream.") 170 } 171 172 return 0 173 } 174 175 type queryOutput interface { 176 emit(*coordinator.LogStream) error 177 end() error 178 } 179 180 // pathQueryOutput outputs query results as a list of stream path names. 181 type pathQueryOutput struct { 182 *bufio.Writer 183 184 unified bool 185 } 186 187 func (p *pathQueryOutput) emit(s *coordinator.LogStream) error { 188 path := string(s.Path) 189 if p.unified { 190 path = makeUnifiedPath(s.Project, s.Path) 191 } 192 193 if _, err := p.WriteString(path); err != nil { 194 return err 195 } 196 if _, err := p.WriteRune('\n'); err != nil { 197 return err 198 } 199 if err := p.Flush(); err != nil { 200 return err 201 } 202 return nil 203 } 204 205 func (p *pathQueryOutput) end() error { return nil } 206 207 // We will emit a JSON list of results. To get streaming JSON, we will 208 // manually construct the outer list and then use the JOSN library to build 209 // each internal element. 210 type jsonQueryOutput struct { 211 *bufio.Writer 212 213 enc *json.Encoder 214 count int 215 } 216 217 func (p *jsonQueryOutput) emit(s *coordinator.LogStream) error { 218 if err := p.ensureStart(); err != nil { 219 return err 220 } 221 222 if p.count > 0 { 223 // Emit comma from previous element. 224 _, err := p.WriteRune(',') 225 if err != nil { 226 return err 227 } 228 } 229 p.count++ 230 231 o := struct { 232 Project string `json:"project"` 233 Path string `json:"path"` 234 Descriptor *logpb.LogStreamDescriptor `json:"descriptor,omitempty"` 235 236 TerminalIndex int64 `json:"terminalIndex"` 237 ArchiveIndexURL string `json:"archiveIndexUrl,omitempty"` 238 ArchiveStreamURL string `json:"archiveStreamUrl,omitempty"` 239 ArchiveDataURL string `json:"archiveDataUrl,omitempty"` 240 Purged bool `json:"purged,omitempty"` 241 }{ 242 Project: string(s.Project), 243 Path: string(s.Path), 244 } 245 o.TerminalIndex = int64(s.State.TerminalIndex) 246 o.ArchiveIndexURL = s.State.ArchiveIndexURL 247 o.ArchiveStreamURL = s.State.ArchiveStreamURL 248 o.ArchiveDataURL = s.State.ArchiveDataURL 249 o.Purged = s.State.Purged 250 o.Descriptor = &s.Desc 251 252 if p.enc == nil { 253 p.enc = json.NewEncoder(p) 254 } 255 if err := p.enc.Encode(&o); err != nil { 256 return err 257 } 258 259 return p.Flush() 260 } 261 262 func (p *jsonQueryOutput) ensureStart() error { 263 if p.count > 0 { 264 return nil 265 } 266 _, err := p.WriteString("[\n") 267 return err 268 } 269 270 func (p *jsonQueryOutput) end() error { 271 if err := p.ensureStart(); err != nil { 272 return err 273 } 274 275 _, err := p.WriteRune(']') 276 return err 277 }