Skip to content

Commit c891b6f

Browse files
author
Russ Egan
committed
Added a way to register constructors for named sink types
Also broke up ConfigWithEnv() and added tests, and renamed Config.Encoding to Config.DefaultSink
1 parent 2d091ad commit c891b6f

File tree

6 files changed

+420
-104
lines changed

6 files changed

+420
-104
lines changed

v2/.golangci.yml

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,6 @@ linters-settings:
88
disable:
99
- shadow
1010

11-
paralleltest:
12-
ignore-missing: true
13-
1411
wrapcheck:
1512
# ignoreInterfaceRegexps:
1613
# -
@@ -54,6 +51,7 @@ linters:
5451
- mnd
5552
- godox
5653
- recvcheck # good idea, but can't get it to ignore UnmarshalXXX functions
54+
- paralleltest # noisy, and false "Range statement for test TestXXX does not reinitialise the variable..." errors in non-parallel tests
5755
issues:
5856
# List of regexps of issue texts to exclude, empty list by default.
5957
# But independently from this option we use default exclude patterns,

v2/TODO.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,11 @@
1818
- [x] still not crazy about some of the names, in particular "conf" and "delegate". How about "sink" for the delegate handler?
1919
- [x] add convenience methods for creating a handler *and* creating a new logger from it.
2020
- [x] Add a convenience method for loading configuration from the environment, like in v1
21-
- [ ] Add a way to register additional handlers to "encoder" values in config, and maybe change the name "Encoder" to "Handler", "DefaultDelegate", "DefaultSink", etc
21+
- [x] Add a way to register additional handlers to "encoder" values in config, and maybe change the name "Encoder" to "Handler", "DefaultDelegate", "DefaultSink", etc
2222
- [ ] Add an option to Config for v1 compatibility
2323
- installs the DetailedErrors ReplaceAttr
2424
- And what else?
25-
- [ ] Review ConfigFromEnv(). Not sure if I should break that down more.
25+
- [x] Review ConfigFromEnv(). Not sure if I should break that down more.
2626
- [ ] Docs
2727
- [ ] flumetest, and could this be replaced by https://github.com/neilotoole/slogt/blob/master/slogt.go
2828
- [ ] LoggerWriter, could this be replaced by an off the shelf sink?
@@ -43,4 +43,4 @@
4343
- [ ] A gofix style tool to migrate a codebase from v1 to v2, and migrating from Log() to LogCtx() calls?
4444
- [x] I think the states need to be re-organized back into a parent-child graph, and sinks need to trickle down that tree. Creating all the handlers and states in conf isn't working the way it was intended. Rebuilding the leaf handlers is grouping the cached attrs wrong (need tests to verify this), and is also inefficient, since it creates inefficient calls to the sink's WithAttrs()
4545
- [x] Add a middleware which supports ReplaceAttr. Could be used to add ReplaceAttr support to Handlers which don't natively support it
46-
- [ ] We could then promote ReplaceAttr support to the root of Config. If the selected handler natively supports ReplaceAttr, great, otherwise we can add the middleware. To support this, change the way handlers are registered with Config, so that each registration provides a factory method for building the handler, which can take the Config object, and adapt it to the native options that handler supports.
46+
- [x] We could then promote ReplaceAttr support to the root of Config. If the selected handler natively supports ReplaceAttr, great, otherwise we can add the middleware. To support this, change the way handlers are registered with Config, so that each registration provides a factory method for building the handler, which can take the Config object, and adapt it to the native options that handler supports.

v2/config.go

Lines changed: 122 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,67 @@ import (
99
"os"
1010
"strconv"
1111
"strings"
12+
"sync"
1213

1314
"github.com/ansel1/console-slog"
1415
"github.com/ansel1/merry/v2"
1516
)
1617

