Last active
August 10, 2024 16:41
-
-
Save KernelPryanic/e7267fed2e121174cb76f7482d308cc8 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package log | |
import ( | |
"context" | |
"fmt" | |
"os" | |
"runtime" | |
"sync" | |
"time" | |
"github.com/rs/zerolog" | |
) | |
func init() { | |
zerolog.SetGlobalLevel(zerolog.TraceLevel) | |
zerolog.TimeFieldFormat = time.RFC3339 | |
logger := zerolog.New(os.Stdout).With().Timestamp().Logger().Hook(callerHook{}) | |
zerolog.DefaultContextLogger = &logger | |
} | |
type Logger struct { | |
zerolog.Logger | |
} | |
var mutex = &sync.RWMutex{} | |
var level = zerolog.TraceLevel | |
var logLevels = map[string]zerolog.Level{ | |
"trace": zerolog.TraceLevel, | |
"debug": zerolog.DebugLevel, | |
"info": zerolog.InfoLevel, | |
"warn": zerolog.WarnLevel, | |
"error": zerolog.ErrorLevel, | |
"fatal": zerolog.FatalLevel, | |
"panic": zerolog.PanicLevel, | |
"disabled": zerolog.Disabled, | |
} | |
// SetLogLevel sets the log level for all logger instances. | |
// NOTE: Can be called at any time to change the log level. | |
func SetLogLevel(l string) { | |
mutex.Lock() | |
defer mutex.Unlock() | |
if lvl, ok := logLevels[l]; ok { | |
level = lvl | |
} | |
} | |
// SetTimeFormat sets the time format for all logger instances. | |
// NOTE: Must be called before calling the constructor, otherwise it will not take effect. | |
func SetTimeFormat(format string) { | |
mutex.Lock() | |
defer mutex.Unlock() | |
zerolog.TimeFieldFormat = format | |
} | |
type LoggerConfig struct { | |
ConsoleWriterOutput bool | |
} | |
type Option func(*LoggerConfig) | |
// WithConsoleWriter sets the logger to output basic console logs. | |
func WithConsoleWriter(enable bool) Option { | |
return func(config *LoggerConfig) { | |
config.ConsoleWriterOutput = enable | |
} | |
} | |
// New creates a new logger instance using the provided options. | |
func New(options ...Option) Logger { | |
config := &LoggerConfig{} | |
for _, option := range options { | |
option(config) | |
} | |
var logger zerolog.Logger | |
if config.ConsoleWriterOutput { | |
output := zerolog.ConsoleWriter{ | |
Out: os.Stdout, | |
TimeFormat: zerolog.TimeFieldFormat, | |
} | |
logger = zerolog.New(output).With().Timestamp().Logger() | |
} else { | |
logger = zerolog.New(os.Stdout).With().Timestamp().Logger() | |
} | |
logger = logger.Level(level).Hook(callerHook{}) | |
return Logger{Logger: logger} | |
} | |
func (l *Logger) syncLevel() { | |
mutex.RLock() | |
defer mutex.RUnlock() | |
if l.GetLevel() != level { | |
l.Logger = l.Logger.Level(level) | |
} | |
} | |
func (l *Logger) Trace() *zerolog.Event { | |
l.syncLevel() | |
return l.Logger.Trace() | |
} | |
func (l *Logger) Debug() *zerolog.Event { | |
l.syncLevel() | |
return l.Logger.Debug() | |
} | |
func (l *Logger) Info() *zerolog.Event { | |
l.syncLevel() | |
return l.Logger.Info() | |
} | |
func (l *Logger) Warn() *zerolog.Event { | |
l.syncLevel() | |
return l.Logger.Warn() | |
} | |
func (l *Logger) Error() *zerolog.Event { | |
l.syncLevel() | |
return l.Logger.Error() | |
} | |
func (l *Logger) Fatal() *zerolog.Event { | |
l.syncLevel() | |
return l.Logger.Fatal() | |
} | |
func (l *Logger) Panic() *zerolog.Event { | |
l.syncLevel() | |
return l.Logger.Panic() | |
} | |
// Ctx extracts the logger from the context. | |
func Ctx(ctx context.Context) *Logger { | |
logger := zerolog.Ctx(ctx) | |
return &Logger{Logger: *logger} | |
} | |
// CtxErr is an error type that contains contextual information related to the error. | |
// It is used to propagate the contextual information up the call stack. | |
type CtxErr struct { | |
Err error | |
Ctx map[string]any | |
} | |
func (e CtxErr) Error() string { | |
return e.Err.Error() | |
} | |
// ErrWithErrCtx adds the error context values to the error. | |
// Use this to propagate the error context up the call stack. | |
// Example: return log.ErrWithErrCtx(err, map[string]any{"user_id": 1}) | |
func ErrWithErrCtx(err error, errCtx map[string]any) error { | |
if ctxErr, ok := err.(CtxErr); ok { | |
for key, value := range errCtx { | |
ctxErr.Ctx[key] = value | |
} | |
return ctxErr | |
} | |
return CtxErr{Err: err, Ctx: errCtx} | |
} | |
// CtxErrKey is a context key for storing CtxErr objects. | |
type CtxErrKey struct{} | |
// CtxWithCtxErr adds the context error to the context. | |
// Use this to pass the context error to the logger. | |
// Logger will automatically extract the error and the context values and log them. | |
// Example: logger.Error().Ctx(log.CtxWithCtxErr(ctx, err)).Msg("an error occurred") | |
func CtxWithCtxErr(ctx context.Context, ctxErr error) context.Context { | |
var err error | |
ctxMapNew := make(map[string]any) | |
if c := ctx.Value(CtxErrKey{}); c != nil { | |
if ctxErrExisting, ok := c.(CtxErr); ok { | |
for key, value := range ctxErrExisting.Ctx { | |
ctxMapNew[key] = value | |
} | |
} | |
} | |
if ctxErrNew, ok := ctxErr.(CtxErr); ok { | |
err = ctxErrNew.Err | |
for key, value := range ctxErrNew.Ctx { | |
ctxMapNew[key] = value | |
} | |
} | |
return context.WithValue(ctx, CtxErrKey{}, CtxErr{Err: err, Ctx: ctxMapNew}) | |
} | |
// callerHook is a zerolog hook for post-processing log events. | |
type callerHook struct{} | |
// Run implements the zerolog.Hook interface. | |
func (h callerHook) Run(e *zerolog.Event, level zerolog.Level, msg string) { | |
switch level { | |
case zerolog.ErrorLevel, zerolog.FatalLevel, zerolog.PanicLevel: | |
v := e.GetCtx().Value(CtxErrKey{}) | |
if v != nil { | |
if ctxErr, ok := v.(CtxErr); ok { | |
e.Fields(ctxErr.Ctx) | |
e.Err(ctxErr.Err) | |
} | |
} | |
_, file, line, ok := runtime.Caller(3) | |
if ok { | |
e.Str("file", fmt.Sprintf("%s:%d", file, line)) | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment