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  }