github.com/buildpacks/pack@v0.33.3-0.20240516162812-884dd1837311/pkg/logging/logger_writers.go (about) 1 // Package logging implements the logger for the pack CLI. 2 package logging 3 4 import ( 5 "fmt" 6 "io" 7 "regexp" 8 "sync" 9 "time" 10 11 "github.com/apex/log" 12 "github.com/heroku/color" 13 14 "github.com/buildpacks/pack/internal/style" 15 ) 16 17 const ( 18 errorLevelText = "ERROR: " 19 warnLevelText = "Warning: " 20 lineFeed = '\n' 21 // log level to use when quiet is true 22 quietLevel = log.WarnLevel 23 // log level to use when debug is true 24 verboseLevel = log.DebugLevel 25 // time format the out logging uses 26 timeFmt = "2006/01/02 15:04:05.000000" 27 // InvalidFileDescriptor based on https://golang.org/src/os/file_unix.go?s=2183:2210#L57 28 InvalidFileDescriptor = ^(uintptr(0)) 29 ) 30 31 var colorCodeMatcher = regexp.MustCompile(`\x1b\[[0-9;]*m`) 32 33 var _ Logger = (*LogWithWriters)(nil) 34 35 // LogWithWriters is a logger used with the pack CLI, allowing users to print logs for various levels, including Info, Debug and Error 36 type LogWithWriters struct { 37 sync.Mutex 38 log.Logger 39 wantTime bool 40 clock func() time.Time 41 out io.Writer 42 errOut io.Writer 43 } 44 45 // NewLogWithWriters creates a logger to be used with pack CLI. 46 func NewLogWithWriters(stdout, stderr io.Writer, opts ...func(*LogWithWriters)) *LogWithWriters { 47 lw := &LogWithWriters{ 48 Logger: log.Logger{ 49 Level: log.InfoLevel, 50 }, 51 wantTime: false, 52 clock: time.Now, 53 out: stdout, 54 errOut: stderr, 55 } 56 lw.Logger.Handler = lw 57 58 for _, opt := range opts { 59 opt(lw) 60 } 61 62 return lw 63 } 64 65 // WithClock is an option used to initialize a LogWithWriters with a given clock function 66 func WithClock(clock func() time.Time) func(writers *LogWithWriters) { 67 return func(logger *LogWithWriters) { 68 logger.clock = clock 69 } 70 } 71 72 // WithVerbose is an option used to initialize a LogWithWriters with Verbose turned on 73 func WithVerbose() func(writers *LogWithWriters) { 74 return func(logger *LogWithWriters) { 75 logger.Level = log.DebugLevel 76 } 77 } 78 79 // HandleLog handles log events, printing entries appropriately 80 func (lw *LogWithWriters) HandleLog(e *log.Entry) error { 81 lw.Lock() 82 defer lw.Unlock() 83 84 writer := lw.WriterForLevel(Level(e.Level)) 85 _, err := fmt.Fprint(writer, appendMissingLineFeed(fmt.Sprintf("%s%s", formatLevel(e.Level), e.Message))) 86 87 return err 88 } 89 90 // WriterForLevel returns a Writer for the given Level 91 func (lw *LogWithWriters) WriterForLevel(level Level) io.Writer { 92 if lw.Level > log.Level(level) { 93 return io.Discard 94 } 95 96 if level == ErrorLevel { 97 return newLogWriter(lw.errOut, lw.clock, lw.wantTime) 98 } 99 100 return newLogWriter(lw.out, lw.clock, lw.wantTime) 101 } 102 103 // Writer returns the base Writer for the LogWithWriters 104 func (lw *LogWithWriters) Writer() io.Writer { 105 return lw.out 106 } 107 108 // WantTime turns timestamps on in log entries 109 func (lw *LogWithWriters) WantTime(f bool) { 110 lw.wantTime = f 111 } 112 113 // WantQuiet reduces the number of logs returned 114 func (lw *LogWithWriters) WantQuiet(f bool) { 115 if f { 116 lw.Level = quietLevel 117 } 118 } 119 120 // WantVerbose increases the number of logs returned 121 func (lw *LogWithWriters) WantVerbose(f bool) { 122 if f { 123 lw.Level = verboseLevel 124 } 125 } 126 127 // IsVerbose returns whether verbose logging is on 128 func (lw *LogWithWriters) IsVerbose() bool { 129 return lw.Level == log.DebugLevel 130 } 131 132 func formatLevel(ll log.Level) string { 133 switch ll { 134 case log.ErrorLevel: 135 return style.Error(errorLevelText) 136 case log.WarnLevel: 137 return style.Warn(warnLevelText) 138 } 139 140 return "" 141 } 142 143 // Preserve behavior of other loggers 144 func appendMissingLineFeed(msg string) string { 145 buff := []byte(msg) 146 if len(buff) == 0 || buff[len(buff)-1] != lineFeed { 147 buff = append(buff, lineFeed) 148 } 149 return string(buff) 150 } 151 152 // logWriter is a writer used for logs 153 type logWriter struct { 154 sync.Mutex 155 out io.Writer 156 clock func() time.Time 157 wantTime bool 158 wantNoColor bool 159 } 160 161 func newLogWriter(writer io.Writer, clock func() time.Time, wantTime bool) *logWriter { 162 wantNoColor := !color.Enabled() 163 return &logWriter{ 164 out: writer, 165 clock: clock, 166 wantTime: wantTime, 167 wantNoColor: wantNoColor, 168 } 169 } 170 171 // Write writes a message prepended by the time to the set io.Writer 172 func (lw *logWriter) Write(buf []byte) (n int, err error) { 173 lw.Lock() 174 defer lw.Unlock() 175 176 length := len(buf) 177 if lw.wantNoColor { 178 buf = stripColor(buf) 179 } 180 181 prefix := "" 182 if lw.wantTime { 183 prefix = fmt.Sprintf("%s ", lw.clock().Format(timeFmt)) 184 } 185 186 _, err = fmt.Fprintf(lw.out, "%s%s", prefix, buf) 187 return length, err 188 } 189 190 // Writer returns the base Writer for the logWriter 191 func (lw *logWriter) Writer() io.Writer { 192 return lw.out 193 } 194 195 // Fd returns the file descriptor of the writer. This is used to ensure it is a Console, and can therefore display streams of text 196 func (lw *logWriter) Fd() uintptr { 197 lw.Lock() 198 defer lw.Unlock() 199 200 if file, ok := lw.out.(hasDescriptor); ok { 201 return file.Fd() 202 } 203 204 return InvalidFileDescriptor 205 } 206 207 // Remove all ANSI color information. 208 func stripColor(b []byte) []byte { 209 return colorCodeMatcher.ReplaceAll(b, []byte("")) 210 } 211 212 type hasDescriptor interface { 213 Fd() uintptr 214 }