18+
type SinkConstructor func(Config) (slog.Handler, error)
19+
20+
var sinkConstructors sync.Map
21+
22+
const (
23+
TextSink = "text"
24+
JSONSink = "json"
25+
ConsoleSink = "console"
26+
TermSink = "term"
27+
TermColorSink = "term-color"
28+
)
29+
30+
func init() {
31+
RegisterSinkConstructor(TextSink, textSinkConstructor)
32+
// for v1 compatibility, "console" is an alias for "text"
33+
RegisterSinkConstructor(ConsoleSink, textSinkConstructor)
34+
RegisterSinkConstructor(JSONSink, jsonSinkConstructor)
35+
RegisterSinkConstructor(TermSink, termSinkConstructor(false))
36+
RegisterSinkConstructor(TermColorSink, termSinkConstructor(true))
37+
}
38+
39+
func RegisterSinkConstructor(name string, constructor SinkConstructor) {
40+
sinkConstructors.Store(name, constructor)
41+
}
42+
43+
func textSinkConstructor(c Config) (slog.Handler, error) {
44+
opts := slog.HandlerOptions{
45+
AddSource: c.AddSource,
46+
ReplaceAttr: ChainReplaceAttrs(c.ReplaceAttrs...),
47+
}
48+
return slog.NewTextHandler(c.Out, &opts), nil
49+
}
50+
51+
func jsonSinkConstructor(c Config) (slog.Handler, error) {
52+
opts := slog.HandlerOptions{
53+
AddSource: c.AddSource,
54+
ReplaceAttr: ChainReplaceAttrs(c.ReplaceAttrs...),
55+
}
56+
return slog.NewJSONHandler(c.Out, &opts), nil
57+
}
58+
59+
func termSinkConstructor(color bool) SinkConstructor {
60+
return func(c Config) (slog.Handler, error) {
61+
return console.NewHandler(c.Out, &console.HandlerOptions{
62+
NoColor: !color,
63+
AddSource: c.AddSource,
64+
Theme: console.NewDefaultTheme(),
65+
ReplaceAttr: ChainReplaceAttrs(c.ReplaceAttrs...),
66+
TimeFormat: "15:04:05.000",
67+
Headers: []string{LoggerKey},
68+
HeaderWidth: 13,
69+
}), nil
70+
}
71+
}
72+
1773
// DefaultConfigEnvVars is a list of the environment variables
1874
// that ConfigFromEnv will search by default.
1975
var DefaultConfigEnvVars = []string{"FLUME"}
@@ -32,50 +88,54 @@ var DefaultConfigEnvVars = []string{"FLUME"}
3288
// If no environment variable is set, it silently does nothing.
3389
//
3490
// If an environment variable with a value is found, but parsing
35-
// fails, an error is printed to stdout, and the error is returned.
91+
// fails, an error is returned.
3692
//
3793
// If envvars is empty, it defaults to DefaultConfigEnvVars.
3894
func ConfigFromEnv(envvars ...string) error {
39-
// todo: We might want to change this to just unmarshal a configuration from the environment
40-
// and return the Config. Then it could be re-used to configure multiple Controllers. It
41-
// also gives the caller the chance to further customize the Config, particularly those attributes
42-
// which can't be set from json.
43-
// We could also have a `MustConfig...` variant which ensures unmarshaling is successful, and panics
44-
// if not? Or a `TryConfig...` variant which prints the error to stdout like this one does?
4595
if len(envvars) == 0 {
4696
envvars = DefaultConfigEnvVars
4797
}
98+
var c Config
99+
err := c.UnmarshalEnv(envvars...)
100+
if err != nil {
101+
return err
102+
}
103+
104+
return c.Configure(Default())
105+
}
48106

49-
var configString string
107+
func MustConfigFromEnv(envvars ...string) {
108+
err := ConfigFromEnv(envvars...)
109+
if err != nil {
110+
panic(err)
111+
}
112+
}
50113

