## Does this PR need a docs update or release note? - [x] ⛔ No ## Type of change - [x] 🐛 Bugfix - [x] 🤖 Test - [x] 💻 CI/Deployment ## Test Plan - [x] 💪 Manual
351 lines
8.8 KiB
Go
351 lines
8.8 KiB
Go
package logger
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"path/filepath"
|
|
"time"
|
|
|
|
"github.com/spf13/cobra"
|
|
"github.com/spf13/pflag"
|
|
"go.uber.org/zap"
|
|
"go.uber.org/zap/zapcore"
|
|
)
|
|
|
|
// Default location for writing logs, initialized in platform specific files
|
|
var userLogsDir string
|
|
|
|
var (
|
|
logCore *zapcore.Core
|
|
loggerton *zap.SugaredLogger
|
|
|
|
// logging level flag
|
|
llFlag = "info"
|
|
|
|
// logging file flags
|
|
lfFlag = ""
|
|
|
|
DebugAPI bool
|
|
readableOutput bool
|
|
)
|
|
|
|
type logLevel int
|
|
|
|
const (
|
|
Development logLevel = iota
|
|
Info
|
|
Warn
|
|
Production
|
|
Disabled
|
|
)
|
|
|
|
const (
|
|
debugAPIFN = "debug-api-calls"
|
|
logLevelFN = "log-level"
|
|
logFileFN = "log-file"
|
|
readableLogsFN = "readable-logs"
|
|
)
|
|
|
|
// Returns the default location for writing logs
|
|
func defaultLogLocation() string {
|
|
return filepath.Join(userLogsDir, "corso", "logs", time.Now().UTC().Format("2006-01-02T15-04-05Z")+".log")
|
|
}
|
|
|
|
// adds the persistent flag --log-level and --log-file to the provided command.
|
|
// defaults to "info" and the default log location.
|
|
// This is a hack for help displays. Due to seeding the context, we also
|
|
// need to parse the log level before we execute the command.
|
|
func AddLoggingFlags(cmd *cobra.Command) {
|
|
fs := cmd.PersistentFlags()
|
|
fs.StringVar(&llFlag, logLevelFN, "info", "set the log level to debug|info|warn|error")
|
|
|
|
// The default provided here is only for help info
|
|
fs.StringVar(&lfFlag, logFileFN, "corso-<timestamp>.log", "location for writing logs, use '-' for stdout")
|
|
|
|
fs.Bool(debugAPIFN, false, "add non-2xx request/response errors to logging")
|
|
|
|
fs.Bool(
|
|
readableLogsFN, false,
|
|
"minimizes log output for console readability: removes the file and date, colors the level")
|
|
//nolint:errcheck
|
|
fs.MarkHidden(readableLogsFN)
|
|
}
|
|
|
|
// Due to races between the lazy evaluation of flags in cobra and the
|
|
// need to init logging behavior in a ctx, log-level and log-file gets
|
|
// pre-processed manually here using pflags. The canonical
|
|
// AddLogLevelFlag() and AddLogFileFlag() ensures the flags are
|
|
// displayed as part of the help/usage output.
|
|
func PreloadLoggingFlags() (string, string) {
|
|
dlf := defaultLogLocation()
|
|
fs := pflag.NewFlagSet("seed-logger", pflag.ContinueOnError)
|
|
fs.ParseErrorsWhitelist.UnknownFlags = true
|
|
fs.String(logLevelFN, "info", "set the log level to debug|info|warn|error")
|
|
fs.String(logFileFN, dlf, "location for writing logs")
|
|
fs.BoolVar(&DebugAPI, debugAPIFN, false, "add non-2xx request/response errors to logging")
|
|
fs.BoolVar(&readableOutput, readableLogsFN, false, "minimizes log output: removes the file and date, colors the level")
|
|
// prevents overriding the corso/cobra help processor
|
|
fs.BoolP("help", "h", false, "")
|
|
|
|
// parse the os args list to find the log level flag
|
|
if err := fs.Parse(os.Args[1:]); err != nil {
|
|
return "info", dlf
|
|
}
|
|
|
|
// retrieve the user's preferred log level
|
|
// automatically defaults to "info"
|
|
levelString, err := fs.GetString(logLevelFN)
|
|
if err != nil {
|
|
return "info", dlf
|
|
}
|
|
|
|
// retrieve the user's preferred log file location
|
|
// automatically defaults to default log location
|
|
logfile, err := fs.GetString(logFileFN)
|
|
if err != nil {
|
|
return "info", dlf
|
|
}
|
|
|
|
// if not specified, attempt to fall back to env declaration.
|
|
if len(logfile) == 0 {
|
|
logfile = os.Getenv("CORSO_LOG_FILE")
|
|
}
|
|
|
|
if logfile == "-" {
|
|
logfile = "stdout"
|
|
}
|
|
|
|
if logfile != "stdout" && logfile != "stderr" {
|
|
logdir := filepath.Dir(logfile)
|
|
|
|
err := os.MkdirAll(logdir, 0o755)
|
|
if err != nil {
|
|
return "info", "stderr"
|
|
}
|
|
}
|
|
|
|
return levelString, logfile
|
|
}
|
|
|
|
func genLogger(level logLevel, logfile string) (*zapcore.Core, *zap.SugaredLogger) {
|
|
// when testing, ensure debug logging matches the test.v setting
|
|
for _, arg := range os.Args {
|
|
if arg == `--test.v=true` {
|
|
level = Development
|
|
}
|
|
}
|
|
|
|
// set up a logger core to use as a fallback
|
|
levelFilter := zap.LevelEnablerFunc(func(lvl zapcore.Level) bool {
|
|
switch level {
|
|
case Info:
|
|
return lvl >= zapcore.InfoLevel
|
|
case Warn:
|
|
return lvl >= zapcore.WarnLevel
|
|
case Production:
|
|
return lvl >= zapcore.ErrorLevel
|
|
case Disabled:
|
|
return false
|
|
default:
|
|
return true
|
|
}
|
|
})
|
|
out := zapcore.Lock(os.Stderr)
|
|
consoleEncoder := zapcore.NewConsoleEncoder(zap.NewDevelopmentEncoderConfig())
|
|
core := zapcore.NewTee(
|
|
zapcore.NewCore(consoleEncoder, out, levelFilter),
|
|
)
|
|
|
|
// then try to set up a logger directly
|
|
var (
|
|
lgr *zap.Logger
|
|
err error
|
|
)
|
|
|
|
if level != Production {
|
|
cfg := zap.NewDevelopmentConfig()
|
|
|
|
switch level {
|
|
case Info:
|
|
cfg.Level = zap.NewAtomicLevelAt(zapcore.InfoLevel)
|
|
case Warn:
|
|
cfg.Level = zap.NewAtomicLevelAt(zapcore.WarnLevel)
|
|
case Disabled:
|
|
cfg.Level = zap.NewAtomicLevelAt(zapcore.FatalLevel)
|
|
}
|
|
|
|
opts := []zap.Option{}
|
|
|
|
if readableOutput {
|
|
opts = append(opts, zap.WithCaller(false), zap.AddStacktrace(zapcore.DPanicLevel))
|
|
cfg.EncoderConfig.EncodeTime = zapcore.TimeEncoderOfLayout("15:04:05.00")
|
|
cfg.EncoderConfig.EncodeLevel = zapcore.CapitalColorLevelEncoder
|
|
}
|
|
|
|
cfg.OutputPaths = []string{logfile}
|
|
lgr, err = cfg.Build(opts...)
|
|
} else {
|
|
cfg := zap.NewProductionConfig()
|
|
cfg.OutputPaths = []string{logfile}
|
|
lgr, err = cfg.Build()
|
|
}
|
|
|
|
// fall back to the core config if the default creation fails
|
|
if err != nil {
|
|
lgr = zap.New(core)
|
|
}
|
|
|
|
return &core, lgr.Sugar()
|
|
}
|
|
|
|
func singleton(level logLevel, logfile string) *zap.SugaredLogger {
|
|
if loggerton != nil {
|
|
return loggerton
|
|
}
|
|
|
|
if logCore != nil {
|
|
lgr := zap.New(*logCore)
|
|
loggerton = lgr.Sugar()
|
|
|
|
return loggerton
|
|
}
|
|
|
|
logCore, loggerton = genLogger(level, logfile)
|
|
|
|
return loggerton
|
|
}
|
|
|
|
// ------------------------------------------------------------------------------------------------
|
|
// context management
|
|
// ------------------------------------------------------------------------------------------------
|
|
|
|
type loggingKey string
|
|
|
|
const ctxKey loggingKey = "corsoLogger"
|
|
|
|
// Seed generates a logger within the context for later retrieval.
|
|
// It also parses the command line for flag values prior to executing
|
|
// cobra. This early parsing is necessary since logging depends on
|
|
// a seeded context prior to cobra evaluating flags.
|
|
func Seed(ctx context.Context, lvl, logfile string) (context.Context, *zap.SugaredLogger) {
|
|
if len(lvl) == 0 {
|
|
lvl = "info"
|
|
}
|
|
|
|
zsl := singleton(levelOf(lvl), logfile)
|
|
|
|
return Set(ctx, zsl), zsl
|
|
}
|
|
|
|
// SeedLevel generates a logger within the context with the given log-level.
|
|
func SeedLevel(ctx context.Context, level logLevel) (context.Context, *zap.SugaredLogger) {
|
|
l := ctx.Value(ctxKey)
|
|
if l == nil {
|
|
zsl := singleton(level, defaultLogLocation())
|
|
return Set(ctx, zsl), zsl
|
|
}
|
|
|
|
return ctx, l.(*zap.SugaredLogger)
|
|
}
|
|
|
|
// Set allows users to embed their own zap.SugaredLogger within the context.
|
|
func Set(ctx context.Context, logger *zap.SugaredLogger) context.Context {
|
|
if logger == nil {
|
|
return ctx
|
|
}
|
|
|
|
return context.WithValue(ctx, ctxKey, logger)
|
|
}
|
|
|
|
// Ctx retrieves the logger embedded in the context.
|
|
func Ctx(ctx context.Context) *zap.SugaredLogger {
|
|
l := ctx.Value(ctxKey)
|
|
if l == nil {
|
|
return singleton(levelOf(llFlag), defaultLogLocation())
|
|
}
|
|
|
|
return l.(*zap.SugaredLogger)
|
|
}
|
|
|
|
// transforms the llevel flag value to a logLevel enum
|
|
func levelOf(lvl string) logLevel {
|
|
switch lvl {
|
|
case "debug":
|
|
return Development
|
|
case "warn":
|
|
return Warn
|
|
case "error":
|
|
return Production
|
|
case "disabled":
|
|
return Disabled
|
|
}
|
|
|
|
return Info
|
|
}
|
|
|
|
// Flush writes out all buffered logs.
|
|
func Flush(ctx context.Context) {
|
|
_ = Ctx(ctx).Sync()
|
|
}
|
|
|
|
// ------------------------------------------------------------------------------------------------
|
|
// log wrapper for downstream api compliance
|
|
// ------------------------------------------------------------------------------------------------
|
|
|
|
type wrapper struct {
|
|
zap.SugaredLogger
|
|
|
|
forceDebugLogLevel bool
|
|
}
|
|
|
|
func (w *wrapper) process(opts ...option) {
|
|
for _, opt := range opts {
|
|
opt(w)
|
|
}
|
|
}
|
|
|
|
type option func(*wrapper)
|
|
|
|
// ForceDebugLogLevel reduces all logs emitted in the wrapper to
|
|
// debug level, independent of their original log level. Useful
|
|
// for silencing noisy dependency packages without losing the info
|
|
// altogether.
|
|
func ForceDebugLogLevel() option {
|
|
return func(w *wrapper) {
|
|
w.forceDebugLogLevel = true
|
|
}
|
|
}
|
|
|
|
// Wrap returns the logger in the package with an extended api used for
|
|
// dependency package interface compliance.
|
|
func WrapCtx(ctx context.Context, opts ...option) *wrapper {
|
|
return Wrap(Ctx(ctx), opts...)
|
|
}
|
|
|
|
// Wrap returns the sugaredLogger with an extended api used for
|
|
// dependency package interface compliance.
|
|
func Wrap(zsl *zap.SugaredLogger, opts ...option) *wrapper {
|
|
w := &wrapper{SugaredLogger: *zsl}
|
|
w.process(opts...)
|
|
|
|
return w
|
|
}
|
|
|
|
func (w *wrapper) Logf(tmpl string, args ...any) {
|
|
if w.forceDebugLogLevel {
|
|
w.SugaredLogger.Debugf(tmpl, args...)
|
|
return
|
|
}
|
|
|
|
w.SugaredLogger.Infof(tmpl, args...)
|
|
}
|
|
|
|
func (w *wrapper) Errorf(tmpl string, args ...any) {
|
|
if w.forceDebugLogLevel {
|
|
w.SugaredLogger.Debugf(tmpl, args...)
|
|
return
|
|
}
|
|
|
|
w.SugaredLogger.Errorf(tmpl, args...)
|
|
}
|