options.go

v0.2.0
Doc Versions Source
1
package scribe
2
3
import (
4
	"io"
5
	"log/slog"
6
	"os"
7
	"regexp"
8
	"time"
9
)
10
11
// MaskRule defines a pattern for masking sensitive values.
12
type MaskRule struct {
13
	Pattern     *regexp.Regexp
14
	Replacement string
15
	KeepPrefix  int // If > 0, keep first N chars and append Replacement
16
}
17
18
// Option configures a Handler.
19
type Option func(*options)
20
21
type options struct {
22
	// Common options
23
	writer      io.Writer
24
	level       slog.Leveler
25
	replaceAttr func(groups []string, a slog.Attr) slog.Attr
26
	addSource   bool
27
28
	// Tint-specific options
29
	timeFormat  string
30
	noColor     bool
31
	levelColors map[slog.Level]Color
32
33
	// Buffer-specific options
34
	bufferSize int
35
36
	// Error formatting options
37
	expandErrors   bool
38
	fullStacktrace bool
39
40
	// Masking options
41
	maskRules []MaskRule
42
}
43
44
func defaultOptions() *options {
45
	return &options{
46
		writer:       os.Stderr,
47
		level:        slog.LevelInfo,
48
		timeFormat:   time.DateTime,
49
		levelColors:  DefaultLevelColors(),
50
		bufferSize:   1000,
51
		expandErrors: true, // Flatten error details by default
52
	}
53
}
54
55
// WithWriter sets the output destination.
56
func WithWriter(w io.Writer) Option {
57
	return func(o *options) {
58
		o.writer = w
59
	}
60
}
61
62
// WithLevel sets the minimum log level.
63
func WithLevel(l slog.Leveler) Option {
64
	return func(o *options) {
65
		o.level = l
66
	}
67
}
68
69
// WithReplaceAttr sets the attribute replacement function.
70
// This function is called for each attribute before it is formatted.
71
func WithReplaceAttr(f func(groups []string, a slog.Attr) slog.Attr) Option {
72
	return func(o *options) {
73
		o.replaceAttr = f
74
	}
75
}
76
77
// WithSource enables source code location in log output.
78
func WithSource(enabled bool) Option {
79
	return func(o *options) {
80
		o.addSource = enabled
81
	}
82
}
83
84
// WithTimeFormat sets the time format for tint output.
85
// Uses Go's time format strings (e.g., time.RFC3339, time.Kitchen).
86
func WithTimeFormat(format string) Option {
87
	return func(o *options) {
88
		o.timeFormat = format
89
	}
90
}
91
92
// WithNoColor disables colorized output.
93
func WithNoColor(noColor bool) Option {
94
	return func(o *options) {
95
		o.noColor = noColor
96
	}
97
}
98
99
// WithLevelColors sets custom colors for log levels.
100
func WithLevelColors(colors map[slog.Level]Color) Option {
101
	return func(o *options) {
102
		o.levelColors = colors
103
	}
104
}
105
106
// WithBufferSize sets the circular buffer capacity (number of records).
107
func WithBufferSize(size int) Option {
108
	return func(o *options) {
109
		if size > 0 {
110
			o.bufferSize = size
111
		}
112
	}
113
}
114
115
// WithExpandErrors controls whether error details are expanded into
116
// separate fields (err.msg, err.code, etc.) or kept as a single group.
117
// Default is true (expanded).
118
func WithExpandErrors(expand bool) Option {
119
	return func(o *options) {
120
		o.expandErrors = expand
121
	}
122
}
123
124
// WithFullStacktrace controls whether full stacktraces are shown.
125
// When false (default), only the first frame (file:line) is shown.
126
// When true, the complete stacktrace is displayed.
127
func WithFullStacktrace(full bool) Option {
128
	return func(o *options) {
129
		o.fullStacktrace = full
130
	}
131
}
132
133
// WithMask adds a masking rule for sensitive data.
134
// Pattern is a regex that matches against the full key path (e.g., "user.password").
135
// Replacement is the string to show instead of the actual value (default: "***").
136
//
137
// Example patterns:
138
//   - `(?i)password` - matches any key containing "password" (case-insensitive)
139
//   - `(?i)(secret|token|key)` - matches keys with secret, token, or key
140
//   - `^user\..*_secret$` - matches user.api_secret, user.client_secret, etc.
141
//   - `\[\d+\]\.apiKey` - matches items[0].apiKey, items[1].apiKey, etc.
142
func WithMask(pattern string, replacement string) Option {
143
	return func(o *options) {
144
		re, err := regexp.Compile(pattern)
145
		if err != nil {
146
			return // Skip invalid patterns
147
		}
148
		if replacement == "" {
149
			replacement = "***"
150
		}
151
		o.maskRules = append(o.maskRules, MaskRule{
152
			Pattern:     re,
153
			Replacement: replacement,
154
		})
155
	}
156
}
157
158
// WithMaskPartial adds a partial masking rule that keeps the first N characters.
159
// Useful for tokens, IDs, or hashes where you want to see a prefix for debugging.
160
//
161
// Example:
162
//
163
//	WithMaskPartial(`(?i)token`, 6) // "bearer-abc123xyz" → "bearer***"
164
//	WithMaskPartial(`(?i)hash`, 8)  // "0xabcdef123456" → "0xabcdef***"
165
func WithMaskPartial(pattern string, keepPrefix int) Option {
166
	return func(o *options) {
167
		re, err := regexp.Compile(pattern)
168
		if err != nil {
169
			return
170
		}
171
		if keepPrefix < 0 {
172
			keepPrefix = 0
173
		}
174
		o.maskRules = append(o.maskRules, MaskRule{
175
			Pattern:     re,
176
			Replacement: "***",
177
			KeepPrefix:  keepPrefix,
178
		})
179
	}
180
}
181
182
// WithMaskKeys adds exact key masking (case-insensitive).
183
// This is a convenience wrapper around WithMask for simple cases.
184
//
185
// Example:
186
//
187
//	WithMaskKeys("password", "secret", "token", "apiKey")
188
func WithMaskKeys(keys ...string) Option {
189
	return func(o *options) {
190
		for _, key := range keys {
191
			// Match the key anywhere in the path, case-insensitive
192
			pattern := `(?i)(^|\.|\])` + regexp.QuoteMeta(key) + `($|\.|\[)`
193
			re, err := regexp.Compile(pattern)
194
			if err != nil {
195
				continue
196
			}
197
			o.maskRules = append(o.maskRules, MaskRule{
198
				Pattern:     re,
199
				Replacement: "***",
200
			})
201
		}
202
	}
203
}
204

Source Files