114+
func (c *Config) UnmarshalEnv(envvars ...string) error {
51115
for _, v := range envvars {
52-
configString = os.Getenv(v)
53-
if configString != "" {
54-
var config Config
55-
err := json.Unmarshal([]byte(configString), &config)
116+
if configString := os.Getenv(v); configString != "" {
117+
err := json.Unmarshal([]byte(configString), c)
56118
if err != nil {
57-
err = merry.Prependf(err, "parsing configuration from environment variable %v", v)
58-
fmt.Println("error parsing configuration from environment variable " + v + ": " + err.Error()) //nolint:forbidigo
119+
return fmt.Errorf("parsing configuration from environment variable %v: %w", v, err)
59120
}
60-
return err
121+
return nil
61122
}
62123
}
63-
64124
return nil
65125
}
66126

67127
type Config struct {
68128
// DefaultLevel is the default log level for all loggers not
69129
// otherwise configured by Levels. Defaults to Info.
70130
DefaultLevel slog.Level `json:"defaultLevel,omitempty"`
71-
// Levels configures log levels for particular named loggers.
72-
Levels Levels `json:"levels,omitempty"`
73131
// Encoding sets the logger's encoding. Valid values are "json",
74132
// "text", "ltsv", "term", and "term-color".
75133
//
76134
// For compatibility with flume v1, "console" is also accepted, and
77135
// is an alias for "text"
78-
Encoding string `json:"encoding,omitempty"`
136+
DefaultSink string `json:"defaultSink,omitempty"`
137+
// Levels configures log levels for particular named loggers.
138+
Levels Levels `json:"levels,omitempty"`
79139
// AddSource causes the handler to compute the source code position
80140
// of the log statement and add a SourceKey attribute to the output.
81141
// Defaults to true when the Development flag is set, false otherwise.
@@ -93,8 +153,8 @@ const (
93153

94154
func DevDefaults() Config {
95155
return Config{
96-
Encoding: "term-color",
97-
AddSource: true,
156+
DefaultSink: "term-color",
157+
AddSource: true,
98158
}
99159
}
100160

@@ -181,11 +241,14 @@ func (c *Config) UnmarshalJSON(bytes []byte) error {
181241

182242
s := struct {
183243
config
184-
DefaultLevel any `json:"defaultLevel"`
185-
Level any `json:"level"`
186-
Levels any `json:"levels"`
187-
AddSource *bool `json:"addSource"`
188-
AddCaller *bool `json:"addCaller"`
244+
Sink string `json:"sink"`
245+
DefaultLevel any `json:"defaultLevel"`
246+
Level any `json:"level"`
247+
Levels any `json:"levels"`
248+
AddSource *bool `json:"addSource"`
249+
AddCaller *bool `json:"addCaller"`
250+
Encoding string `json:"encoding"`
251+
Out string `json:"out"`
189252
}{}
190253

191254
if s1.Development {
@@ -240,52 +303,44 @@ func (c *Config) UnmarshalJSON(bytes []byte) error {
240303
s.config.AddSource = *s.AddSource
241304
}
242305

306+
// allow "sink" as alias for "defaultSink"
307+
if s.DefaultSink == "" {
308+
s.DefaultSink = s.Sink
309+
}
310+
311+
// for backward compat with v1, allow "encoding" as
312+
// an alias for "defaultSink"
313+
if s.DefaultSink == "" {
314+
s.DefaultSink = s.Encoding
315+
}
316+
317+
switch s.Out {
318+
case "stdout":
319+
s.config.Out = os.Stdout
320+
case "stderr":
321+
s.config.Out = os.Stderr
322+
}
323+
243324
*c = Config(s.config)
244325

245326
return nil
246327
}
247328

248-
func (c Config) Handler() slog.Handler {
249-
out := c.Out
250-
if out == nil {
251-
out = os.Stdout
329+
func (c Config) Handler() (slog.Handler, error) {
330+
if c.Out == nil {
331+
c.Out = os.Stdout
252332
}
253333

254-
opts := slog.HandlerOptions{
255-
AddSource: c.AddSource,
256-
ReplaceAttr: ChainReplaceAttrs(c.ReplaceAttrs...),
334+
if c.DefaultSink == "" {
335+
c.DefaultSink = JSONSink
257336
}
258337

259-
var handler slog.Handler
260-
261-
switch c.Encoding {
262-
case "text", "console":
263-
handler = slog.NewTextHandler(out, &opts)
264-
case EncodingTerm:
265-
handler = console.NewHandler(out, &console.HandlerOptions{
266-
NoColor: true,
267-
AddSource: c.AddSource,
268-
Theme: console.NewDefaultTheme(),
269-
ReplaceAttr: ChainReplaceAttrs(c.ReplaceAttrs...),
270-
TimeFormat: "15:04:05.000",
271-
Headers: []string{LoggerKey},
272-
HeaderWidth: 13,
273-
})
274-
case EncodingTermColor:
275-
handler = console.NewHandler(out, &console.HandlerOptions{
276-
AddSource: c.AddSource,
277-
Theme: console.NewDefaultTheme(),
278-
ReplaceAttr: ChainReplaceAttrs(c.ReplaceAttrs...),
279-
TimeFormat: "15:04:05.000",
280-
Headers: []string{LoggerKey},
281-
})
282-
case "json":
283-
fallthrough
284-
default:
285-
handler = slog.NewJSONHandler(out, &opts)
338+
v, ok := sinkConstructors.Load(c.DefaultSink)
339+
if !ok {
340+
return nil, errors.New("unknown sink constructor: " + c.DefaultSink)
286341
}
287-
288-
return handler
342+
constructor := v.(SinkConstructor)
343+
return constructor(c)
289344
}
290345

291346
func (c Config) Configure(ctl *Controller) error {
@@ -295,7 +350,11 @@ func (c Config) Configure(ctl *Controller) error {
295350
ctl.SetLevel(name, level)
296351
}
297352

298-
ctl.SetDefaultSink(c.Handler())
353+
h, err := c.Handler()
354+
if err != nil {
355+
return err
356+
}
357+
ctl.SetDefaultSink(h)
299358

300359
return nil
301360
}

0 commit comments

Comments
 (0)