Skip to content

Commit f14aa63

Browse files
author
Russ Egan
committed
Improve docs and tests
1 parent 2b8304d commit f14aa63

File tree

5 files changed

+151
-36
lines changed

5 files changed

+151
-36
lines changed

go.sum

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -670,7 +670,6 @@ golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
670670
golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
671671
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
672672
golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
673-
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
674673
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
675674
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
676675
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=

v2/config_from_env.go

Lines changed: 65 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -56,13 +56,61 @@ func ConfigFromEnv(envvars ...string) error {
5656
return nil
5757
}
5858

59+
// MustConfigFromEnv is like ConfigFromEnv, but panics on error.
5960
func MustConfigFromEnv(envvars ...string) {
6061
err := ConfigFromEnv(envvars...)
6162
if err != nil {
6263
panic(err)
6364
}
6465
}
6566

67+
// UnmarshalEnv reads handler options from an environment variable. The first environment
68+
// variable in the list with a non-empty value will be unmarshaled into the options arg.
69+
//
70+
// The first argument must not be nil.
71+
//
72+
// The value of the environment variable can be either json, or a levels string:
73+
//
74+
// FLUME={"level":"inf"} // json
75+
// FLUME=*=inf // levels string
76+
//
77+
// This is the full schema for the json encoding:
78+
//
79+
// {
80+
// "development": <bool>,
81+
// "handler": <str>, // looks up HandlerFn using LookupHandlerFn()
82+
// "encoding": <str>, // v1 alias for "handler"; if both set, "handler" wins
83+
// "level": <str>, // e.g. "INF", "INFO", "INF-1"
84+
// "levels": <str or obj>, // either a levels string, or an object where the keys
85+
// // are logger names, and the values are levels (in the same
86+
// // format as the "level" property)
87+
// "addSource": <bool>,
88+
// "addCaller": <bool>, // v1 alias for "addSource"; if both set, "addSource" wins
89+
// }
90+
//
91+
// Level strings are in the form:
92+
//
93+
// Levels = Directive {"," Directive} .
94+
// Directive = logger | "-" logger | logger "=" Level | "*" .
95+
// Level = LevelName [ "-" offset ] | int .
96+
// LevelName = "DEBUG" | "DBG" | "INFO" | "INF" |
97+
// "WARN" | "WRN" | "ERROR" | "ERR" |
98+
// "ALL" | "OFF" | ""
99+
//
100+
// Where `logger` is the name of a logger. "*" sets the default level. LevelName is
101+
// case-insensitive.
102+
//
103+
// Example:
104+
//
105+
// *=INF,http,-sql,boot=DEBUG,authz=ERR,authn=INF+1,keys=4
106+
//
107+
// - sets default level to info
108+
// - enables all log levels on the http logger
109+
// - disables all logging from the sql logger
110+
// - sets the boot logger to debug
111+
// - sets the authz logger to ERR
112+
// - sets the authn logger to level 1 (slog.LevelInfo + 1)
113+
// - sets the keys logger to WARN (slog.LevelWarn == 4)
66114
func UnmarshalEnv(o *HandlerOptions, envvars ...string) error {
67115
for _, v := range envvars {
68116
configString := os.Getenv(v)
@@ -125,17 +173,33 @@ func initHandlerFns() {
125173
})
126174
}
127175

176+
// LookupHandlerFn looks for a handler registered with the given name. Registered
177+
// handlers are stored in an internal, package level map, which is initialized with some
178+
// built-in handlers. Handlers can be added or replaced via RegisterHandlerFn.
179+
//
180+
// Returns nil if name is not found.
181+
//
182+
// LookupHandlerFn is used when unmarshaling HandlerOptions from json, to resolve the
183+
// "handler" property to a handler function.
128184
func LookupHandlerFn(name string) HandlerFn {
129185
initHandlerFns()
130186
v, ok := handlerFns.Load(name)
131187
if !ok {
132188
return nil
133189
}
134-
// fn := v.(func(string, io.Writer, *slog.HandlerOptions) slog.Handler) //nolint:forcetypeassert // if it's not a HandlerFn, we should panic
135190
fn := v.(HandlerFn) //nolint:forcetypeassert // if it's not a HandlerFn, we should panic
136191
return fn
137192
}
138193

