github.com/grahambrereton-form3/tilt@v0.10.18/pkg/model/log.go (about)

     1  package model
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  	"strings"
     7  	"time"
     8  )
     9  
    10  // At this limit, with one resource having a 120k log, render time was ~20ms and CPU usage was ~70% on an MBP.
    11  // 70% still isn't great when tilt doesn't really have any necessary work to do, but at least it's usable.
    12  // A render time of ~40ms was about when the interface started being noticeably laggy to me.
    13  const maxLogLengthInBytes = 120 * 1000
    14  
    15  // After a log hits its limit, we need to truncate it to keep it small
    16  // we do this by cutting a big chunk at a time, so that we have rarer, larger changes, instead of
    17  // a small change every time new data is written to the log
    18  // https://github.com/windmilleng/tilt/issues/1935#issuecomment-531390353
    19  const logTruncationTarget = maxLogLengthInBytes / 2
    20  
    21  const newlineByte = byte('\n')
    22  
    23  // All LogLines should end in a \n to be considered "complete".
    24  // We expect this will have more metadata over time about where the line came from.
    25  type logLine []byte
    26  
    27  func (l logLine) IsComplete() bool {
    28  	lineLen := len(l)
    29  	return lineLen > 0 && l[lineLen-1] == newlineByte
    30  }
    31  
    32  func (l logLine) Len() int {
    33  	return len(l)
    34  }
    35  
    36  func (l logLine) String() string {
    37  	return string(l)
    38  }
    39  
    40  func linesFromString(s string) []logLine {
    41  	return linesFromBytes([]byte(s))
    42  }
    43  
    44  func linesFromBytes(bs []byte) []logLine {
    45  	lines := []logLine{}
    46  	lastBreak := 0
    47  	for i, b := range bs {
    48  		if b == newlineByte {
    49  			lines = append(lines, bs[lastBreak:i+1])
    50  			lastBreak = i + 1
    51  		}
    52  	}
    53  	if lastBreak < len(bs) {
    54  		lines = append(lines, bs[lastBreak:])
    55  	}
    56  	return lines
    57  }
    58  
    59  type Log struct {
    60  	lines []logLine
    61  }
    62  
    63  func NewLog(s string) Log {
    64  	return Log{lines: linesFromString(s)}
    65  }
    66  
    67  // Get at most N lines from the tail of the log.
    68  func (l Log) Tail(n int) Log {
    69  	if len(l.lines) <= n {
    70  		return l
    71  	}
    72  	return Log{lines: l.lines[len(l.lines)-n:]}
    73  }
    74  
    75  func (l Log) MarshalJSON() ([]byte, error) {
    76  	return json.Marshal(l.String())
    77  }
    78  
    79  func (l *Log) UnmarshalJSON(data []byte) error {
    80  	var s string
    81  	err := json.Unmarshal(data, &s)
    82  	if err != nil {
    83  		return err
    84  	}
    85  	l.lines = linesFromString(s)
    86  	return nil
    87  }
    88  
    89  func (l Log) ScrubSecretsStartingAt(secrets SecretSet, index int) {
    90  	for i := index; i < len(l.lines); i++ {
    91  		l.lines[i] = secrets.Scrub(l.lines[i])
    92  	}
    93  }
    94  
    95  func (l Log) LineCount() int {
    96  	return len(l.lines)
    97  }
    98  
    99  func (l Log) Len() int {
   100  	result := 0
   101  	for _, line := range l.lines {
   102  		result += len(line)
   103  	}
   104  	return result
   105  }
   106  
   107  func (l Log) String() string {
   108  	lines := make([]string, len(l.lines))
   109  	for i, line := range l.lines {
   110  		lines[i] = line.String()
   111  	}
   112  	return strings.Join(lines, "")
   113  }
   114  
   115  func (l Log) Empty() bool {
   116  	return l.Len() == 0
   117  }
   118  
   119  func TimestampPrefix(ts time.Time) []byte {
   120  	t := ts.Format("2006/01/02 15:04:05")
   121  	return []byte(fmt.Sprintf("%s ", t))
   122  }
   123  
   124  // Returns a new instance of `Log` with content equal to `b` appended to the end of `l`
   125  // Performs truncation off the start of the log (at a newline) to ensure the resulting log is not
   126  // longer than `maxLogLengthInBytes`. (which maybe means a pedant would say this isn't strictly an `append`?)
   127  func AppendLog(l Log, le LogEvent, timestampsEnabled bool, prefix string, secrets SecretSet) Log {
   128  	msg := secrets.Scrub(le.Message())
   129  	isStartingNewLine := len(l.lines) == 0 || l.lines[len(l.lines)-1].IsComplete()
   130  	addedLines := linesFromBytes(msg)
   131  	if len(addedLines) == 0 {
   132  		return l
   133  	}
   134  
   135  	if timestampsEnabled {
   136  		ts := le.Time()
   137  		for i, line := range addedLines {
   138  			if i != 0 || isStartingNewLine {
   139  				addedLines[i] = append(TimestampPrefix(ts), line...)
   140  			}
   141  		}
   142  	}
   143  
   144  	if len(prefix) > 0 {
   145  		for i, line := range addedLines {
   146  			if i != 0 || isStartingNewLine {
   147  				addedLines[i] = append([]byte(prefix), line...)
   148  			}
   149  		}
   150  	}
   151  
   152  	var newLines []logLine
   153  	if isStartingNewLine {
   154  		newLines = append(l.lines, addedLines...)
   155  	} else {
   156  		lastIndex := len(l.lines) - 1
   157  		newLastLine := append(l.lines[lastIndex], addedLines[0]...)
   158  
   159  		// We have to be a bit careful here to avoid mutating the original Log struct.
   160  		newLines = append(l.lines[0:lastIndex], newLastLine)
   161  		newLines = append(newLines, addedLines[1:]...)
   162  	}
   163  
   164  	return Log{ensureMaxLength(newLines)}
   165  }
   166  
   167  type LogEvent interface {
   168  	Message() []byte
   169  	Time() time.Time
   170  }
   171  
   172  func ensureMaxLength(lines []logLine) []logLine {
   173  	bytesSpent := 0
   174  	truncationIndex := -1
   175  	for i := len(lines) - 1; i >= 0; i-- {
   176  		line := lines[i]
   177  		bytesSpent += line.Len()
   178  		if truncationIndex == -1 && bytesSpent > logTruncationTarget {
   179  			truncationIndex = i + 1
   180  		}
   181  		if bytesSpent > maxLogLengthInBytes {
   182  			return lines[truncationIndex:]
   183  		}
   184  	}
   185  
   186  	return lines
   187  }