github.com/tilt-dev/tilt@v0.33.15-0.20240515162809-0a22ed45d8a0/pkg/logger/logger.go (about)

     1  package logger
     2  
     3  import (
     4  	"context"
     5  	"io"
     6  	"os"
     7  
     8  	"github.com/fatih/color"
     9  
    10  	"github.com/mattn/go-isatty"
    11  )
    12  
    13  // Logger with better controls for levels and colors.
    14  //
    15  // Note that our loggers often serve as both traditional loggers (where each
    16  // call to PodLog() is a discrete log entry that may be emitted as JSON or with
    17  // newlines) and as Writers (where each call to Write() may be part of a larger
    18  // output stream, and each message may not end in a newline).
    19  //
    20  // Logger implementations that bridge these two worlds should have discrete
    21  // messages (like Infof) append a newline to the string before passing it to
    22  // Write().
    23  type Logger interface {
    24  	// log information that is likely to only be of interest to tilt developers
    25  	Debugf(format string, a ...interface{})
    26  
    27  	// log information that a tilt user might not want to see on every run, but that they might find
    28  	// useful when debugging their Tiltfile/docker/k8s configs
    29  	Verbosef(format string, a ...interface{})
    30  
    31  	// log information that we always want to show
    32  	Infof(format string, a ...interface{})
    33  
    34  	// Warnings to show in the alert pane.
    35  	Warnf(format string, a ...interface{})
    36  
    37  	// Halting errors to show in the alert pane.
    38  	Errorf(format string, a ...interface{})
    39  
    40  	Write(level Level, bytes []byte)
    41  
    42  	// gets an io.Writer that filters to the specified level for, e.g., passing to a subprocess
    43  	Writer(level Level) io.Writer
    44  
    45  	Level() Level
    46  
    47  	SupportsColor() bool
    48  
    49  	WithFields(fields Fields) Logger
    50  }
    51  
    52  type LogHandler interface {
    53  	Write(level Level, fields Fields, bytes []byte) error
    54  }
    55  
    56  type Level struct {
    57  	// UGH, for backwards-compatibility, the serialized value doesn't say anything
    58  	// about relative priority.
    59  	id int32
    60  
    61  	severity int32
    62  }
    63  
    64  func (l Level) ToProtoID() int32 {
    65  	return l.id
    66  }
    67  
    68  // If l is the logger level, determine if we should display
    69  // logs of the given severity.
    70  func (l Level) ShouldDisplay(log Level) bool {
    71  	return l.severity <= log.severity
    72  }
    73  
    74  func (l Level) AsSevereAs(log Level) bool {
    75  	return l.severity >= log.severity
    76  }
    77  
    78  var (
    79  	NoneLvl    = Level{id: 0, severity: 0}
    80  	DebugLvl   = Level{id: 3, severity: 100}
    81  	VerboseLvl = Level{id: 2, severity: 200}
    82  	InfoLvl    = Level{id: 1, severity: 300}
    83  	WarnLvl    = Level{id: 4, severity: 400}
    84  	ErrorLvl   = Level{id: 5, severity: 500}
    85  )
    86  
    87  type contextKey struct{}
    88  
    89  var LoggerContextKey = contextKey{}
    90  
    91  func Get(ctx context.Context) Logger {
    92  	val := ctx.Value(LoggerContextKey)
    93  
    94  	if val != nil {
    95  		return val.(Logger)
    96  	}
    97  
    98  	// No logger found in context, something is wrong.
    99  	panic("Called logger.Get(ctx) on a context with no logger attached!")
   100  }
   101  
   102  func NewLogger(minLevel Level, writer io.Writer) Logger {
   103  	// adapted from fatih/color
   104  	supportsColor := true
   105  	if os.Getenv("TERM") == "dumb" {
   106  		supportsColor = false
   107  	} else {
   108  		file, isFile := writer.(*os.File)
   109  		if isFile {
   110  			fd := file.Fd()
   111  			supportsColor = isatty.IsTerminal(fd) || isatty.IsCygwinTerminal(fd)
   112  		}
   113  	}
   114  	return NewFuncLogger(supportsColor, minLevel, func(level Level, fields Fields, bytes []byte) error {
   115  		_, err := writer.Write(bytes)
   116  		return err
   117  	})
   118  }
   119  
   120  func NewTestLogger(writer io.Writer) Logger {
   121  	return NewFuncLogger(false, DebugLvl, func(level Level, fields Fields, bytes []byte) error {
   122  		_, err := writer.Write(bytes)
   123  		return err
   124  	})
   125  }
   126  
   127  func WithLogger(ctx context.Context, logger Logger) context.Context {
   128  	return context.WithValue(ctx, LoggerContextKey, logger)
   129  }
   130  
   131  func getColor(l Logger, c color.Attribute) *color.Color {
   132  	color := color.New(c)
   133  	if !l.SupportsColor() {
   134  		color.DisableColor()
   135  	}
   136  	return color
   137  }
   138  
   139  func Blue(l Logger) *color.Color   { return getColor(l, color.FgBlue) }
   140  func Yellow(l Logger) *color.Color { return getColor(l, color.FgYellow) }
   141  func Green(l Logger) *color.Color  { return getColor(l, color.FgGreen) }
   142  func Red(l Logger) *color.Color    { return getColor(l, color.FgRed) }
   143  
   144  func CtxWithLogHandler(ctx context.Context, handler LogHandler) context.Context {
   145  	original := Get(ctx)
   146  	newLogger := NewFuncLogger(original.SupportsColor(), original.Level(), handler.Write)
   147  	return WithLogger(ctx, newLogger)
   148  }
   149  
   150  // Returns a context containing a logger that forks all of its output
   151  // to both the parent context's logger and to the given `io.Writer`
   152  func CtxWithForkedOutput(ctx context.Context, writer io.Writer) context.Context {
   153  	l := Get(ctx)
   154  
   155  	write := func(level Level, fields Fields, b []byte) error {
   156  		l.Write(level, b)
   157  		if l.Level().ShouldDisplay(level) {
   158  			b = append([]byte{}, b...)
   159  			_, err := writer.Write(b)
   160  			if err != nil {
   161  				return err
   162  			}
   163  		}
   164  		return nil
   165  	}
   166  
   167  	forkedLogger := NewFuncLogger(l.SupportsColor(), l.Level(), write)
   168  	return WithLogger(ctx, forkedLogger)
   169  }