diff --git a/logger/context.go b/logger/context.go index 99d2cf3..17a8bee 100644 --- a/logger/context.go +++ b/logger/context.go @@ -3,13 +3,12 @@ package logger import ( "context" "log/slog" - "math/rand/v2" "os" ) type loggerKey struct{} -// FromContext returns the logger from the context. If no logger is found, a new +// FromContext returns the logger from the context. If no logger is found, the new logger is created with default logger's attributes. func FromContext(ctx context.Context) *slog.Logger { if ctx == nil { return logger.With() @@ -42,34 +41,34 @@ func WithGroupContext(ctx context.Context, group string) context.Context { } // DebugContext logs at [LevelDebug] from logger in the given context. -func DebugContext(ctx context.Context, msg string, args ...any) { - log(ctx, FromContext(ctx), slog.LevelDebug, msg, args...) +func DebugContext(ctx context.Context, msg string, args ...slog.Attr) { + logAttrs(ctx, FromContext(ctx), slog.LevelDebug, msg, args...) } // InfoContext logs at [LevelInfo] from logger in the given context. -func InfoContext(ctx context.Context, msg string, args ...any) { - log(ctx, FromContext(ctx), slog.LevelInfo, msg, args...) +func InfoContext(ctx context.Context, msg string, args ...slog.Attr) { + logAttrs(ctx, FromContext(ctx), slog.LevelInfo, msg, args...) } // WarnContext logs at [LevelWarn] from logger in the given context. -func WarnContext(ctx context.Context, msg string, args ...any) { - log(ctx, FromContext(ctx), slog.LevelWarn, msg, args...) +func WarnContext(ctx context.Context, msg string, args ...slog.Attr) { + logAttrs(ctx, FromContext(ctx), slog.LevelWarn, msg, args...) } // ErrorContext logs at [LevelError] from logger in the given context. -func ErrorContext(ctx context.Context, msg string, args ...any) { - log(ctx, FromContext(ctx), slog.LevelError, msg, args...) +func ErrorContext(ctx context.Context, msg string, args ...slog.Attr) { + logAttrs(ctx, FromContext(ctx), slog.LevelError, msg, args...) } // PanicContext logs at [LevelPanic] and then panics from logger in the given context. -func PanicContext(ctx context.Context, msg string, args ...any) { - log(ctx, FromContext(ctx), LevelPanic, msg, args...) +func PanicContext(ctx context.Context, msg string, args ...slog.Attr) { + logAttrs(ctx, FromContext(ctx), LevelPanic, msg, args...) panic(msg) } // FatalContext logs at [LevelFatal] and then [os.Exit](1) from logger in the given context. -func FatalContext(ctx context.Context, msg string, args ...any) { - log(ctx, FromContext(ctx), LevelFatal, msg, args...) +func FatalContext(ctx context.Context, msg string, args ...slog.Attr) { + logAttrs(ctx, FromContext(ctx), LevelFatal, msg, args...) os.Exit(1) } @@ -77,36 +76,3 @@ func FatalContext(ctx context.Context, msg string, args ...any) { func LogContext(ctx context.Context, level slog.Level, msg string, args ...any) { log(ctx, FromContext(ctx), level, msg, args...) } - -// SamplingInfoContext logs at [LevelInfo] with sampling rate from logger in the given context. -func SamplingInfoContext(ctx context.Context, rate float64, msg string, args ...any) { - if shouldLog(rate) { - InfoContext(ctx, msg, args...) - } -} - -// SamplingWarnContext logs at [LevelWarn] with sampling rate from logger in the given context. -func SamplingWarnContext(ctx context.Context, rate float64, msg string, args ...any) { - if shouldLog(rate) { - WarnContext(ctx, msg, args...) - } -} - -// SamplingErrorContext logs at [LevelError] with sampling rate from logger in the given context. -func SamplingErrorContext(ctx context.Context, rate float64, msg string, args ...any) { - if shouldLog(rate) { - ErrorContext(ctx, msg, args...) - } -} - -// SamplingLogContext logs at the given level with sampling rate from logger in the given context. -func SamplingLogContext(ctx context.Context, level slog.Level, rate float64, msg string, args ...any) { - if shouldLog(rate) { - LogContext(ctx, level, msg, args...) - } -} - -// nolint: gosec -func shouldLog(probability float64) bool { - return rand.Float64() < probability -} diff --git a/logger/duration.go b/logger/duration.go index af28f7b..99c0675 100644 --- a/logger/duration.go +++ b/logger/duration.go @@ -1,6 +1,8 @@ package logger import ( + "context" + "fmt" "log/slog" ) @@ -13,3 +15,24 @@ func durationToMsAttrReplacer(groups []string, attr slog.Attr) slog.Attr { } return attr } + +// middlewareDurationHuman is a middleware that adds a new string attribute for each duration attribute to render formatted duration +func middlewareDurationHuman() middleware { + return func(next handleFunc) handleFunc { + return func(ctx context.Context, rec slog.Record) error { + rec.Attrs(func(attr slog.Attr) bool { + if attr.Value.Kind() == slog.KindDuration { + duration := attr.Value.Duration() + rec.AddAttrs( + slog.String( + fmt.Sprintf("%s_human", attr.Key), + duration.String(), + ), + ) + } + return true + }) + return next(ctx, rec) + } + } +} diff --git a/logger/error.go b/logger/error.go index 7d80259..5b6dcd2 100644 --- a/logger/error.go +++ b/logger/error.go @@ -6,8 +6,10 @@ import ( "github.com/Cleverse/go-utilities/logger/slogx" "github.com/Cleverse/go-utilities/logger/stacktrace" + "github.com/lmittmann/tint" ) +// middlewareErrorStackTrace is a middleware that adds the error stack trace when ErrorKey or "err" is found. func middlewareErrorStackTrace() middleware { return func(next handleFunc) handleFunc { return func(ctx context.Context, rec slog.Record) error { @@ -15,11 +17,15 @@ func middlewareErrorStackTrace() middleware { if attr.Key == slogx.ErrorKey || attr.Key == "err" { err := attr.Value.Any() if err, ok := err.(error); ok && err != nil { - // rec.AddAttrs(slog.String(slogx.ErrorVerboseKey, fmt.Sprintf("%+v", err))) - rec.AddAttrs(slog.Any(slogx.ErrorStackTraceKey, stacktrace.ExtractErrorStackTraces(err))) + rec.AddAttrs( + slogx.Stringer( + slogx.ErrorStackTraceKey, + stacktrace.ExtractErrorStackTraces(err), + ), + ) } } - return false + return true }) return next(ctx, rec) } @@ -31,10 +37,9 @@ func errorAttrReplacer(groups []string, attr slog.Attr) slog.Attr { switch attr.Key { case slogx.ErrorKey, "err": if err, ok := attr.Value.Any().(error); ok { - if err != nil { - return slog.Attr{Key: slogx.ErrorKey, Value: slog.StringValue(err.Error())} - } - return slog.Attr{Key: slogx.ErrorKey, Value: slog.StringValue("null")} + aErr := tint.Attr(9, slogx.String(slogx.ErrorKey, err.Error())) + aErr.Key = slogx.ErrorKey + return aErr } case slogx.ErrorStackTraceKey: type stackDetails struct { diff --git a/logger/logger.go b/logger/logger.go index 6bcea18..50de474 100644 --- a/logger/logger.go +++ b/logger/logger.go @@ -53,7 +53,13 @@ func SetLevel(level slog.Level) (old slog.Level) { return old } -// GetLogger returns the top-level logger +// SetLogger sets the global logger to the provided logger instance. +// This is useful for testing purposes where a mock logger is needed. +func SetLogger(l *slog.Logger) { + logger = l + slog.SetDefault(logger) +} + func GetLogger() *slog.Logger { return logger } @@ -77,34 +83,34 @@ func WithGroup(group string) *slog.Logger { } // Debug logs at [LevelDebug]. -func Debug(msg string, args ...any) { - log(context.Background(), logger, slog.LevelDebug, msg, args...) +func Debug(msg string, args ...slog.Attr) { + logAttrs(context.Background(), logger, slog.LevelDebug, msg, args...) } // Info logs at [LevelInfo]. -func Info(msg string, args ...any) { - log(context.Background(), logger, slog.LevelInfo, msg, args...) +func Info(msg string, args ...slog.Attr) { + logAttrs(context.Background(), logger, slog.LevelInfo, msg, args...) } // Warn logs at [LevelWarn]. -func Warn(msg string, args ...any) { - log(context.Background(), logger, slog.LevelWarn, msg, args...) +func Warn(msg string, args ...slog.Attr) { + logAttrs(context.Background(), logger, slog.LevelWarn, msg, args...) } // Error logs at [LevelError] with an error. -func Error(msg string, args ...any) { - log(context.Background(), logger, slog.LevelError, msg, args...) +func Error(msg string, args ...slog.Attr) { + logAttrs(context.Background(), logger, slog.LevelError, msg, args...) } // Panic logs at [LevelPanic] and then panics. -func Panic(msg string, args ...any) { - log(context.Background(), logger, LevelPanic, msg, args...) +func Panic(msg string, args ...slog.Attr) { + logAttrs(context.Background(), logger, LevelPanic, msg, args...) panic(msg) } // Fatal logs at [LevelFatal] followed by a call to [os.Exit](1). -func Fatal(msg string, args ...any) { - log(context.Background(), logger, LevelFatal, msg, args...) +func Fatal(msg string, args ...slog.Attr) { + logAttrs(context.Background(), logger, LevelFatal, msg, args...) os.Exit(1) } @@ -160,15 +166,28 @@ func Init(cfg Config) error { if cfg.Debug { lvl.Set(slog.LevelDebug) options.AddSource = true + } + + // enable error stack traces if debug mode is set, or output is not text + if cfg.Debug || cfg.Output != "text" { middlewares = append(middlewares, middlewareErrorStackTrace()) } switch strings.ToLower(cfg.Output) { case "json": - lvl.Set(slog.LevelInfo) + options.ReplaceAttr = attrReplacerChain(options.ReplaceAttr, durationToMsAttrReplacer) handler = slog.NewJSONHandler(os.Stdout, options) + + middlewares = append(middlewares, + middlewareDurationHuman(), + ) case "gcp": + options.ReplaceAttr = attrReplacerChain(options.ReplaceAttr, durationToMsAttrReplacer) handler = NewGCPHandler(options) + + middlewares = append(middlewares, + middlewareDurationHuman(), + ) default: handler = tint.NewHandler(os.Stdout, &tint.Options{ diff --git a/logger/logger_gcp.go b/logger/logger_gcp.go index 437b3dd..18f7921 100644 --- a/logger/logger_gcp.go +++ b/logger/logger_gcp.go @@ -18,7 +18,6 @@ func NewGCPHandler(opts *slog.HandlerOptions) slog.Handler { Level: opts.Level, ReplaceAttr: attrReplacerChain( GCPAttrReplacer, - durationToMsAttrReplacer, opts.ReplaceAttr, ), }) diff --git a/logger/slogx/attr.go b/logger/slogx/attr.go index fae1ae7..e644cbd 100644 --- a/logger/slogx/attr.go +++ b/logger/slogx/attr.go @@ -54,6 +54,9 @@ func String(key, value string) slog.Attr { // Stringer returns an slog.Attr for a fmt.Stringer value. func Stringer(key string, value fmt.Stringer) slog.Attr { + if value == nil { + return slog.Attr{} + } return slog.String(key, value.String()) }