go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/logdog/client/cli/subcommandCat.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  	"context"
    19  	"io"
    20  	"os"
    21  	"strconv"
    22  	"strings"
    23  	"time"
    24  
    25  	"go.chromium.org/luci/common/errors"
    26  	"go.chromium.org/luci/common/flag/flagenum"
    27  	log "go.chromium.org/luci/common/logging"
    28  	"go.chromium.org/luci/logdog/api/logpb"
    29  	"go.chromium.org/luci/logdog/client/coordinator"
    30  	"go.chromium.org/luci/logdog/common/fetcher"
    31  	"go.chromium.org/luci/logdog/common/renderer"
    32  	"go.chromium.org/luci/logdog/common/types"
    33  	annopb "go.chromium.org/luci/luciexe/legacy/annotee/proto"
    34  
    35  	"github.com/golang/protobuf/proto"
    36  	"github.com/maruel/subcommands"
    37  )
    38  
    39  type timestampsFlag string
    40  
    41  const (
    42  	timestampsOff   timestampsFlag = ""
    43  	timestampsLocal timestampsFlag = "local"
    44  	timestampsUTC   timestampsFlag = "utc"
    45  )
    46  
    47  func (t *timestampsFlag) Set(v string) error { return timestampFlagEnum.FlagSet(t, v) }
    48  func (t *timestampsFlag) String() string     { return timestampFlagEnum.FlagString(t) }
    49  
    50  var timestampFlagEnum = flagenum.Enum{
    51  	"":      timestampsOff,
    52  	"local": timestampsLocal,
    53  	"utc":   timestampsUTC,
    54  }
    55  
    56  type catCommandRun struct {
    57  	subcommands.CommandRunBase
    58  
    59  	index      int64
    60  	count      int64
    61  	buffer     int
    62  	fetchSize  int
    63  	fetchBytes int
    64  	raw        bool
    65  
    66  	timestamps      timestampsFlag
    67  	showStreamIndex bool
    68  }
    69  
    70  func newCatCommand() *subcommands.Command {
    71  	return &subcommands.Command{
    72  		UsageLine: "cat",
    73  		ShortDesc: "Write log stream to STDOUT.",
    74  		CommandRun: func() subcommands.CommandRun {
    75  			cmd := &catCommandRun{}
    76  
    77  			cmd.Flags.Int64Var(&cmd.index, "index", 0, "Starting index.")
    78  			cmd.Flags.Int64Var(&cmd.count, "count", 0, "The number of log entries to fetch.")
    79  			cmd.Flags.Var(&cmd.timestamps, "timestamps",
    80  				"When rendering text logs, prefix them with their timestamps. Options are: "+timestampFlagEnum.Choices())
    81  			cmd.Flags.BoolVar(&cmd.showStreamIndex, "show-stream-index", false,
    82  				"When rendering text logs, show their stream index.")
    83  			cmd.Flags.IntVar(&cmd.buffer, "buffer", 64,
    84  				"The size of the read buffer. A smaller buffer will more responsive while streaming, whereas "+
    85  					"a larger buffer will have higher throughput.")
    86  			cmd.Flags.IntVar(&cmd.fetchSize, "fetch-size", 0, "Constrains the number of log entries to fetch per request.")
    87  			cmd.Flags.IntVar(&cmd.fetchBytes, "fetch-bytes", 0, "Constrains the number of bytes to fetch per request.")
    88  			cmd.Flags.BoolVar(&cmd.raw, "raw", false,
    89  				"Reproduce original log stream, instead of attempting to render for humans.")
    90  			return cmd
    91  		},
    92  	}
    93  }
    94  
    95  func (cmd *catCommandRun) Run(scApp subcommands.Application, args []string, _ subcommands.Env) int {
    96  	a := scApp.(*application)
    97  
    98  	if len(args) == 0 {
    99  		log.Errorf(a, "At least one log path must be supplied.")
   100  		return 1
   101  	}
   102  
   103  	// Validate and construct our cat addresses.
   104  	addrs := make([]*types.StreamAddr, len(args))
   105  	for i, arg := range args {
   106  		// If the address parses as a URL, use it directly.
   107  		var err error
   108  		if addrs[i], err = types.ParseURL(arg); err == nil {
   109  			continue
   110  		}
   111  
   112  		// User-friendly: trim any leading or trailing slashes from the path.
   113  		project, path, _, err := a.splitPath(arg)
   114  		if err != nil {
   115  			log.WithError(err).Errorf(a, "Invalid path specifier.")
   116  			return 1
   117  		}
   118  
   119  		addr := types.StreamAddr{Project: project, Path: types.StreamPath(path)}
   120  		if err := addr.Path.Validate(); err != nil {
   121  			log.Fields{
   122  				log.ErrorKey: err,
   123  				"index":      i,
   124  				"project":    addr.Project,
   125  				"path":       addr.Path,
   126  			}.Errorf(a, "Invalid command-line stream path.")
   127  			return 1
   128  		}
   129  
   130  		if addr.Host, err = a.resolveHost(""); err != nil {
   131  			err = errors.Annotate(err, "failed to resolve host: %q", addr.Host).Err()
   132  			errors.Log(a, err)
   133  			return 1
   134  		}
   135  
   136  		addrs[i] = &addr
   137  	}
   138  	if cmd.buffer <= 0 {
   139  		log.Fields{
   140  			"value": cmd.buffer,
   141  		}.Errorf(a, "Buffer size must be >0.")
   142  	}
   143  
   144  	coords := make(map[string]*coordinator.Client, len(addrs))
   145  	for _, addr := range addrs {
   146  		if _, ok := coords[addr.Host]; ok {
   147  			continue
   148  		}
   149  
   150  		var err error
   151  		if coords[addr.Host], err = a.coordinatorClient(addr.Host); err != nil {
   152  			err = errors.Annotate(err, "failed to create Coordinator client for %q", addr.Host).Err()
   153  
   154  			errors.Log(a, err)
   155  			return 1
   156  		}
   157  	}
   158  
   159  	tctx, _ := a.timeoutCtx(a)
   160  	for i, addr := range addrs {
   161  		if err := cmd.catPath(tctx, coords[addr.Host], addr); err != nil {
   162  			log.Fields{
   163  				log.ErrorKey: err,
   164  				"project":    addr.Project,
   165  				"path":       addr.Path,
   166  				"index":      i,
   167  			}.Errorf(a, "Failed to fetch log stream.")
   168  
   169  			if err == context.DeadlineExceeded {
   170  				return 2
   171  			}
   172  			return 1
   173  		}
   174  	}
   175  
   176  	return 0
   177  }
   178  
   179  func (cmd *catCommandRun) catPath(c context.Context, coord *coordinator.Client, addr *types.StreamAddr) error {
   180  	// Pull stream information.
   181  	f := coord.Stream(addr.Project, addr.Path).Fetcher(c, &fetcher.Options{
   182  		Index:       types.MessageIndex(cmd.index),
   183  		Count:       cmd.count,
   184  		BufferCount: cmd.fetchSize,
   185  		BufferBytes: int64(cmd.fetchBytes),
   186  	})
   187  
   188  	rend := renderer.Renderer{
   189  		Source: f,
   190  		Raw:    cmd.raw,
   191  		TextPrefix: func(le *logpb.LogEntry, line *logpb.Text_Line) string {
   192  			desc := f.Descriptor()
   193  			if desc == nil {
   194  				log.Errorf(c, "Failed to get text prefix descriptor.")
   195  				return ""
   196  			}
   197  			return cmd.getTextPrefix(desc, le)
   198  		},
   199  		DatagramWriter: func(w io.Writer, dg []byte) bool {
   200  			desc := f.Descriptor()
   201  			if desc == nil {
   202  				log.Errorf(c, "Failed to get stream descriptor.")
   203  				return false
   204  			}
   205  			return getDatagramWriter(c, desc)(w, dg)
   206  		},
   207  	}
   208  	if _, err := io.CopyBuffer(os.Stdout, &rend, make([]byte, cmd.buffer)); err != nil {
   209  		return err
   210  	}
   211  	return nil
   212  }
   213  
   214  func (cmd *catCommandRun) getTextPrefix(desc *logpb.LogStreamDescriptor, le *logpb.LogEntry) string {
   215  	var parts []string
   216  	if cmd.timestamps != timestampsOff {
   217  		ts := desc.Timestamp.AsTime()
   218  		ts = ts.Add(le.TimeOffset.AsDuration())
   219  		switch cmd.timestamps {
   220  		case timestampsLocal:
   221  			parts = append(parts, ts.Local().Format(time.StampMilli))
   222  
   223  		case timestampsUTC:
   224  			parts = append(parts, ts.UTC().Format(time.StampMilli))
   225  		}
   226  	}
   227  
   228  	if cmd.showStreamIndex {
   229  		parts = append(parts, strconv.FormatUint(le.StreamIndex, 10))
   230  	}
   231  	if len(parts) == 0 {
   232  		return ""
   233  	}
   234  	return strings.Join(parts, " ") + "| "
   235  }
   236  
   237  // getDatagramWriter returns a datagram writer function that can be used as a
   238  // Renderer's DatagramWriter. The writer is bound to desc.
   239  func getDatagramWriter(c context.Context, desc *logpb.LogStreamDescriptor) renderer.DatagramWriter {
   240  
   241  	return func(w io.Writer, dg []byte) bool {
   242  		var pb proto.Message
   243  		switch desc.ContentType {
   244  		case annopb.ContentTypeAnnotations:
   245  			mp := annopb.Step{}
   246  			if err := proto.Unmarshal(dg, &mp); err != nil {
   247  				log.WithError(err).Errorf(c, "Failed to unmarshal datagram data.")
   248  				return false
   249  			}
   250  			pb = &mp
   251  
   252  		default:
   253  			return false
   254  		}
   255  
   256  		if err := proto.MarshalText(w, pb); err != nil {
   257  			log.WithError(err).Errorf(c, "Failed to marshal datagram as text.")
   258  			return false
   259  		}
   260  
   261  		return true
   262  	}
   263  }