Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 13 additions & 47 deletions logger/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -42,71 +41,38 @@ 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)
}

// LogContext logs at the given level from logger in the given context.
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
}
23 changes: 23 additions & 0 deletions logger/duration.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package logger

import (
"context"
"fmt"
"log/slog"
)

Expand All @@ -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)
}
}
}
19 changes: 12 additions & 7 deletions logger/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,26 @@ 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 {
rec.Attrs(func(attr slog.Attr) bool {
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)
}
Expand All @@ -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 {
Expand Down
47 changes: 33 additions & 14 deletions logger/logger.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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)
}

Expand Down Expand Up @@ -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{
Expand Down
1 change: 0 additions & 1 deletion logger/logger_gcp.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ func NewGCPHandler(opts *slog.HandlerOptions) slog.Handler {
Level: opts.Level,
ReplaceAttr: attrReplacerChain(
GCPAttrReplacer,
durationToMsAttrReplacer,
opts.ReplaceAttr,
),
})
Expand Down
3 changes: 3 additions & 0 deletions logger/slogx/attr.go
Original file line number Diff line number Diff line change
Expand Up @@ -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())
}

Expand Down
Loading