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  }