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  }