github.com/Schaudge/grailbase@v0.0.0-20240223061707-44c758a471c0/limitbuf/limitbuf.go (about) 1 package limitbuf 2 3 import ( 4 "fmt" 5 "strings" 6 7 "github.com/Schaudge/grailbase/log" 8 ) 9 10 type ( 11 // Logger is like strings.Builder, but with maximum length. If the caller tries 12 // to add data beyond the capacity, they will be dropped, and Logger.String() 13 // will append "(truncated)" at the end. 14 // 15 // TODO: Consider renaming to Builder or Buffer since this type's behavior 16 // is analogous to those. 17 Logger struct { 18 maxLen int 19 b strings.Builder 20 21 // seen counts the total number of bytes passed to Write. 22 seen int64 23 logIfTruncatingMaxMultiple float64 24 } 25 26 // LoggerOption is passed to NewLogger to configure a Logger. 27 LoggerOption func(*Logger) 28 ) 29 30 // LogIfTruncatingMaxMultiple controls informative logging about how much data 31 // passed to Write has been truncated. 32 // 33 // If zero, this logging is disabled. Otherwise, if the sum of len(data) 34 // passed to prior Write calls is greater than LogIfTruncatingMaxMultiple * 35 // maxLen (passed to NewLogger), a log message is written in the next call 36 // to String(). After logging, LogIfTruncatingMaxMultiple is set to zero 37 // to avoid repeating the same message. 38 // 39 // This can be a useful diagnostic for both CPU and memory usage if a huge 40 // amount of data is written and only a tiny fraction is used. For example, 41 // if a caller writes to the log with fmt.Fprint(logger, ...) they may 42 // not realize that fmt.Fprint* actually buffers the *entire* formatted 43 // string in memory first, then writes it to logger. 44 // TODO: Consider serving the fmt use case better for e.g. bigslice. 45 // 46 // Note that the log message is written to log.Error, not the Logger itself 47 // (it's not part of String's return). 48 func LogIfTruncatingMaxMultiple(m float64) LoggerOption { 49 return func(l *Logger) { l.logIfTruncatingMaxMultiple = m } 50 } 51 52 // NewLogger creates a new Logger object with the given capacity. 53 func NewLogger(maxLen int, opts ...LoggerOption) *Logger { 54 l := Logger{maxLen: maxLen} 55 for _, opt := range opts { 56 opt(&l) 57 } 58 return &l 59 } 60 61 // Write implements io.Writer interface. 62 func (b *Logger) Write(data []byte) (int, error) { 63 n := b.maxLen - b.b.Len() 64 if n > len(data) { 65 n = len(data) 66 } 67 if n > 0 { 68 b.b.Write(data[:n]) 69 } 70 b.seen += int64(len(data)) 71 return len(data), nil 72 } 73 74 // String reports the data written so far. If the length of the data exceeds the 75 // buffer capacity, the prefix of the data, plus "(truncated)" will be reported. 76 func (b *Logger) String() string { 77 if b.seen <= int64(b.maxLen) { 78 return b.b.String() 79 } 80 // Truncated. 81 if b.logIfTruncatingMaxMultiple > 0 && 82 b.seen > int64(float64(b.maxLen)*b.logIfTruncatingMaxMultiple) { 83 b.logIfTruncatingMaxMultiple = 0 84 log.Errorf("limitbuf: extreme truncation: %d -> %d bytes", b.seen, b.maxLen) 85 } 86 return b.b.String() + fmt.Sprintf("(truncated %d bytes)", b.seen-int64(b.maxLen)) 87 }