go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/buildbucket/cli/log.go (about)

     1  // Copyright 2019 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  	"fmt"
    20  	"io"
    21  	"os"
    22  	"time"
    23  
    24  	"github.com/maruel/subcommands"
    25  	"google.golang.org/genproto/protobuf/field_mask"
    26  	"google.golang.org/grpc/codes"
    27  	"google.golang.org/grpc/status"
    28  
    29  	"go.chromium.org/luci/buildbucket/protoutil"
    30  	"go.chromium.org/luci/common/cli"
    31  	"go.chromium.org/luci/common/data/stringset"
    32  	"go.chromium.org/luci/common/sync/parallel"
    33  	"go.chromium.org/luci/grpc/prpc"
    34  
    35  	"go.chromium.org/luci/logdog/client/coordinator"
    36  	"go.chromium.org/luci/logdog/common/fetcher"
    37  	"go.chromium.org/luci/logdog/common/types"
    38  
    39  	pb "go.chromium.org/luci/buildbucket/proto"
    40  	logpb "go.chromium.org/luci/logdog/api/logpb"
    41  )
    42  
    43  func cmdLog(p Params) *subcommands.Command {
    44  	return &subcommands.Command{
    45  		UsageLine: `log [flags] <BUILD> <STEP> [<LOG>...]`,
    46  		ShortDesc: "prints step logs",
    47  		LongDesc: doc(`
    48  			Prints step logs.
    49  
    50  			Argument BUILD can be an int64 build id or a string
    51  			<project>/<bucket>/<builder>/<build_number>, e.g. chromium/ci/linux-rel/1
    52  
    53  			Argument STEP is a build step name, e.g. "bot_update".
    54  			Use | as parent-child separator, e.g. "parent|child".
    55  
    56  			Arguments LOG is one or more log names of the STEP. They will be multiplexed
    57  			by time. Defaults to stdout and stderr.
    58  			Log "stderr" is printed to stderr.
    59  		`),
    60  		CommandRun: func() subcommands.CommandRun {
    61  			r := &logRun{}
    62  			r.RegisterDefaultFlags(p)
    63  			return r
    64  		},
    65  	}
    66  }
    67  
    68  type logRun struct {
    69  	baseCommandRun
    70  
    71  	// args
    72  
    73  	build    string
    74  	getBuild *pb.GetBuildRequest
    75  	step     string
    76  	logs     stringset.Set
    77  }
    78  
    79  func (r *logRun) Run(a subcommands.Application, args []string, env subcommands.Env) int {
    80  	ctx := cli.GetContext(a, r, env)
    81  	if err := r.initClients(ctx, nil); err != nil {
    82  		return r.done(ctx, err)
    83  	}
    84  
    85  	if err := r.parseArgs(args); err != nil {
    86  		return r.done(ctx, err)
    87  	}
    88  
    89  	// Find the step.
    90  	steps, err := r.getSteps(ctx)
    91  	if err != nil {
    92  		return r.done(ctx, err)
    93  	}
    94  	var step *pb.Step
    95  	for _, s := range steps {
    96  		if s.Name == r.step {
    97  			step = s
    98  			break
    99  		}
   100  	}
   101  	if step == nil {
   102  		fmt.Fprintf(os.Stderr, "step %q not found in build %q. Available steps:\n", r.step, r.build)
   103  		for _, s := range steps {
   104  			fmt.Fprintf(os.Stderr, "  %s\n", s.Name)
   105  		}
   106  		return 1
   107  	}
   108  
   109  	// Find the logs.
   110  	logByName := map[string]*pb.Log{}
   111  	for _, l := range step.Logs {
   112  		logByName[l.Name] = l
   113  	}
   114  	var logs []*pb.Log
   115  	if len(r.logs) == 0 {
   116  		logs = r.defaultLogs(logByName)
   117  	} else {
   118  		logs = make([]*pb.Log, 0, len(r.logs))
   119  		for name := range r.logs {
   120  			l, ok := logByName[name]
   121  			if !ok {
   122  				fmt.Fprintf(os.Stderr, "log %q not found in step %q of build %q. Available logs:\n", name, r.step, r.build)
   123  				for _, l := range step.Logs {
   124  					fmt.Fprintf(os.Stderr, "  %s\n", l.Name)
   125  				}
   126  				return 1
   127  			}
   128  			logs = append(logs, l)
   129  		}
   130  	}
   131  
   132  	return r.done(ctx, r.printLogs(ctx, logs))
   133  }
   134  
   135  func (r *logRun) parseArgs(args []string) error {
   136  	if len(args) < 2 {
   137  		return fmt.Errorf("usage: bb log <BUILD> <STEP> [LOG...]")
   138  	}
   139  
   140  	r.build = args[0]
   141  	r.step = args[1]
   142  	r.logs = stringset.NewFromSlice(args[2:]...)
   143  	var err error
   144  	r.getBuild, err = protoutil.ParseGetBuildRequest(r.build)
   145  	return err
   146  }
   147  
   148  func (r *logRun) defaultLogs(available map[string]*pb.Log) []*pb.Log {
   149  	logs := make([]*pb.Log, 0, 2)
   150  	if log, ok := available["stdout"]; ok {
   151  		logs = append(logs, log)
   152  	}
   153  	if log, ok := available["stderr"]; ok {
   154  		logs = append(logs, log)
   155  	}
   156  	return logs
   157  }
   158  
   159  // getSteps fetches steps of the build.
   160  func (r *logRun) getSteps(ctx context.Context) ([]*pb.Step, error) {
   161  	r.getBuild.Fields = &field_mask.FieldMask{Paths: []string{"steps"}}
   162  	build, err := r.buildsClient.GetBuild(ctx, r.getBuild, prpc.ExpectedCode(codes.NotFound))
   163  	switch status.Code(err) {
   164  	case codes.OK:
   165  		return build.Steps, nil
   166  	case codes.NotFound:
   167  		return nil, fmt.Errorf("build not found")
   168  	default:
   169  		return nil, err
   170  	}
   171  }
   172  
   173  // printLogs fetches and prints the logs to io.Stdout/io.Stderr.
   174  // Entries from different logs are multiplexed by timestamps.
   175  func (r *logRun) printLogs(ctx context.Context, logs []*pb.Log) error {
   176  	if len(logs) == 0 {
   177  		return nil
   178  	}
   179  
   180  	// Parse Log URLs.
   181  	addrs := make([]*types.StreamAddr, len(logs))
   182  	for i, l := range logs {
   183  		var err error
   184  		addrs[i], err = types.ParseURL(l.Url)
   185  		if err != nil {
   186  			return fmt.Errorf("log %q has unsupported URL %q", l.Name, l.Url)
   187  		}
   188  
   189  		if addrs[0].Host != addrs[i].Host {
   190  			// unrealistic
   191  			return fmt.Errorf("multiple different logdog hosts are not supported")
   192  		}
   193  	}
   194  
   195  	// Create a client.
   196  	client := coordinator.NewClient(&prpc.Client{
   197  		C:    r.httpClient,
   198  		Host: addrs[0].Host,
   199  	})
   200  
   201  	// Fetch descriptors and create fetchers.
   202  	fetchers := make([]*fetcher.Fetcher, len(addrs))
   203  	m := logMultiplexer{Chans: make([]logChan, len(addrs))}
   204  	err := parallel.FanOutIn(func(work chan<- func() error) {
   205  		for i, addr := range addrs {
   206  			i := i
   207  			stream := client.Stream(addr.Project, addr.Path)
   208  			fetchers[i] = stream.Fetcher(ctx, nil)
   209  			work <- func() error {
   210  				s, err := stream.State(ctx)
   211  				if err != nil {
   212  					return err
   213  				}
   214  				if s.Desc.StreamType != logpb.StreamType_TEXT {
   215  					return fmt.Errorf("log %q is not a text stream; only text streams are currently supported", logs[i].Name)
   216  				}
   217  				m.Chans[i].start = s.Desc.Timestamp.AsTime()
   218  				return err
   219  			}
   220  		}
   221  	})
   222  	if err != nil {
   223  		return err
   224  	}
   225  
   226  	// Start fetching.
   227  	for i := range m.Chans {
   228  		i := i
   229  		c := make(chan logAndErr, 256)
   230  		m.Chans[i].c = c
   231  		go func() {
   232  			defer close(c)
   233  			for {
   234  				log, err := fetchers[i].NextLogEntry()
   235  				c <- logAndErr{log, err}
   236  				if err != nil {
   237  					break
   238  				}
   239  			}
   240  		}()
   241  	}
   242  
   243  	stdout, stderr := newStdioPrinters(r.noColor)
   244  	for {
   245  		chanIndex, entry, err := m.Next()
   246  		out := stdout
   247  		switch {
   248  		case err == io.EOF:
   249  			return nil
   250  		case err != nil:
   251  			return err
   252  		case logs[chanIndex].Name == "stderr":
   253  			out = stderr
   254  		}
   255  
   256  		for _, line := range entry.GetText().GetLines() {
   257  			out.f("%s%s", line.Value, line.Delimiter)
   258  		}
   259  		if out.Err != nil {
   260  			return out.Err
   261  		}
   262  	}
   263  }
   264  
   265  type logChan struct {
   266  	start time.Time      // base time for log entries
   267  	c     chan logAndErr // channel of log entries
   268  }
   269  
   270  type logAndErr struct {
   271  	*logpb.LogEntry
   272  	err error
   273  }
   274  
   275  // logMultiplexer receives from Chans and multiplexes log entries by timestamp.
   276  type logMultiplexer struct {
   277  	Chans []logChan
   278  
   279  	// internal state
   280  
   281  	heads []logAndErr
   282  }
   283  
   284  // Next returns the next log entry.
   285  // Returns io.EOF if there is no next log entry.
   286  func (m *logMultiplexer) Next() (chanIndex int, log *logpb.LogEntry, err error) {
   287  	if m.heads == nil {
   288  		// initialization
   289  		m.heads = make([]logAndErr, len(m.Chans))
   290  		for i := range m.heads {
   291  			m.heads[i] = <-m.Chans[i].c
   292  		}
   293  	}
   294  
   295  	// Choose the earliest one.
   296  	// Note: using heap is overkill here.
   297  	// Most of the time we deal with only two logs.
   298  	earliest := -1
   299  	earliestTime := time.Time{}
   300  	for i, h := range m.heads {
   301  		if h.err == io.EOF {
   302  			continue
   303  		}
   304  		if h.err != nil {
   305  			return 0, nil, h.err
   306  		}
   307  
   308  		curTime := m.Chans[i].start
   309  		if h.TimeOffset != nil {
   310  			if err := h.TimeOffset.CheckValid(); err != nil {
   311  				return 0, nil, err
   312  			}
   313  			offset := h.TimeOffset.AsDuration()
   314  			curTime = curTime.Add(offset)
   315  		}
   316  
   317  		isEarlier := earliest == -1 ||
   318  			curTime.Before(earliestTime) ||
   319  			(curTime.Equal(earliestTime) && h.PrefixIndex < m.heads[earliest].PrefixIndex)
   320  		if isEarlier {
   321  			earliest = i
   322  			earliestTime = curTime
   323  		}
   324  	}
   325  	if earliest == -1 {
   326  		// all finished
   327  		return 0, nil, io.EOF
   328  	}
   329  
   330  	// Call f and replace the entry with the freshest one from the same
   331  	// channel.
   332  	earliestEntry := m.heads[earliest].LogEntry
   333  	m.heads[earliest] = <-m.Chans[earliest].c
   334  	return earliest, earliestEntry, nil
   335  }