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 }