aboutsummaryrefslogtreecommitdiffstats
path: root/logging/log.go
diff options
context:
space:
mode:
Diffstat (limited to 'logging/log.go')
-rw-r--r--logging/log.go136
1 files changed, 136 insertions, 0 deletions
diff --git a/logging/log.go b/logging/log.go
new file mode 100644
index 0000000..8dc8017
--- /dev/null
+++ b/logging/log.go
@@ -0,0 +1,136 @@
+// Package logging provides helpers for context-aware logging using Go's slog package.
+//
+// It allows attaching loggers to context.Context for structured logging,
+// recovering and logging panics with stack traces,
+// and provides a simple package-level logger setup function.
+package logging
+
+import (
+ "context"
+ "fmt"
+ "io"
+ "log/slog"
+ "os"
+ "runtime"
+ "strings"
+)
+
+type ctxLoggerKeyType struct{}
+
+var ctxLoggerKey = ctxLoggerKeyType{}
+
+// WithLogger returns a new context derived from ctx that carries the provided slog.Logger.
+//
+// Typical usage:
+//
+// ctx = logging.WithLogger(ctx, logger)
+func WithLogger(ctx context.Context, logger *slog.Logger) context.Context {
+ return context.WithValue(ctx, ctxLoggerKey, logger)
+}
+
+// FromContext retrieves the slog.Logger stored in ctx if any.
+// If no logger is attached to the context, returns slog.Default().
+func FromContext(ctx context.Context) *slog.Logger {
+ if logger, ok := ctx.Value(ctxLoggerKey).(*slog.Logger); ok {
+ return logger
+ }
+ return slog.Default()
+}
+
+// With returns a new slog.Logger augmented with the passed key-value pairs,
+// and a new context holding this logger.
+//
+// Typical usage:
+//
+// logger, ctx := logging.With(ctx, "user_id", userID)
+func With(ctx context.Context, args ...any) (*slog.Logger, context.Context) {
+ logger := FromContext(ctx)
+ logger = logger.With(args...)
+ ctx = WithLogger(ctx, logger)
+ return logger, ctx
+}
+
+// WithGroup returns a new slog.Logger grouped under name,
+// and a new context holding this logger.
+//
+// Typical usage:
+//
+// logger, ctx := logging.WithGroup(ctx, "http")
+func WithGroup(ctx context.Context, name string) (*slog.Logger, context.Context) {
+ logger := FromContext(ctx)
+ logger = logger.WithGroup(name)
+ ctx = WithLogger(ctx, logger)
+ return logger, ctx
+}
+
+// RecoverAndLog logs a recovered panic or error with a stack trace using the logger from ctx.
+//
+// Typically used in a deferred recover block:
+//
+// defer func() {
+// if err := recover(); err != nil {
+// logging.RecoverAndLog(ctx, "Panic recovered", err)
+// os.Exit(1)
+// }
+// }()
+func RecoverAndLog(ctx context.Context, msg string, err any) {
+ const size = 64 << 10 // 64 KB stack trace buffer
+ buf := make([]byte, size)
+ buf = buf[:runtime.Stack(buf, false)]
+ logger := FromContext(ctx)
+ logger.ErrorContext(ctx, msg, "Error", err, "stack", string(buf))
+}
+
+type LogSink string
+
+const (
+ SinkStdout LogSink = "stdout"
+ SinkStderr LogSink = "stderr"
+ SinkFile LogSink = "file"
+)
+
+type LogConfig struct {
+ Level slog.Level `toml:"level"`
+ Sink LogSink `toml:"sink"`
+ Format string `toml:"format"`
+ File string `toml:"file"`
+}
+
+func Setup(cfg LogConfig) error {
+ var w io.Writer
+
+ switch cfg.Sink {
+ case SinkStdout, "":
+ w = os.Stdout
+ case SinkStderr:
+ w = os.Stderr
+ case SinkFile:
+ if cfg.File == "" {
+ return fmt.Errorf("log sink set to 'file' but no file path provided")
+ }
+ f, err := os.OpenFile(cfg.File, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
+ if err != nil {
+ return fmt.Errorf("failed to open log file: %w", err)
+ }
+ w = f
+ default:
+ return fmt.Errorf("unsupported log sink: %s", cfg.Sink)
+ }
+
+ var handler slog.Handler
+ opts := &slog.HandlerOptions{
+ Level: cfg.Level,
+ }
+
+ switch strings.ToLower(cfg.Format) {
+ case "text":
+ handler = slog.NewTextHandler(w, opts)
+ case "json", "":
+ handler = slog.NewJSONHandler(w, opts)
+ default:
+ return fmt.Errorf("unsupported log format: %s", cfg.Format)
+ }
+
+ slog.SetDefault(slog.New(handler))
+ return nil
+}