trufflehog/pkg/log/log.go
Miccah f3367d7910
[THOG-643] Implement independent log level controls (#733)
* [THOG-643] Implement independent log level controls

There are two log level controls to mentally distinguish. Log levels
associated with a sink (e.g. stdout and streamed), and log levels
associated with a logger (e.g. a GitHub source).

The level is determined to be the minimum of the two. If a sink is at
level 0, then it will only output level 0 logs regardless of the
logger's level. This is best demonstrated by TestSinkWithName.

* Rename WithName to WithNamedLevel

* Check flush errors

* Replace IncreaseLevelCore with custom LevelCore

Adding a leveler that was less verbose would cause the initialization
fail, and therefore not be added to the core. This check is only at
the time of initialization.

An alternative approach to creating our own core is to set the child log
level equal to the parent, so initialization is guaranteed (with the
added benefit of intuitive behavior).

* Use controller if it exists, otherwise inherit parent's log level

* Cleanup some tests
2022-08-26 15:27:09 -05:00

219 lines
5.9 KiB
Go

package log
import (
"errors"
"fmt"
"io"
"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
}
// 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) {
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...))
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
}
// WithJSONSink adds a JSON encoded output to the logger.
func WithJSONSink(sink io.Writer, opts ...func(*sinkConfig)) 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 ...func(*sinkConfig)) 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.
func AddSink(l logr.Logger, sink logConfig) (logr.Logger, func() error, error) {
if sink.err != nil {
return l, nil, sink.err
}
zapLogger, err := getZapLogger(l)
if err != nil {
return l, nil, errors.New("unsupported logr implementation")
}
zapLogger = zapLogger.WithOptions(zap.WrapCore(func(core zapcore.Core) zapcore.Core {
return zapcore.NewTee(core, sink.core)
}))
return zapr.NewLogger(zapLogger), 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) func(*sinkConfig) {
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) func(*sinkConfig) {
return func(conf *sinkConfig) {
conf.level = leveler
}
}
// 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 ...func(*sinkConfig),
) logConfig {
conf := sinkConfig{
encoder: defaultEncoder,
sink: defaultSink,
level: defaultLevel,
}
for _, f := range opts {
f(&conf)
}
return logConfig{
core: zapcore.NewCore(
conf.encoder,
conf.sink,
conf.level,
),
}
}