Skip to content

Instantly share code, notes, and snippets.

@KernelPryanic
Last active August 10, 2024 16:41
Show Gist options
  • Save KernelPryanic/e7267fed2e121174cb76f7482d308cc8 to your computer and use it in GitHub Desktop.
Save KernelPryanic/e7267fed2e121174cb76f7482d308cc8 to your computer and use it in GitHub Desktop.
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