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 }