Lint / golangci-lint (push) Waiting to run
Lint / semgrep (push) Waiting to run
Release / Release (push) Waiting to run
Scan for secrets / test (push) Waiting to run
Test / test (push) Waiting to run
Test / test-community (push) Waiting to run
This commit extends our logging wrapper code to allow the addition of automatically generated caller information to log entries. It also adds a way to selectively suppress this caller information for individual sinks. This commit does not change any of trufflehog's behavior - it just enables future functionality in case we want it to use it later. This commit also provides an exportable type alias for sink configuration option functions so that callers have more flexibility in how they prepare groups of them. This commit also adds some new tests for the existing redactionCore and fixes an existing bug in it. (The bug hasn't yet caused problems, but it created the potential for future problems.)
293 lines
8.8 KiB
Go
293 lines
8.8 KiB
Go
package log
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"log/slog"
|
|
"time"
|
|
|
|
"github.com/TheZeroSlave/zapsentry"
|
|
"github.com/getsentry/sentry-go"
|
|
"github.com/go-logr/logr"
|
|
"github.com/go-logr/zapr"
|
|
"go.uber.org/zap"
|
|
"go.uber.org/zap/zapcore"
|
|
)
|
|
|
|
type logConfig struct {
|
|
core zapcore.Core
|
|
cleanup func() error
|
|
err error
|
|
}
|
|
|
|
type SinkOption func(*sinkConfig)
|
|
|
|
// New creates a new log object with the provided configurations. If no sinks
|
|
// are provided, a no-op sink will be used. Returns the logger and a cleanup
|
|
// function that should be executed before the program exits.
|
|
func New(service string, configs ...logConfig) (logr.Logger, func() error) {
|
|
return NewWithCaller(service, false, configs...)
|
|
}
|
|
|
|
// NewWithCaller creates a new logger named after the specified service with the provided sink configurations. If
|
|
// addCaller is true, call site information will be attached to each emitted log message. (This behavior can be disabled
|
|
// on a per-sink basis using WithSuppressCaller.)
|
|
func NewWithCaller(service string, addCaller bool, configs ...logConfig) (logr.Logger, func() error) {
|
|
var cores []zapcore.Core
|
|
var cleanupFuncs []func() error
|
|
|
|
// create cores for the logger
|
|
for _, config := range configs {
|
|
if config.err != nil {
|
|
continue
|
|
}
|
|
cores = append(cores, config.core)
|
|
if config.cleanup != nil {
|
|
cleanupFuncs = append(cleanupFuncs, config.cleanup)
|
|
}
|
|
}
|
|
// create logger
|
|
zapLogger := zap.New(zapcore.NewTee(cores...), zap.WithCaller(addCaller))
|
|
cleanupFuncs = append(cleanupFuncs, zapLogger.Sync)
|
|
logger := zapr.NewLogger(zapLogger).WithName(service)
|
|
|
|
// report the errors we encountered in the configs
|
|
for _, config := range configs {
|
|
if config.err != nil {
|
|
logger.Error(config.err, "error configuring logger")
|
|
}
|
|
}
|
|
|
|
return logger, firstErrorFunc(cleanupFuncs...)
|
|
}
|
|
|
|
// WithSentry adds sentry integration to the logger. This configuration may
|
|
// fail, in which case, sentry will not be added and execution will continue
|
|
// normally.
|
|
func WithSentry(opts sentry.ClientOptions, tags map[string]string) logConfig {
|
|
client, err := sentry.NewClient(opts)
|
|
if err != nil {
|
|
return logConfig{err: err}
|
|
}
|
|
|
|
// create sentry core
|
|
cfg := zapsentry.Configuration{
|
|
Tags: tags,
|
|
Level: zapcore.ErrorLevel,
|
|
EnableBreadcrumbs: true,
|
|
BreadcrumbLevel: zapcore.InfoLevel,
|
|
}
|
|
core, err := zapsentry.NewCore(cfg, zapsentry.NewSentryClientFromClient(client))
|
|
if err != nil {
|
|
return logConfig{err: err}
|
|
}
|
|
|
|
return logConfig{
|
|
core: core,
|
|
cleanup: func() error {
|
|
sentry.Flush(5 * time.Second)
|
|
return nil
|
|
},
|
|
}
|
|
}
|
|
|
|
type sinkConfig struct {
|
|
encoder zapcore.Encoder
|
|
sink zapcore.WriteSyncer
|
|
level levelSetter
|
|
redactor *dynamicRedactor
|
|
suppressCaller bool
|
|
}
|
|
|
|
// WithJSONSink adds a JSON encoded output to the logger.
|
|
func WithJSONSink(sink io.Writer, opts ...SinkOption) logConfig {
|
|
return newCoreConfig(
|
|
zapcore.NewJSONEncoder(defaultEncoderConfig()),
|
|
zapcore.Lock(zapcore.AddSync(sink)),
|
|
globalLogLevel,
|
|
opts...,
|
|
)
|
|
}
|
|
|
|
// WithConsoleSink adds a console-style output to the logger.
|
|
func WithConsoleSink(sink io.Writer, opts ...SinkOption) logConfig {
|
|
return newCoreConfig(
|
|
zapcore.NewConsoleEncoder(defaultEncoderConfig()),
|
|
zapcore.Lock(zapcore.AddSync(sink)),
|
|
globalLogLevel,
|
|
opts...,
|
|
)
|
|
}
|
|
|
|
func defaultEncoderConfig() zapcore.EncoderConfig {
|
|
conf := zap.NewProductionEncoderConfig()
|
|
// Use more human-readable time format.
|
|
conf.EncodeTime = zapcore.TimeEncoderOfLayout(time.RFC3339)
|
|
conf.EncodeLevel = func(level zapcore.Level, enc zapcore.PrimitiveArrayEncoder) {
|
|
if level == zapcore.ErrorLevel {
|
|
enc.AppendString("error")
|
|
return
|
|
}
|
|
enc.AppendString(fmt.Sprintf("info-%d", -int8(level)))
|
|
}
|
|
return conf
|
|
}
|
|
|
|
// WithCore adds any user supplied zap core to the logger.
|
|
func WithCore(core zapcore.Core) logConfig {
|
|
return logConfig{core: core}
|
|
}
|
|
|
|
// AddSentry initializes a sentry client and extends an existing
|
|
// logr.Logger with the hook.
|
|
func AddSentry(l logr.Logger, opts sentry.ClientOptions, tags map[string]string) (logr.Logger, func() error, error) {
|
|
return AddSink(l, WithSentry(opts, tags))
|
|
}
|
|
|
|
// AddSink extends an existing logr.Logger with a new sink. It returns the new logr.Logger, a cleanup function, and an
|
|
// error.
|
|
//
|
|
// The new sink will not inherit any of the existing logger's key-value pairs. Key-value pairs can be added to the new
|
|
// sink specifically by passing them to this function.
|
|
func AddSink(l logr.Logger, sink logConfig, keysAndValues ...any) (logr.Logger, func() error, error) {
|
|
if sink.err != nil {
|
|
return l, nil, sink.err
|
|
}
|
|
|
|
// New key-value pairs cannot be ergonomically added directly to cores. logr has code to do it, but that code is not
|
|
// exported. Rather than replicating it ourselves, we indirectly use it by creating a temporary logger for the new
|
|
// core, adding the key-value pairs to the temporary logger, and then extracting the temporary logger's modified
|
|
// core.
|
|
newSinkLogger := zapr.NewLogger(zap.New(sink.core))
|
|
newSinkLogger = newSinkLogger.WithValues(keysAndValues...)
|
|
newCoreLogger, err := getZapLogger(newSinkLogger)
|
|
if err != nil {
|
|
return l, nil, fmt.Errorf("error setting up new key-value pairs: %w", err)
|
|
}
|
|
newSinkCore := newCoreLogger.Core()
|
|
|
|
zapLogger, err := getZapLogger(l)
|
|
if err != nil {
|
|
return l, nil, errors.New("unsupported logr implementation")
|
|
}
|
|
|
|
newLoggerOptions := []zap.Option{
|
|
// Tee the new core together with the original core
|
|
zap.WrapCore(func(core zapcore.Core) zapcore.Core { return zapcore.NewTee(core, newSinkCore) }),
|
|
|
|
// CMR: zapr.NewLogger, for whatever reason, assumes that the passed-in logger doesn't have its caller frame
|
|
// adjustment already set up, so it adds a frame skip of 2. However, that assumption doesn't hold here because
|
|
// we're adding a core to an existing logger rather than creating a new one. I can't figure out a way to disable
|
|
// this automatic frame adjustment, so we compensate for it with the hamfisted kludge of a compensating offset.
|
|
zap.AddCallerSkip(-2),
|
|
}
|
|
|
|
zapLogger = zapLogger.WithOptions(newLoggerOptions...)
|
|
newLogger := zapr.NewLogger(zapLogger)
|
|
return newLogger, firstErrorFunc(zapLogger.Sync, sink.cleanup), nil
|
|
}
|
|
|
|
// getZapLogger is a helper function that gets the underlying zap logger from a
|
|
// logr.Logger interface.
|
|
func getZapLogger(l logr.Logger) (*zap.Logger, error) {
|
|
if u, ok := l.GetSink().(zapr.Underlier); ok {
|
|
return u.GetUnderlying(), nil
|
|
}
|
|
return nil, errors.New("not a zapr logger")
|
|
}
|
|
|
|
// WithLevel sets the sink's level to a static level. This option prevents
|
|
// changing the log level for this sink later on.
|
|
func WithLevel(level int8) SinkOption {
|
|
return WithLeveler(
|
|
// Zap's levels get more verbose as the number gets smaller, as explained
|
|
// by zapr here: https://github.com/go-logr/zapr#increasing-verbosity
|
|
// For example setting the level to -2 below, means log.V(2) will be enabled.
|
|
zap.NewAtomicLevelAt(zapcore.Level(-level)),
|
|
)
|
|
}
|
|
|
|
// WithLeveler sets the sink's level enabler to leveler.
|
|
func WithLeveler(leveler levelSetter) SinkOption {
|
|
return func(conf *sinkConfig) {
|
|
conf.level = leveler
|
|
}
|
|
}
|
|
|
|
// WithGlobalRedaction adds values to be redacted from logs.
|
|
func WithGlobalRedaction() SinkOption {
|
|
return func(conf *sinkConfig) {
|
|
conf.redactor = globalRedactor
|
|
}
|
|
}
|
|
|
|
// WithSuppressCaller prevents the sink being configured from logging any caller information, irrespective of any other
|
|
// logger settings.
|
|
func WithSuppressCaller() SinkOption {
|
|
return func(conf *sinkConfig) {
|
|
conf.suppressCaller = true
|
|
}
|
|
}
|
|
|
|
// ToLogger converts the logr.Logger into a legacy *log.Logger.
|
|
func ToLogger(l logr.Logger) *log.Logger {
|
|
return slog.NewLogLogger(logr.ToSlogHandler(l), slog.LevelInfo)
|
|
}
|
|
|
|
// ToSlogger converts the logr.Logger into a *slog.Logger.
|
|
func ToSlogger(l logr.Logger) *slog.Logger {
|
|
return slog.New(logr.ToSlogHandler(l))
|
|
}
|
|
|
|
// firstErrorFunc is a helper function that returns a function that executes
|
|
// all provided args and returns the first error, if any.
|
|
func firstErrorFunc(fs ...func() error) func() error {
|
|
return func() error {
|
|
var firstErr error = nil
|
|
for _, f := range fs {
|
|
if f == nil {
|
|
continue
|
|
}
|
|
if err := f(); err != nil && firstErr == nil {
|
|
firstErr = err
|
|
}
|
|
}
|
|
return firstErr
|
|
}
|
|
}
|
|
|
|
// newCoreConfig is a helper function that creates a default sinkConfig,
|
|
// applies the options, then creates a zapcore.Core.
|
|
func newCoreConfig(
|
|
defaultEncoder zapcore.Encoder,
|
|
defaultSink zapcore.WriteSyncer,
|
|
defaultLevel levelSetter,
|
|
opts ...SinkOption,
|
|
) logConfig {
|
|
conf := sinkConfig{
|
|
encoder: defaultEncoder,
|
|
sink: defaultSink,
|
|
level: defaultLevel,
|
|
}
|
|
for _, f := range opts {
|
|
f(&conf)
|
|
}
|
|
core := zapcore.NewCore(
|
|
conf.encoder,
|
|
conf.sink,
|
|
conf.level,
|
|
)
|
|
|
|
if conf.redactor != nil {
|
|
core = NewRedactionCore(core, conf.redactor)
|
|
}
|
|
|
|
if conf.suppressCaller {
|
|
core = &suppressCallerCore{Core: core}
|
|
}
|
|
|
|
return logConfig{core: core}
|
|
}
|