@@ -28,6 +28,7 @@ import (
28
28
"time"
29
29
)
30
30
31
+ // LogStyle represents the common logging formats in the Prometheus ecosystem.
31
32
type LogStyle string
32
33
33
34
const (
@@ -38,115 +39,29 @@ const (
38
39
)
39
40
40
41
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.
42
45
FormatFlagOptions = []string {"logfmt" , "json" }
43
46
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
140
48
)
141
49
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 {
146
54
lvl * slog.LevelVar
147
55
}
148
56
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 {
150
65
var s string
151
66
type plain string
152
67
if err := unmarshal ((* plain )(& s )); err != nil {
@@ -155,55 +70,60 @@ func (l *AllowedLevel) UnmarshalYAML(unmarshal func(interface{}) error) error {
155
70
if s == "" {
156
71
return nil
157
72
}
158
- lo := & AllowedLevel {}
159
- if err := lo .Set (s ); err != nil {
73
+ if err := l .Set (s ); err != nil {
160
74
return err
161
75
}
162
- * l = * lo
163
76
return nil
164
77
}
165
78
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 ""
174
92
}
93
+ }
175
94
95
+ // Set updates the logging level with the validation.
96
+ func (l * Level ) Set (s string ) error {
176
97
switch strings .ToLower (s ) {
177
98
case "debug" :
178
99
l .lvl .Set (slog .LevelDebug )
179
- callerAddFunc = true
180
100
case "info" :
181
101
l .lvl .Set (slog .LevelInfo )
182
- callerAddFunc = false
183
102
case "warn" :
184
103
l .lvl .Set (slog .LevelWarn )
185
- callerAddFunc = false
186
104
case "error" :
187
105
l .lvl .Set (slog .LevelError )
188
- callerAddFunc = false
189
106
default :
190
107
return fmt .Errorf ("unrecognized log level %s" , s )
191
108
}
192
- l .s = s
193
109
return nil
194
110
}
195
111
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 {
198
115
s string
199
116
}
200
117
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 {
202
122
return f .s
203
123
}
204
124
205
125
// Set updates the value of the allowed format.
206
- func (f * AllowedFormat ) Set (s string ) error {
126
+ func (f * Format ) Set (s string ) error {
207
127
switch s {
208
128
case "logfmt" , "json" :
209
129
f .s = s
@@ -215,18 +135,113 @@ func (f *AllowedFormat) Set(s string) error {
215
135
216
136
// Config is a struct containing configurable settings for the logger
217
137
type Config struct {
218
- Level * AllowedLevel
219
- Format * AllowedFormat
138
+ Level * Level
139
+ Format * Format
220
140
Style LogStyle
221
141
Writer io.Writer
222
142
}
223
143
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
+
224
240
// New returns a new slog.Logger. Each logged line will be annotated
225
241
// with a timestamp. The output always goes to stderr.
226
242
func New (config * Config ) * slog.Logger {
227
243
if config .Level == nil {
228
- config .Level = & AllowedLevel {}
229
- _ = config .Level .Set ("info" )
244
+ config .Level = NewLevel ()
230
245
}
231
246
232
247
if config .Writer == nil {
@@ -236,11 +251,11 @@ func New(config *Config) *slog.Logger {
236
251
logHandlerOpts := & slog.HandlerOptions {
237
252
Level : config .Level .lvl ,
238
253
AddSource : true ,
239
- ReplaceAttr : defaultReplaceAttrFunc ,
254
+ ReplaceAttr : defaultReplaceAttr ,
240
255
}
241
256
242
257
if config .Style == GoKitStyle {
243
- logHandlerOpts .ReplaceAttr = goKitStyleReplaceAttrFunc
258
+ logHandlerOpts .ReplaceAttr = newGoKitStyleReplaceAttrFunc ( config . Level )
244
259
}
245
260
246
261
if config .Format != nil && config .Format .s == "json" {
0 commit comments