194+
// RegisterHandlerFn registers a handler with a name. The handler can be looked up
195+
// with LookupHandlerFn. If a handler function was already registered with the given
196+
// name, the old handler function is replaced. Built-in handler functions can also
197+
// be replaced in this manner.
198+
func RegisterHandlerFn(name string, fn HandlerFn) {
199+
initHandlerFns()
200+
registerHandlerFn(name, fn)
201+
}
202+
139203
// JSONHandlerFn is shorthand for LookupHandlerFn("json"). Will never be nil.
140204
func JSONHandlerFn() HandlerFn {
141205
return LookupHandlerFn(JSONHandler)
@@ -161,11 +225,6 @@ func NoopHandlerFn() HandlerFn {
161225
return LookupHandlerFn(NoopHandler)
162226
}
163227

164-
func RegisterHandlerFn(name string, fn HandlerFn) {
165-
initHandlerFns()
166-
registerHandlerFn(name, fn)
167-
}
168-
169228
func registerHandlerFn(name string, fn HandlerFn) {
170229
if fn == nil {
171230
panic(fmt.Sprintf("constructor for sink %q is nil", name))

v2/config_from_env_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ func TestUnmarshalEnv(t *testing.T) {
6666
"FLUME": `blue=RED`,
6767
},
6868
envvars: DefaultConfigEnvVars,
69-
expectError: "parsing levels string from environment variable FLUME: invalid log levels: invalid log level 'RED': slog: level string \"RED\": unknown name",
69+
expectError: "parsing levels string from environment variable FLUME: invalid levels value 'blue=RED': invalid log level 'RED': slog: level string \"RED\": unknown name",
7070
},
7171
{
7272
name: "parse levels string",

v2/handler_options.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ import (
1414

1515
// Define static error variables
1616
var (
17-
ErrInvalidLevelsValue = errors.New("invalid levels value")
18-
ErrInvalidLevelType = errors.New("levels must be a string or int value")
17+
ErrInvalidLevels = errors.New("invalid levels value")
18+
ErrInvalidLevel = errors.New("invalid log level")
1919
ErrUnregisteredHandler = errors.New("unregistered handler")
2020
)
2121

@@ -120,7 +120,7 @@ func (o *HandlerOptions) UnmarshalJSON(bytes []byte) error {
120120
}
121121
}
122122
default:
123-
return fmt.Errorf("%w: %v", ErrInvalidLevelsValue, s.Levels)
123+
return fmt.Errorf("%w '%v': must be a levels string or map", ErrInvalidLevels, s.Levels)
124124
}
125125

126126
// for backward compatibility with flumev1, if there is a level named "*"
@@ -202,7 +202,7 @@ func parseLevel(v any) (slog.Level, error) {
202202
}
203203
return LevelOff, nil
204204
default:
205-
return 0, ErrInvalidLevelType
205+
return 0, fmt.Errorf("%w: should be string, number, or bool", ErrInvalidLevel)
206206
}
207207

208208
// allow raw integer values for level
@@ -235,7 +235,7 @@ func parseLevel(v any) (slog.Level, error) {
235235
var l slog.Level
236236
err := l.UnmarshalText([]byte(s))
237237
if err != nil {
238-
return 0, fmt.Errorf("invalid log level '%v': %w", v, err)
238+
return 0, fmt.Errorf("%w '%v': %w", ErrInvalidLevel, v, err)
239239
}
240240
return l, nil
241241
}
@@ -299,7 +299,7 @@ func parseLevels(s string) (map[string]slog.Leveler, error) {
299299
}
300300
}
301301
if errs != nil {
302-
return nil, fmt.Errorf("invalid log levels: %w", errs)
302+
return nil, fmt.Errorf("%w '%v': %w", ErrInvalidLevels, s, errs)
303303
}
304304
return m, nil
305305
}

v2/handler_options_test.go

