Skip to content

Commit a784287

Browse files
authored
promslog: Make AllowedLevel concurrency safe. (#754)
* promslog: Make AllowedLevel concurrency safe. Needed for prometheus/prometheus#10352 Also I renamed AllowedLevel and AllowedFormat to Level and Format. Default level (and String()) is also now 'info' not empty. It's a breaking change, but I suspect nobody was using those constructs directly, WDYT? Signed-off-by: bwplotka <[email protected]> * info is by default, so no need for the set in New Signed-off-by: bwplotka <[email protected]> --------- Signed-off-by: bwplotka <[email protected]>
1 parent 7684929 commit a784287

File tree

3 files changed

+154
-139
lines changed

3 files changed

+154
-139
lines changed

promslog/flag/flag.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -42,12 +42,12 @@ var FormatFlagHelp = "Output format of log messages. One of: [" + strings.Join(p
4242
// AddFlags adds the flags used by this package to the Kingpin application.
4343
// To use the default Kingpin application, call AddFlags(kingpin.CommandLine)
4444
func AddFlags(a *kingpin.Application, config *promslog.Config) {
45-
config.Level = &promslog.AllowedLevel{}
45+
config.Level = promslog.NewLevel()
4646
a.Flag(LevelFlagName, LevelFlagHelp).
4747
Default("info").HintOptions(promslog.LevelFlagOptions...).
4848
SetValue(config.Level)
4949

50-
config.Format = &promslog.AllowedFormat{}
50+
config.Format = promslog.NewFormat()
5151
a.Flag(FormatFlagName, FormatFlagHelp).
5252
Default("logfmt").HintOptions(promslog.FormatFlagOptions...).
5353
SetValue(config.Format)

promslog/slog.go

+143-128
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import (
2828
"time"
2929
)
3030

31+
// LogStyle represents the common logging formats in the Prometheus ecosystem.
3132
type LogStyle string
3233

3334
const (
@@ -38,115 +39,29 @@ const (
3839
)
3940

4041
var (
41-
LevelFlagOptions = []string{"debug", "info", "warn", "error"}
42+
// LevelFlagOptions represents allowed logging levels.
43+
LevelFlagOptions = []string{"debug", "info", "warn", "error"}
44+
// FormatFlagOptions represents allowed formats.
4245
FormatFlagOptions = []string{"logfmt", "json"}
4346

44-
callerAddFunc = false
45-
defaultWriter = os.Stderr
46-
goKitStyleReplaceAttrFunc = func(groups []string, a slog.Attr) slog.Attr {
47-
key := a.Key
48-
switch key {
49-
case slog.TimeKey, "ts":
50-
if t, ok := a.Value.Any().(time.Time); ok {
51-
a.Key = "ts"
52-
53-
// This timestamp format differs from RFC3339Nano by using .000 instead
54-
// of .999999999 which changes the timestamp from 9 variable to 3 fixed
55-
// decimals (.130 instead of .130987456).
56-
a.Value = slog.StringValue(t.UTC().Format("2006-01-02T15:04:05.000Z07:00"))
57-
} else {
58-
// If we can't cast the any from the value to a
59-
// time.Time, it means the caller logged
60-
// another attribute with a key of `ts`.
61-
// Prevent duplicate keys (necessary for proper
62-
// JSON) by renaming the key to `logged_ts`.
63-
a.Key = reservedKeyPrefix + key
64-
}
65-
case slog.SourceKey, "caller":
66-
if src, ok := a.Value.Any().(*slog.Source); ok {
67-
a.Key = "caller"
68-
switch callerAddFunc {
69-
case true:
70-
a.Value = slog.StringValue(filepath.Base(src.File) + "(" + filepath.Base(src.Function) + "):" + strconv.Itoa(src.Line))
71-
default:
72-
a.Value = slog.StringValue(filepath.Base(src.File) + ":" + strconv.Itoa(src.Line))
73-
}
74-
} else {
75-
// If we can't cast the any from the value to
76-
// an *slog.Source, it means the caller logged
77-
// another attribute with a key of `caller`.
78-
// Prevent duplicate keys (necessary for proper
79-
// JSON) by renaming the key to
80-
// `logged_caller`.
81-
a.Key = reservedKeyPrefix + key
82-
}
83-
case slog.LevelKey:
84-
if lvl, ok := a.Value.Any().(slog.Level); ok {
85-
a.Value = slog.StringValue(strings.ToLower(lvl.String()))
86-
} else {
87-
// If we can't cast the any from the value to
88-
// an slog.Level, it means the caller logged
89-
// another attribute with a key of `level`.
90-
// Prevent duplicate keys (necessary for proper
91-
// JSON) by renaming the key to `logged_level`.
92-
a.Key = reservedKeyPrefix + key
93-
}
94-
default:
95-
}
96-
97-
return a
98-
}
99-
defaultReplaceAttrFunc = func(groups []string, a slog.Attr) slog.Attr {
100-
key := a.Key
101-
switch key {
102-
case slog.TimeKey:
103-
if t, ok := a.Value.Any().(time.Time); ok {
104-
a.Value = slog.TimeValue(t.UTC())
105-
} else {
106-
// If we can't cast the any from the value to a
107-
// time.Time, it means the caller logged
108-
// another attribute with a key of `time`.
109-
// Prevent duplicate keys (necessary for proper
110-
// JSON) by renaming the key to `logged_time`.
111-
a.Key = reservedKeyPrefix + key
112-
}
113-
case slog.SourceKey:
114-
if src, ok := a.Value.Any().(*slog.Source); ok {
115-
a.Value = slog.StringValue(filepath.Base(src.File) + ":" + strconv.Itoa(src.Line))
116-
} else {
117-
// If we can't cast the any from the value to
118-
// an *slog.Source, it means the caller logged
119-
// another attribute with a key of `source`.
120-
// Prevent duplicate keys (necessary for proper
121-
// JSON) by renaming the key to
122-
// `logged_source`.
123-
a.Key = reservedKeyPrefix + key
124-
}
125-
case slog.LevelKey:
126-
if _, ok := a.Value.Any().(slog.Level); !ok {
127-
// If we can't cast the any from the value to
128-
// an slog.Level, it means the caller logged
129-
// another attribute with a key of `level`.
130-
// Prevent duplicate keys (necessary for proper
131-
// JSON) by renaming the key to
132-
// `logged_level`.
133-
a.Key = reservedKeyPrefix + key
134-
}
135-
default:
136-
}
137-
138-
return a
139-
}
47+
defaultWriter = os.Stderr
14048
)
14149

142-
// AllowedLevel is a settable identifier for the minimum level a log entry
143-
// must be have.
144-
type AllowedLevel struct {
145-
s string
50+
// Level controls a logging level, with an info default.
51+
// It wraps slog.LevelVar with string-based level control.
52+
// Level is safe to be used concurrently.
53+
type Level struct {
14654
lvl *slog.LevelVar
14755
}
14856

149-
func (l *AllowedLevel) UnmarshalYAML(unmarshal func(interface{}) error) error {
57+
// NewLevel returns a new Level.
58+
func NewLevel() *Level {
59+
return &Level{
60+
lvl: &slog.LevelVar{},
61+
}
62+
}
63+
64+
func (l *Level) UnmarshalYAML(unmarshal func(interface{}) error) error {
15065
var s string
15166
type plain string
15267
if err := unmarshal((*plain)(&s)); err != nil {
@@ -155,55 +70,60 @@ func (l *AllowedLevel) UnmarshalYAML(unmarshal func(interface{}) error) error {
15570
if s == "" {
15671
return nil
15772
}
158-
lo := &AllowedLevel{}
159-
if err := lo.Set(s); err != nil {
73+
if err := l.Set(s); err != nil {
16074
return err
16175
}
162-
*l = *lo
16376
return nil
16477
}
16578

166-
func (l *AllowedLevel) String() string {
167-
return l.s
168-
}
169-
170-
// Set updates the value of the allowed level.
171-
func (l *AllowedLevel) Set(s string) error {
172-
if l.lvl == nil {
173-
l.lvl = &slog.LevelVar{}
79+
// String returns the current level.
80+
func (l *Level) String() string {
81+
switch l.lvl.Level() {
82+
case slog.LevelDebug:
83+
return "debug"
84+
case slog.LevelInfo:
85+
return "info"
86+
case slog.LevelWarn:
87+
return "warn"
88+
case slog.LevelError:
89+
return "error"
90+
default:
91+
return ""
17492
}
93+
}
17594

95+
// Set updates the logging level with the validation.
96+
func (l *Level) Set(s string) error {
17697
switch strings.ToLower(s) {
17798
case "debug":
17899
l.lvl.Set(slog.LevelDebug)
179-
callerAddFunc = true
180100
case "info":
181101
l.lvl.Set(slog.LevelInfo)
182-
callerAddFunc = false
183102
case "warn":
184103
l.lvl.Set(slog.LevelWarn)
185-
callerAddFunc = false
186104
case "error":
187105
l.lvl.Set(slog.LevelError)
188-
callerAddFunc = false
189106
default:
190107
return fmt.Errorf("unrecognized log level %s", s)
191108
}
192-
l.s = s
193109
return nil
194110
}
195111

196-
// AllowedFormat is a settable identifier for the output format that the logger can have.
197-
type AllowedFormat struct {
112+
// Format controls a logging output format.
113+
// Not concurrency-safe.
114+
type Format struct {
198115
s string
199116
}
200117

201-
func (f *AllowedFormat) String() string {
118+
// NewFormat creates a new Format.
119+
func NewFormat() *Format { return &Format{} }
120+
121+
func (f *Format) String() string {
202122
return f.s
203123
}
204124

205125
// Set updates the value of the allowed format.
206-
func (f *AllowedFormat) Set(s string) error {
126+
func (f *Format) Set(s string) error {
207127
switch s {
208128
case "logfmt", "json":
209129
f.s = s
@@ -215,18 +135,113 @@ func (f *AllowedFormat) Set(s string) error {
215135

216136
// Config is a struct containing configurable settings for the logger
217137
type Config struct {
218-
Level *AllowedLevel
219-
Format *AllowedFormat
138+
Level *Level
139+
Format *Format
220140
Style LogStyle
221141
Writer io.Writer
222142
}
223143

144+
func newGoKitStyleReplaceAttrFunc(lvl *Level) func(groups []string, a slog.Attr) slog.Attr {
145+
return func(groups []string, a slog.Attr) slog.Attr {
146+
key := a.Key
147+
switch key {
148+
case slog.TimeKey, "ts":
149+
if t, ok := a.Value.Any().(time.Time); ok {
150+
a.Key = "ts"
151+
152+
// This timestamp format differs from RFC3339Nano by using .000 instead
153+
// of .999999999 which changes the timestamp from 9 variable to 3 fixed
154+
// decimals (.130 instead of .130987456).
155+
a.Value = slog.StringValue(t.UTC().Format("2006-01-02T15:04:05.000Z07:00"))
156+
} else {
157+
// If we can't cast the any from the value to a
158+
// time.Time, it means the caller logged
159+
// another attribute with a key of `ts`.
160+
// Prevent duplicate keys (necessary for proper
161+
// JSON) by renaming the key to `logged_ts`.
162+
a.Key = reservedKeyPrefix + key
163+
}
164+
case slog.SourceKey, "caller":
165+
if src, ok := a.Value.Any().(*slog.Source); ok {
166+
a.Key = "caller"
167+
switch lvl.String() {
168+
case "debug":
169+
a.Value = slog.StringValue(filepath.Base(src.File) + "(" + filepath.Base(src.Function) + "):" + strconv.Itoa(src.Line))
170+
default:
171+
a.Value = slog.StringValue(filepath.Base(src.File) + ":" + strconv.Itoa(src.Line))
172+
}
173+
} else {
174+
// If we can't cast the any from the value to
175+
// an *slog.Source, it means the caller logged
176+
// another attribute with a key of `caller`.
177+
// Prevent duplicate keys (necessary for proper
178+
// JSON) by renaming the key to
179+
// `logged_caller`.
180+
a.Key = reservedKeyPrefix + key
181+
}
182+
case slog.LevelKey:
183+
if lvl, ok := a.Value.Any().(slog.Level); ok {
184+
a.Value = slog.StringValue(strings.ToLower(lvl.String()))
185+
} else {
186+
// If we can't cast the any from the value to
187+
// an slog.Level, it means the caller logged
188+
// another attribute with a key of `level`.
189+
// Prevent duplicate keys (necessary for proper
190+
// JSON) by renaming the key to `logged_level`.
191+
a.Key = reservedKeyPrefix + key
192+
}
193+
default:
194+
}
195+
return a
196+
}
197+
}
198+
199+
func defaultReplaceAttr(_ []string, a slog.Attr) slog.Attr {
200+
key := a.Key
201+
switch key {
202+
case slog.TimeKey:
203+
if t, ok := a.Value.Any().(time.Time); ok {
204+
a.Value = slog.TimeValue(t.UTC())
205+
} else {
206+
// If we can't cast the any from the value to a
207+
// time.Time, it means the caller logged
208+
// another attribute with a key of `time`.
209+
// Prevent duplicate keys (necessary for proper
210+
// JSON) by renaming the key to `logged_time`.
211+
a.Key = reservedKeyPrefix + key
212+
}
213+
case slog.SourceKey:
214+
if src, ok := a.Value.Any().(*slog.Source); ok {
215+
a.Value = slog.StringValue(filepath.Base(src.File) + ":" + strconv.Itoa(src.Line))
216+
} else {
217+
// If we can't cast the any from the value to
218+
// an *slog.Source, it means the caller logged
219+
// another attribute with a key of `source`.
220+
// Prevent duplicate keys (necessary for proper
221+
// JSON) by renaming the key to
222+
// `logged_source`.
223+
a.Key = reservedKeyPrefix + key
224+
}
225+
case slog.LevelKey:
226+
if _, ok := a.Value.Any().(slog.Level); !ok {
227+
// If we can't cast the any from the value to
228+
// an slog.Level, it means the caller logged
229+
// another attribute with a key of `level`.
230+
// Prevent duplicate keys (necessary for proper
231+
// JSON) by renaming the key to
232+
// `logged_level`.
233+
a.Key = reservedKeyPrefix + key
234+
}
235+
default:
236+
}
237+
return a
238+
}
239+
224240
// New returns a new slog.Logger. Each logged line will be annotated
225241
// with a timestamp. The output always goes to stderr.
226242
func New(config *Config) *slog.Logger {
227243
if config.Level == nil {
228-
config.Level = &AllowedLevel{}
229-
_ = config.Level.Set("info")
244+
config.Level = NewLevel()
230245
}
231246

232247
if config.Writer == nil {
@@ -236,11 +251,11 @@ func New(config *Config) *slog.Logger {
236251
logHandlerOpts := &slog.HandlerOptions{
237252
Level: config.Level.lvl,
238253
AddSource: true,
239-
ReplaceAttr: defaultReplaceAttrFunc,
254+
ReplaceAttr: defaultReplaceAttr,
240255
}
241256

242257
if config.Style == GoKitStyle {
243-
logHandlerOpts.ReplaceAttr = goKitStyleReplaceAttrFunc
258+
logHandlerOpts.ReplaceAttr = newGoKitStyleReplaceAttrFunc(config.Level)
244259
}
245260

246261
if config.Format != nil && config.Format.s == "json" {

0 commit comments

Comments
 (0)