aboutsummaryrefslogtreecommitdiffstats
path: root/logging/log.go
blob: 8dc8017c107425ac72023376b54d6fb2a0fc5605 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
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
}