Lines changed: 79 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ func TestParseLevel_error(t *testing.T) {
7171
{
7272
name: "map",
7373
input: map[string]string{},
74-
wantError: "levels must be a string or int value",
74+
wantError: "invalid log level: should be string, number, or bool",
7575
},
7676
{
7777
name: "invalid level modifier",
@@ -88,7 +88,7 @@ func TestParseLevel_error(t *testing.T) {
8888
}
8989
}
9090

91-
func TestLevelsMarshaling(t *testing.T) {
91+
func TestLevels_MarshalText(t *testing.T) {
9292
tests := []struct {
9393
name string
9494
levels Levels
@@ -211,11 +211,12 @@ func TestLevels_UnmarshalText(t *testing.T) {
211211
func TestHandlerOptions_UnmarshalJSON(t *testing.T) {
212212
theme := console.NewDefaultTheme()
213213
tests := []struct {
214-
name string
215-
confJSON string
216-
want string
217-
expected HandlerOptions
218-
wantErr string
214+
name string
215+
confJSON string
216+
want string
217+
expected HandlerOptions
218+
wantErr string
219+
wantErrIs error
219220
}{
220221
{
221222
name: "defaults",
@@ -280,18 +281,36 @@ func TestHandlerOptions_UnmarshalJSON(t *testing.T) {
280281
},
281282
},
282283
{
283-
name: "encoding as alias for defaultSink",
284+
name: "encoding as alias for handler",
284285
confJSON: `{"encoding":"text"}`,
285286
expected: HandlerOptions{
286287
HandlerFn: TextHandlerFn(),
287288
},
289+
want: "level=INFO msg=hi\n",
288290
},
289291
{
290292
name: "handler has higher precedence than encoding",
291293
confJSON: `{"handler":"text", "encoding":"json"}`,
292294
expected: HandlerOptions{
293295
HandlerFn: TextHandlerFn(),
294296
},
297+
want: "level=INFO msg=hi\n",
298+
},
299+
{
300+
name: "encoding supports ltsv as alias for text",
301+
confJSON: `{"encoding":"ltsv"}`,
302+
expected: HandlerOptions{
303+
HandlerFn: TextHandlerFn(),
304+
},
305+
want: "level=INFO msg=hi\n",
306+
},
307+
{
308+
name: "encoding supports console as alias for term",
309+
confJSON: `{"encoding":"console"}`,
310+
expected: HandlerOptions{
311+
HandlerFn: TermHandlerFn(),
312+
},
313+
want: "|INF| hi\n",
295314
},
296315
{
297316
name: "addCaller as alias for addSource",
@@ -323,39 +342,77 @@ func TestHandlerOptions_UnmarshalJSON(t *testing.T) {
323342
},
324343
want: `{"level":"INFO","msg":"hi"}` + "\n",
325344
},
345+
{
346+
name: "term handler",
347+
confJSON: `{"handler":"term"}`,
348+
expected: HandlerOptions{
349+
HandlerFn: TermHandlerFn(),
350+
},
351+
want: "|INF| hi\n",
352+
},
353+
{
354+
name: "term color handler",
355+
confJSON: `{"handler":"term-color"}`,
356+
expected: HandlerOptions{
357+
HandlerFn: TermHandlerFn(),
358+
},
359+
want: "\x1b[2;1m|\x1b[0m\x1b[36mINF\x1b[0m\x1b[2;1m|\x1b[0m \x1b[1mhi\x1b[0m\n",
360+
},
361+
{
362+
name: "noop handler",
363+
confJSON: `{"handler":"noop"}`,
364+
expected: HandlerOptions{
365+
HandlerFn: NoopHandlerFn(),
366+
},
367+
},
326368
{
327369
name: "invalid JSON",
328370
confJSON: `{out:"stderr"}`,
329371
wantErr: "invalid character 'o' looking for beginning of object key string",
330372
},
331373
{
332-
name: "invalid level",
333-
confJSON: `{"level":"INVALID"}`,
334-
wantErr: "invalid log level 'INVALID': slog: level string \"INVALID\": unknown name",
374+
name: "invalid level",
375+
confJSON: `{"level":"INVALID"}`,
376+
wantErr: "invalid log level 'INVALID': slog: level string \"INVALID\": unknown name",
377+
wantErrIs: ErrInvalidLevel,
378+
},
379+
{
380+
name: "invalid levels string",
381+
confJSON: `{"levels":"*=INVALID"}`,
382+
wantErr: "invalid levels value '*=INVALID': invalid log level 'INVALID': slog: level string \"INVALID\": unknown name",
383+
wantErrIs: ErrInvalidLevels,
335384
},
336385
{
337-
name: "invalid levels string",
338-
confJSON: `{"levels":"*=INVALID"}`,
339-
wantErr: "invalid log level 'INVALID': slog: level string \"INVALID\": unknown name",
386+
name: "invalid levels map",
387+
confJSON: `{"levels":{"*":"INVALID"}}`,
388+
wantErr: "invalid log level 'INVALID': slog: level string \"INVALID\": unknown name",
389+
wantErrIs: ErrInvalidLevel,
340390
},
341391
{
342-
name: "invalid levels map",
343-
confJSON: `{"levels":{"*":"INVALID"}}`,
344-
wantErr: "invalid log level 'INVALID': slog: level string \"INVALID\": unknown name",
392+
name: "invalid levels type",
393+
confJSON: `{"levels":1}`,
394+
wantErr: "invalid levels value '1': must be a levels string or map",
395+
wantErrIs: ErrInvalidLevels,
345396
},
346397
{
347-
name: "invalid levels type",
348-
confJSON: `{"levels":1}`,
349-
wantErr: "invalid levels value: 1",
398+
name: "unregistered handler",
399+
confJSON: `{"handler":"notfound"}`,
400+
wantErr: "unregistered handler: 'notfound'",
401+
wantErrIs: ErrUnregisteredHandler,
350402
},
351403
}
352404

353405
for _, test := range tests {
354406
t.Run(test.name, func(t *testing.T) {
355407
var opts HandlerOptions
356408
err := json.Unmarshal([]byte(test.confJSON), &opts)
357-
if test.wantErr != "" {
358-
assert.Error(t, err, test.wantErr)
409+
if test.wantErr != "" || test.wantErrIs != nil {
410+
if test.wantErrIs != nil {
411+
assert.ErrorIs(t, err, test.wantErrIs) //nolint:testifylint
412+
}
413+
if test.wantErr != "" {
414+
assert.EqualError(t, err, test.wantErr) //nolint:testifylint
415+
}
359416
return
360417
}
361418

0 commit comments

Comments
 (0)