github.com/tilt-dev/tilt@v0.33.15-0.20240515162809-0a22ed45d8a0/pkg/model/logstore/logstore.go (about)

     1  package logstore
     2  
     3  import (
     4  	"fmt"
     5  	"sort"
     6  	"strings"
     7  	"time"
     8  
     9  	"google.golang.org/protobuf/types/known/timestamppb"
    10  
    11  	"github.com/tilt-dev/tilt/pkg/logger"
    12  	"github.com/tilt-dev/tilt/pkg/model"
    13  	"github.com/tilt-dev/tilt/pkg/webview"
    14  )
    15  
    16  // All parts of Tilt should display logs incrementally.
    17  //
    18  // But the initial page load loads all the existing logs.
    19  // https://github.com/tilt-dev/tilt/issues/3359
    20  //
    21  // Until that issue is fixed, we cap the logs at about 2MB.
    22  const defaultMaxLogLengthInBytes = 2 * 1000 * 1000
    23  
    24  const newlineByte = byte('\n')
    25  
    26  type Span struct {
    27  	ManifestName      model.ManifestName
    28  	LastSegmentIndex  int
    29  	FirstSegmentIndex int
    30  }
    31  
    32  func (s *Span) Clone() *Span {
    33  	clone := *s
    34  	return &clone
    35  }
    36  
    37  type SpanID = model.LogSpanID
    38  
    39  type LogSegment struct {
    40  	SpanID SpanID
    41  	Time   time.Time
    42  	Text   []byte
    43  	Level  logger.Level
    44  	Fields logger.Fields
    45  
    46  	// Continues a line from a previous segment.
    47  	ContinuesLine bool
    48  
    49  	// When we store warnings in the LogStore, we break them up into lines and
    50  	// store them as a series of line segments. 'Anchor' marks the beginning of a
    51  	// series of logs that should be kept together.
    52  	//
    53  	// Anchor warning1, line1
    54  	//        warning1, line2
    55  	// Anchor warning2, line1
    56  	Anchor bool
    57  }
    58  
    59  // Whether these two log segments may be printed on the same line
    60  func (l LogSegment) CanContinueLine(other LogSegment) bool {
    61  	return l.SpanID == other.SpanID && l.Level == other.Level
    62  }
    63  
    64  func (l LogSegment) StartsLine() bool {
    65  	return !l.ContinuesLine
    66  }
    67  
    68  func (l LogSegment) IsComplete() bool {
    69  	segmentLen := len(l.Text)
    70  	return segmentLen > 0 && l.Text[segmentLen-1] == newlineByte
    71  }
    72  
    73  func (l LogSegment) Len() int {
    74  	return len(l.Text)
    75  }
    76  
    77  func (l LogSegment) String() string {
    78  	return string(l.Text)
    79  }
    80  
    81  func segmentsFromBytes(spanID SpanID, time time.Time, level logger.Level, fields logger.Fields, bs []byte) []LogSegment {
    82  	segments := []LogSegment{}
    83  	lastBreak := 0
    84  	for i, b := range bs {
    85  		if b == newlineByte {
    86  			segments = append(segments, LogSegment{
    87  				SpanID: spanID,
    88  				Level:  level,
    89  				Time:   time,
    90  				Text:   bs[lastBreak : i+1],
    91  				Fields: fields,
    92  			})
    93  			lastBreak = i + 1
    94  		}
    95  	}
    96  	if lastBreak < len(bs) {
    97  		segments = append(segments, LogSegment{
    98  			SpanID: spanID,
    99  			Level:  level,
   100  			Time:   time,
   101  			Text:   bs[lastBreak:],
   102  			Fields: fields,
   103  		})
   104  	}
   105  	return segments
   106  }
   107  
   108  func linesToString(lines []LogLine) string {
   109  	sb := strings.Builder{}
   110  	for _, line := range lines {
   111  		sb.WriteString(line.Text)
   112  	}
   113  	return sb.String()
   114  }
   115  
   116  type LogEvent interface {
   117  	Message() []byte
   118  	Time() time.Time
   119  	Level() logger.Level
   120  	Fields() logger.Fields
   121  
   122  	// The manifest that this log is associated with.
   123  	ManifestName() model.ManifestName
   124  
   125  	// The SpanID that identifies what Span this is associated with in the LogStore.
   126  	SpanID() SpanID
   127  }
   128  
   129  // An abstract checkpoint in the log store, so we can
   130  // ask questions like "give me all logs since checkpoint X" and
   131  // "scrub everything since checkpoint Y". In practice, this
   132  // is just an index into the segment slice.
   133  type Checkpoint int
   134  
   135  // A central place for storing logs. Not thread-safe.
   136  //
   137  // If you need to read logs in a thread-safe way outside of
   138  // the normal Store state loop, take a look at logstore.Reader.
   139  type LogStore struct {
   140  	// A Span is a grouping of logs by their source.
   141  	// The term "Span" is taken from opentracing, and has similar associations.
   142  	spans map[SpanID]*Span
   143  
   144  	// We store logs as an append-only sequence of segments.
   145  	// Once a segment has been added, it should not be modified.
   146  	segments []LogSegment
   147  
   148  	// The number of bytes stored in this logstore. This is redundant bookkeeping so
   149  	// that we don't need to recompute it each time.
   150  	len int
   151  
   152  	// Used for truncating the log. Set as a property so that we can change it
   153  	// for testing.
   154  	maxLogLengthInBytes int
   155  
   156  	// If the log is truncated, we need to adjust all checkpoints
   157  	checkpointOffset Checkpoint
   158  }
   159  
   160  func NewLogStoreForTesting(msg string) *LogStore {
   161  	s := NewLogStore()
   162  	s.Append(newGlobalTestLogEvent(msg), nil)
   163  	return s
   164  }
   165  
   166  func NewLogStore() *LogStore {
   167  	return &LogStore{
   168  		spans:               make(map[SpanID]*Span),
   169  		segments:            []LogSegment{},
   170  		len:                 0,
   171  		maxLogLengthInBytes: defaultMaxLogLengthInBytes,
   172  	}
   173  }
   174  
   175  func (s *LogStore) Checkpoint() Checkpoint {
   176  	return s.checkpointFromIndex(len(s.segments))
   177  }
   178  
   179  func (s *LogStore) checkpointFromIndex(index int) Checkpoint {
   180  	return Checkpoint(index) + s.checkpointOffset
   181  }
   182  
   183  func (s *LogStore) checkpointToIndex(c Checkpoint) int {
   184  	index := int(c - s.checkpointOffset)
   185  	if index < 0 {
   186  		return 0
   187  	}
   188  	if index > len(s.segments) {
   189  		return len(s.segments)
   190  	}
   191  	return index
   192  }
   193  
   194  // Find the greatest index < index corresponding to a log matching one of the manifests in mns.
   195  // (If mns is empty, all logs match.)
   196  // If no valid i found, return -1
   197  func (s *LogStore) prevIndexMatchingManifests(index int, mns model.ManifestNameSet) int {
   198  	if len(mns) == 0 || index == 0 {
   199  		return index - 1
   200  	}
   201  
   202  	for i := index - 1; i >= 0; i-- {
   203  		span, ok := s.spans[s.segments[i].SpanID]
   204  		if !ok {
   205  			continue
   206  		}
   207  		mn := span.ManifestName
   208  		if mns[mn] {
   209  			return i
   210  		}
   211  	}
   212  	return -1
   213  }
   214  
   215  // Find the greatest index >= index corresponding to a log matching one of the manifests in mns.
   216  // (If mns is empty, all logs match.)
   217  // If no valid i found, return -1
   218  func (s *LogStore) nextIndexMatchingManifests(index int, mns model.ManifestNameSet) int {
   219  	if len(mns) == 0 {
   220  		return index
   221  	}
   222  
   223  	for i := index; i < len(s.segments); i++ {
   224  		span, ok := s.spans[s.segments[i].SpanID]
   225  		if !ok {
   226  			continue
   227  		}
   228  		mn := span.ManifestName
   229  		if mns[mn] {
   230  			return i
   231  		}
   232  	}
   233  	return -1
   234  }
   235  
   236  func (s *LogStore) ScrubSecretsStartingAt(secrets model.SecretSet, checkpoint Checkpoint) {
   237  	index := s.checkpointToIndex(checkpoint)
   238  	for i := index; i < len(s.segments); i++ {
   239  		s.segments[i].Text = secrets.Scrub(s.segments[i].Text)
   240  	}
   241  
   242  	s.len = s.computeLen()
   243  }
   244  
   245  func (s *LogStore) Append(le LogEvent, secrets model.SecretSet) {
   246  	spanID := le.SpanID()
   247  	if spanID == "" && le.ManifestName() != "" {
   248  		spanID = SpanID(fmt.Sprintf("unknown:%s", le.ManifestName()))
   249  	}
   250  	span, ok := s.spans[spanID]
   251  	if !ok {
   252  		span = &Span{
   253  			ManifestName:      le.ManifestName(),
   254  			LastSegmentIndex:  -1,
   255  			FirstSegmentIndex: len(s.segments),
   256  		}
   257  		s.spans[spanID] = span
   258  	}
   259  
   260  	msg := secrets.Scrub(le.Message())
   261  	added := segmentsFromBytes(spanID, le.Time(), le.Level(), le.Fields(), msg)
   262  	if len(added) == 0 {
   263  		return
   264  	}
   265  
   266  	level := le.Level()
   267  	if level.AsSevereAs(logger.WarnLvl) {
   268  		added[0].Anchor = true
   269  	}
   270  
   271  	added[0].ContinuesLine = s.computeContinuesLine(added[0], span)
   272  
   273  	s.segments = append(s.segments, added...)
   274  	span.LastSegmentIndex = len(s.segments) - 1
   275  
   276  	s.len += len(msg)
   277  	s.ensureMaxLength()
   278  }
   279  
   280  func (s *LogStore) Empty() bool {
   281  	return len(s.segments) == 0
   282  }
   283  
   284  // Get at most N lines from the tail of the log.
   285  func (s *LogStore) Tail(n int) string {
   286  	return s.tailHelper(n, s.spans, true)
   287  }
   288  
   289  // Get at most N lines from the tail of the span.
   290  func (s *LogStore) TailSpan(n int, spanID SpanID) string {
   291  	spans, ok := s.idToSpanMap(spanID)
   292  	if !ok {
   293  		return ""
   294  	}
   295  	return s.tailHelper(n, spans, false)
   296  }
   297  
   298  // Get at most N lines from the tail of the log.
   299  func (s *LogStore) tailHelper(n int, spans map[SpanID]*Span, showManifestPrefix bool) string {
   300  	if n <= 0 {
   301  		return ""
   302  	}
   303  
   304  	// Traverse backwards until we have n lines.
   305  	remaining := n
   306  	startIndex, lastIndex := s.startAndLastIndices(spans)
   307  	if startIndex == -1 {
   308  		return ""
   309  	}
   310  
   311  	current := lastIndex
   312  	for ; current >= startIndex; current-- {
   313  		segment := s.segments[current]
   314  		if _, ok := spans[segment.SpanID]; !ok {
   315  			continue
   316  		}
   317  
   318  		if segment.StartsLine() {
   319  			remaining--
   320  			if remaining <= 0 {
   321  				break
   322  			}
   323  		}
   324  	}
   325  
   326  	if remaining > 0 {
   327  		// If there aren't enough lines, just return the whole store.
   328  		return s.toLogString(logOptions{
   329  			spans:              spans,
   330  			showManifestPrefix: showManifestPrefix,
   331  		})
   332  	}
   333  
   334  	startedSpans := make(map[SpanID]bool)
   335  	newSegments := []LogSegment{}
   336  	for i := current; i <= lastIndex; i++ {
   337  		segment := s.segments[i]
   338  		spanID := segment.SpanID
   339  		if _, ok := spans[segment.SpanID]; !ok {
   340  			continue
   341  		}
   342  
   343  		if !segment.StartsLine() && !startedSpans[spanID] {
   344  			// Skip any segments that start on lines from before the Tail started.
   345  			continue
   346  		}
   347  		newSegments = append(newSegments, segment)
   348  		startedSpans[spanID] = true
   349  	}
   350  
   351  	tempStore := &LogStore{spans: s.cloneSpanMap(), segments: newSegments}
   352  	tempStore.recomputeDerivedValues()
   353  	return tempStore.toLogString(logOptions{
   354  		spans:              tempStore.spans,
   355  		showManifestPrefix: showManifestPrefix,
   356  	})
   357  }
   358  
   359  func (s *LogStore) cloneSpanMap() map[SpanID]*Span {
   360  	newSpans := make(map[SpanID]*Span, len(s.spans))
   361  	for spanID, span := range s.spans {
   362  		newSpans[spanID] = span.Clone()
   363  	}
   364  	return newSpans
   365  }
   366  
   367  func (s *LogStore) computeContinuesLine(seg LogSegment, span *Span) bool {
   368  	if span.LastSegmentIndex == -1 {
   369  		return false
   370  	} else {
   371  		lastSeg := s.segments[span.LastSegmentIndex]
   372  		if lastSeg.IsComplete() {
   373  			return false
   374  		}
   375  		if !lastSeg.CanContinueLine(seg) {
   376  			return false
   377  		}
   378  	}
   379  
   380  	return true
   381  }
   382  
   383  func (s *LogStore) recomputeDerivedValues() {
   384  	s.len = s.computeLen()
   385  
   386  	// Reset the last segment index so we can rebuild them from scratch.
   387  	for _, span := range s.spans {
   388  		span.FirstSegmentIndex = -1
   389  		span.LastSegmentIndex = -1
   390  	}
   391  
   392  	// Rebuild information about line continuations.
   393  	for i, segment := range s.segments {
   394  		spanID := segment.SpanID
   395  		span := s.spans[spanID]
   396  		if span.FirstSegmentIndex == -1 {
   397  			span.FirstSegmentIndex = i
   398  		}
   399  
   400  		s.segments[i].ContinuesLine = s.computeContinuesLine(segment, span)
   401  		span.LastSegmentIndex = i
   402  	}
   403  
   404  	for spanID, span := range s.spans {
   405  		if span.FirstSegmentIndex == -1 {
   406  			delete(s.spans, spanID)
   407  		}
   408  	}
   409  }
   410  
   411  // Returns logs incrementally from the given checkpoint.
   412  //
   413  // In many use cases, logs are printed to an append-only stream (like os.Stdout).
   414  // Once they've been printed, they can't be called back.
   415  // ContinuingString() tries to make reasonable product decisions about printing
   416  // all the logs that have streamed in since the given checkpoint.
   417  //
   418  // Typical usage, looks like:
   419  //
   420  // Print(store.ContinuingString(state.LastCheckpoint))
   421  // state.LastCheckpoint = store.Checkpoint()
   422  func (s *LogStore) ContinuingString(checkpoint Checkpoint) string {
   423  	return s.ContinuingStringWithOptions(checkpoint, LineOptions{})
   424  }
   425  
   426  func (s *LogStore) ContinuingStringWithOptions(checkpoint Checkpoint, opts LineOptions) string {
   427  	lines := s.ContinuingLinesWithOptions(checkpoint, opts)
   428  	sb := strings.Builder{}
   429  	for _, line := range lines {
   430  		sb.WriteString(line.Text)
   431  	}
   432  	return sb.String()
   433  }
   434  
   435  func (s *LogStore) IsLastSegmentUncompleted() bool {
   436  	if len(s.segments) == 0 {
   437  		return false
   438  	}
   439  	lastSegment := s.segments[len(s.segments)-1]
   440  	return !lastSegment.IsComplete()
   441  }
   442  
   443  func (s *LogStore) ContinuingLines(checkpoint Checkpoint) []LogLine {
   444  	return s.ContinuingLinesWithOptions(checkpoint, LineOptions{})
   445  }
   446  
   447  func (s *LogStore) ContinuingLinesWithOptions(checkpoint Checkpoint, opts LineOptions) []LogLine {
   448  	isSameSpanContinuation := false
   449  	isDifferentSpan := false
   450  	checkpointIndex := s.checkpointToIndex(checkpoint)
   451  	precedingIndexToPrint := s.prevIndexMatchingManifests(checkpointIndex, opts.ManifestNames)
   452  	nextIndexToPrint := s.nextIndexMatchingManifests(checkpointIndex, opts.ManifestNames)
   453  	var precedingSegment = LogSegment{}
   454  
   455  	if precedingIndexToPrint >= 0 && nextIndexToPrint < len(s.segments) && nextIndexToPrint > 0 {
   456  		// Check the last thing we printed. If it wasn't complete,
   457  		// we have to do some extra work to properly continue the previous print.
   458  		precedingSegment = s.segments[precedingIndexToPrint]
   459  		nextSegment := s.segments[nextIndexToPrint]
   460  		if !precedingSegment.IsComplete() {
   461  			// If this is the same span id, remove the prefix from this line.
   462  			if precedingSegment.CanContinueLine(nextSegment) {
   463  				isSameSpanContinuation = true
   464  			} else {
   465  				// Otherwise, it's from a different span, which means we want a
   466  				// newline after the segment we just printed (even if that segment
   467  				// isn't technically "complete")
   468  				isDifferentSpan = true
   469  			}
   470  		}
   471  	}
   472  
   473  	// --> if checkpointIndex == len(s.segments)
   474  	// nothing new to print, return!
   475  
   476  	tempSegments := s.segments[checkpointIndex:]
   477  	tempLogStore := &LogStore{
   478  		spans:    s.cloneSpanMap(),
   479  		segments: tempSegments,
   480  	}
   481  	tempLogStore.recomputeDerivedValues()
   482  
   483  	spans := tempLogStore.spans
   484  	if len(opts.ManifestNames) != 0 {
   485  		spans = tempLogStore.spansForManifests(opts.ManifestNames)
   486  	}
   487  	result := tempLogStore.toLogLines(logOptions{
   488  		spans:                       spans,
   489  		showManifestPrefix:          !opts.SuppressPrefix,
   490  		skipFirstLineManifestPrefix: isSameSpanContinuation,
   491  	})
   492  
   493  	if isSameSpanContinuation {
   494  		return result
   495  	}
   496  	if isDifferentSpan {
   497  		return append([]LogLine{
   498  			LogLine{
   499  				Text:              "\n",
   500  				SpanID:            precedingSegment.SpanID,
   501  				ProgressID:        precedingSegment.Fields[logger.FieldNameProgressID],
   502  				ProgressMustPrint: precedingSegment.Fields[logger.FieldNameProgressMustPrint] == "1",
   503  				Time:              precedingSegment.Time,
   504  			},
   505  		}, result...)
   506  	}
   507  	return result
   508  }
   509  
   510  func (s *LogStore) ToLogList(fromCheckpoint Checkpoint) (*webview.LogList, error) {
   511  	spans := make(map[string]*webview.LogSpan, len(s.spans))
   512  	for spanID, span := range s.spans {
   513  		spans[string(spanID)] = &webview.LogSpan{
   514  			ManifestName: span.ManifestName.String(),
   515  		}
   516  	}
   517  
   518  	startIndex := s.checkpointToIndex(fromCheckpoint)
   519  	if startIndex >= len(s.segments) {
   520  		// No logs to send down.
   521  		return &webview.LogList{
   522  			FromCheckpoint: -1,
   523  			ToCheckpoint:   -1,
   524  		}, nil
   525  	}
   526  
   527  	segments := make([]*webview.LogSegment, 0, len(s.segments)-startIndex)
   528  	for i := startIndex; i < len(s.segments); i++ {
   529  		segment := s.segments[i]
   530  		time := timestamppb.New(segment.Time)
   531  		segments = append(segments, &webview.LogSegment{
   532  			SpanId: string(segment.SpanID),
   533  			Level:  webview.LogLevel(segment.Level.ToProtoID()),
   534  			Time:   time,
   535  			Text:   string(segment.Text),
   536  			Anchor: segment.Anchor,
   537  			Fields: segment.Fields,
   538  		})
   539  	}
   540  
   541  	return &webview.LogList{
   542  		Spans:          spans,
   543  		Segments:       segments,
   544  		FromCheckpoint: int32(s.checkpointFromIndex(startIndex)),
   545  		ToCheckpoint:   int32(s.Checkpoint()),
   546  	}, nil
   547  }
   548  
   549  func (s *LogStore) String() string {
   550  	return s.toLogString(logOptions{
   551  		spans:              s.spans,
   552  		showManifestPrefix: true,
   553  	})
   554  }
   555  
   556  func (s *LogStore) spansForManifest(mn model.ManifestName) map[SpanID]*Span {
   557  	result := make(map[SpanID]*Span)
   558  	for spanID, span := range s.spans {
   559  		if span.ManifestName == mn {
   560  			result[spanID] = span
   561  		}
   562  	}
   563  	return result
   564  }
   565  
   566  func (s *LogStore) spansForManifests(mnSet model.ManifestNameSet) map[SpanID]*Span {
   567  	result := make(map[SpanID]*Span)
   568  	for spanID, span := range s.spans {
   569  		if mnSet[span.ManifestName] {
   570  			result[spanID] = span
   571  		}
   572  	}
   573  
   574  	return result
   575  }
   576  
   577  func (s *LogStore) idToSpanMap(spanID SpanID) (map[SpanID]*Span, bool) {
   578  	spans := make(map[SpanID]*Span, 1)
   579  	span, ok := s.spans[spanID]
   580  	if !ok {
   581  		return nil, false
   582  	}
   583  	spans[spanID] = span
   584  	return spans, true
   585  }
   586  
   587  func (s *LogStore) SpanLog(spanID SpanID) string {
   588  	spans, ok := s.idToSpanMap(spanID)
   589  	if !ok {
   590  		return ""
   591  	}
   592  	return s.toLogString(logOptions{spans: spans})
   593  }
   594  
   595  func (s *LogStore) Warnings(spanID SpanID) []string {
   596  	spans, ok := s.idToSpanMap(spanID)
   597  	if !ok {
   598  		return nil
   599  	}
   600  
   601  	startIndex, lastIndex := s.startAndLastIndices(spans)
   602  	if startIndex == -1 {
   603  		return nil
   604  	}
   605  
   606  	result := []string{}
   607  	sb := strings.Builder{}
   608  	for i := startIndex; i <= lastIndex; i++ {
   609  		segment := s.segments[i]
   610  		if segment.Level != logger.WarnLvl || spanID != segment.SpanID {
   611  			continue
   612  		}
   613  
   614  		if segment.Anchor && sb.Len() > 0 {
   615  			result = append(result, sb.String())
   616  			sb = strings.Builder{}
   617  		}
   618  
   619  		sb.WriteString(string(segment.Text))
   620  	}
   621  
   622  	if sb.Len() > 0 {
   623  		result = append(result, sb.String())
   624  	}
   625  	return result
   626  }
   627  
   628  func (s *LogStore) ManifestLog(mn model.ManifestName) string {
   629  	spans := s.spansForManifest(mn)
   630  	return s.toLogString(logOptions{spans: spans})
   631  }
   632  
   633  func (s *LogStore) startAndLastIndices(spans map[SpanID]*Span) (startIndex, lastIndex int) {
   634  	earliestStartIndex := -1
   635  	latestEndIndex := -1
   636  	for _, span := range spans {
   637  		if earliestStartIndex == -1 || span.FirstSegmentIndex < earliestStartIndex {
   638  			earliestStartIndex = span.FirstSegmentIndex
   639  		}
   640  		if latestEndIndex == -1 || span.LastSegmentIndex > latestEndIndex {
   641  			latestEndIndex = span.LastSegmentIndex
   642  		}
   643  	}
   644  
   645  	if earliestStartIndex == -1 {
   646  		return -1, -1
   647  	}
   648  
   649  	startIndex = earliestStartIndex
   650  	lastIndex = latestEndIndex
   651  	return startIndex, lastIndex
   652  }
   653  
   654  type logOptions struct {
   655  	spans                       map[SpanID]*Span // only print logs for these spans
   656  	showManifestPrefix          bool
   657  	skipFirstLineManifestPrefix bool
   658  }
   659  
   660  type LineOptions struct {
   661  	ManifestNames  model.ManifestNameSet // only print logs for these manifests
   662  	SuppressPrefix bool
   663  }
   664  
   665  func (s *LogStore) toLogString(options logOptions) string {
   666  	return linesToString(s.toLogLines(options))
   667  }
   668  
   669  // Returns a sequence of lines, including trailing newlines.
   670  func (s *LogStore) toLogLines(options logOptions) []LogLine {
   671  	result := []LogLine{}
   672  	var lineBuilder *logLineBuilder
   673  
   674  	var consumeLineBuilder = func() {
   675  		if lineBuilder == nil {
   676  			return
   677  		}
   678  		result = append(result, lineBuilder.build(options)...)
   679  		lineBuilder = nil
   680  	}
   681  
   682  	// We want to print the log line-by-line, but we don't actually store the logs
   683  	// line-by-line. We store them as segments.
   684  	//
   685  	// This means we need to:
   686  	// 1) At segment x,
   687  	// 2) If x starts a new line, print it, then run ahead to print the rest of the line
   688  	//    until the entire line is consumed.
   689  	// 3) If x does not start a new line, skip it, because we assume it was handled
   690  	//    in a previous line.
   691  	//
   692  	// This can have some O(n^2) perf characteristics in the worst case, but
   693  	// for normal inputs should be fine.
   694  	startIndex, lastIndex := s.startAndLastIndices(options.spans)
   695  	if startIndex == -1 {
   696  		return nil
   697  	}
   698  
   699  	isFirstLine := true
   700  	for i := startIndex; i <= lastIndex; i++ {
   701  		segment := s.segments[i]
   702  		if !segment.StartsLine() {
   703  			continue
   704  		}
   705  
   706  		spanID := segment.SpanID
   707  		span := s.spans[spanID]
   708  		if _, ok := options.spans[spanID]; !ok {
   709  			continue
   710  		}
   711  
   712  		// If the last segment never completed, print a newline now, so that the
   713  		// logs from different sources don't blend together.
   714  		if lineBuilder != nil {
   715  			lineBuilder.needsTrailingNewline = true
   716  			consumeLineBuilder()
   717  		}
   718  
   719  		lineBuilder = newLogLineBuilder(span, segment, isFirstLine)
   720  		isFirstLine = false
   721  
   722  		// If this segment is not complete, run ahead and try to complete it.
   723  		if lineBuilder.isComplete() {
   724  			consumeLineBuilder()
   725  			continue
   726  		}
   727  
   728  		for currentIndex := i + 1; currentIndex <= span.LastSegmentIndex; currentIndex++ {
   729  			currentSeg := s.segments[currentIndex]
   730  			if currentSeg.SpanID != spanID {
   731  				continue
   732  			}
   733  
   734  			if !currentSeg.CanContinueLine(lineBuilder.lastSegment()) {
   735  				break
   736  			}
   737  
   738  			lineBuilder.addSegment(currentSeg)
   739  			if lineBuilder.isComplete() {
   740  				consumeLineBuilder()
   741  				break
   742  			}
   743  		}
   744  	}
   745  
   746  	consumeLineBuilder()
   747  	return result
   748  }
   749  
   750  func (s *LogStore) computeLen() int {
   751  	result := 0
   752  	for _, segment := range s.segments {
   753  		result += segment.Len()
   754  	}
   755  	return result
   756  }
   757  
   758  // After a log hits its limit, we need to truncate it to keep it small
   759  // we do this by cutting a big chunk at a time, so that we have rarer, larger changes, instead of
   760  // a small change every time new data is written to the log
   761  // https://github.com/tilt-dev/tilt/issues/1935#issuecomment-531390353
   762  func (s *LogStore) logTruncationTarget() int {
   763  	return s.maxLogLengthInBytes / 2
   764  }
   765  
   766  func (s *LogStore) ensureMaxLength() {
   767  	if s.len <= s.maxLogLengthInBytes {
   768  		return
   769  	}
   770  
   771  	manifestWeightMap := s.createManifestWeightMap()
   772  
   773  	// Next, repeatedly cut the longest manifest in half until
   774  	// we've reached the target number of bytes to cut.
   775  	leftToCut := s.len - s.logTruncationTarget()
   776  	for leftToCut > 0 {
   777  		mn := manifestWeightMap.heaviest()
   778  		byteCount := manifestWeightMap[mn].byteCount
   779  		amountToCut := byteCount - (byteCount / 2) // ceiling(byteCount/2)
   780  		if amountToCut > leftToCut {
   781  			amountToCut = leftToCut
   782  		}
   783  		leftToCut -= amountToCut
   784  
   785  		// A better algorithm would also update the start time, but
   786  		// this is hard to compute without truncating first.
   787  		manifestWeightMap[mn].byteCount -= amountToCut
   788  	}
   789  
   790  	// Lastly, go through all the segments, and truncate the manifests
   791  	// where we said we would.
   792  	newSegments := make([]LogSegment, 0, len(s.segments)/2)
   793  	trimmedSegmentCount := 0
   794  	for i := len(s.segments) - 1; i >= 0; i-- {
   795  		segment := s.segments[i]
   796  		mn := s.spans[segment.SpanID].ManifestName
   797  		manifestWeightMap[mn].byteCount -= segment.Len()
   798  		if manifestWeightMap[mn].byteCount < 0 {
   799  			trimmedSegmentCount++
   800  			continue
   801  		}
   802  
   803  		newSegments = append(newSegments, segment)
   804  	}
   805  
   806  	reverseLogSegments(newSegments)
   807  	s.checkpointOffset += Checkpoint(trimmedSegmentCount)
   808  	s.segments = newSegments
   809  	s.recomputeDerivedValues()
   810  }
   811  
   812  // Count the number of bytes and start time in each manifest.
   813  func (s *LogStore) createManifestWeightMap() manifestWeightMap {
   814  	manifestWeightMap := manifestWeightMap{}
   815  	for _, segment := range s.segments {
   816  		mn := s.spans[segment.SpanID].ManifestName
   817  		weight, ok := manifestWeightMap[mn]
   818  		if !ok {
   819  			weight = &manifestWeight{name: mn, byteCount: 0, start: segment.Time}
   820  			manifestWeightMap[mn] = weight
   821  		}
   822  		weight.byteCount += segment.Len()
   823  	}
   824  	return manifestWeightMap
   825  }
   826  
   827  // https://github.com/golang/go/wiki/SliceTricks#reversing
   828  func reverseLogSegments(a []LogSegment) {
   829  	for i := len(a)/2 - 1; i >= 0; i-- {
   830  		opp := len(a) - 1 - i
   831  		a[i], a[opp] = a[opp], a[i]
   832  	}
   833  }
   834  
   835  type manifestWeight struct {
   836  	name      model.ManifestName
   837  	byteCount int
   838  	start     time.Time
   839  }
   840  
   841  // Helper struct to find the manifest with the most logs.
   842  type manifestWeightMap map[model.ManifestName]*manifestWeight
   843  
   844  // There are 3 types of logs we need to consider:
   845  // 1) Jobs that print short, critical information at the start.
   846  // 2) Jobs that print lots of health checks continuously.
   847  // 3) Jobs that print recent test results.
   848  //
   849  // Truncating purely on recency would be bad for (1).
   850  // Truncating purely on length would be bad for (3).
   851  //
   852  // So we weight based on both recency and length.
   853  func (s manifestWeightMap) heaviest() model.ManifestName {
   854  	weightsByTime := []*manifestWeight{}
   855  	for _, v := range s {
   856  		weightsByTime = append(weightsByTime, v)
   857  	}
   858  
   859  	// Sort manifests by most recent first.
   860  	sort.Slice(weightsByTime, func(i, j int) bool {
   861  		return weightsByTime[i].start.After(weightsByTime[j].start)
   862  	})
   863  
   864  	heaviest := model.ManifestName("")
   865  	heaviestValue := -1
   866  	for i, weight := range weightsByTime {
   867  		// We compute: weightValue = order * byteCount where the manifest with
   868  		// most recent logs has order 1, the next one has order 2, and so on.
   869  		//
   870  		// This helps ensures older logs get truncated first.
   871  		order := i + 1
   872  		weightValue := order * weight.byteCount
   873  		if weightValue > heaviestValue {
   874  			heaviest = weight.name
   875  			heaviestValue = weightValue
   876  		}
   877  	}
   878  
   879  	return heaviest
   880  }