2022-08-17 20:03:35 +00:00
|
|
|
package log
|
|
|
|
|
|
|
|
import (
|
|
|
|
"errors"
|
2022-08-26 20:27:09 +00:00
|
|
|
"fmt"
|
2022-08-17 20:03:35 +00:00
|
|
|
"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
|
|
|
|
},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-08-26 20:27:09 +00:00
|
|
|
type sinkConfig struct {
|
|
|
|
encoder zapcore.Encoder
|
|
|
|
sink zapcore.WriteSyncer
|
|
|
|
level levelSetter
|
|
|
|
}
|
|
|
|
|
2022-08-17 20:03:35 +00:00
|
|
|
// WithJSONSink adds a JSON encoded output to the logger.
|
2022-08-26 20:27:09 +00:00
|
|
|
func WithJSONSink(sink io.Writer, opts ...func(*sinkConfig)) logConfig {
|
|
|
|
return newCoreConfig(
|
|
|
|
zapcore.NewJSONEncoder(defaultEncoderConfig()),
|
|
|
|
zapcore.Lock(zapcore.AddSync(sink)),
|
|
|
|
globalLogLevel,
|
|
|
|
opts...,
|
|
|
|
)
|
2022-08-17 20:03:35 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// WithConsoleSink adds a console-style output to the logger.
|
2022-08-26 20:27:09 +00:00
|
|
|
func WithConsoleSink(sink io.Writer, opts ...func(*sinkConfig)) logConfig {
|
|
|
|
return newCoreConfig(
|
|
|
|
zapcore.NewConsoleEncoder(defaultEncoderConfig()),
|
|
|
|
zapcore.Lock(zapcore.AddSync(sink)),
|
|
|
|
globalLogLevel,
|
|
|
|
opts...,
|
|
|
|
)
|
2022-08-17 20:03:35 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func defaultEncoderConfig() zapcore.EncoderConfig {
|
|
|
|
conf := zap.NewProductionEncoderConfig()
|
|
|
|
// Use more human-readable time format.
|
|
|
|
conf.EncodeTime = zapcore.TimeEncoderOfLayout(time.RFC3339)
|
2022-08-26 20:27:09 +00:00
|
|
|
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)))
|
|
|
|
}
|
2022-08-17 20:03:35 +00:00
|
|
|
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) {
|
2022-08-26 20:27:09 +00:00
|
|
|
return AddSink(l, WithSentry(opts, tags))
|
2022-08-17 20:03:35 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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) {
|
2022-08-26 20:27:09 +00:00
|
|
|
if sink.err != nil {
|
|
|
|
return l, nil, sink.err
|
|
|
|
}
|
2022-08-17 20:03:35 +00:00
|
|
|
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")
|
|
|
|
}
|
|
|
|
|
2022-08-26 20:27:09 +00:00
|
|
|
// 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
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-08-17 20:03:35 +00:00
|
|
|
// 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
|
|
|
|
}
|
|
|
|
}
|
2022-08-26 20:27:09 +00:00
|
|
|
|
|
|
|
// 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,
|
|
|
|
),
|
|
|
|
}
|
|
|
|
}
|