log/experimental_level: mv to level
Peter Bourgon
6 years ago
0 | package level_test | |
1 | ||
2 | import ( | |
3 | "io/ioutil" | |
4 | "testing" | |
5 | ||
6 | "github.com/go-kit/kit/log" | |
7 | "github.com/go-kit/kit/log/experimental_level" | |
8 | ) | |
9 | ||
10 | func Benchmark(b *testing.B) { | |
11 | contexts := []struct { | |
12 | name string | |
13 | context func(log.Logger) log.Logger | |
14 | }{ | |
15 | {"NoContext", func(l log.Logger) log.Logger { | |
16 | return l | |
17 | }}, | |
18 | {"TimeContext", func(l log.Logger) log.Logger { | |
19 | return log.NewContext(l).With("time", log.DefaultTimestampUTC) | |
20 | }}, | |
21 | {"CallerContext", func(l log.Logger) log.Logger { | |
22 | return log.NewContext(l).With("caller", log.DefaultCaller) | |
23 | }}, | |
24 | {"TimeCallerReqIDContext", func(l log.Logger) log.Logger { | |
25 | return log.NewContext(l).With("time", log.DefaultTimestampUTC, "caller", log.DefaultCaller, "reqID", 29) | |
26 | }}, | |
27 | } | |
28 | ||
29 | loggers := []struct { | |
30 | name string | |
31 | logger log.Logger | |
32 | }{ | |
33 | {"Nop", log.NewNopLogger()}, | |
34 | {"Logfmt", log.NewLogfmtLogger(ioutil.Discard)}, | |
35 | {"JSON", log.NewJSONLogger(ioutil.Discard)}, | |
36 | } | |
37 | ||
38 | filters := []struct { | |
39 | name string | |
40 | filter func(log.Logger) log.Logger | |
41 | }{ | |
42 | {"Baseline", func(l log.Logger) log.Logger { | |
43 | return l | |
44 | }}, | |
45 | {"DisallowedLevel", func(l log.Logger) log.Logger { | |
46 | return level.NewFilter(l, level.AllowInfo()) | |
47 | }}, | |
48 | {"AllowedLevel", func(l log.Logger) log.Logger { | |
49 | return level.NewFilter(l, level.AllowAll()) | |
50 | }}, | |
51 | } | |
52 | ||
53 | for _, c := range contexts { | |
54 | b.Run(c.name, func(b *testing.B) { | |
55 | for _, f := range filters { | |
56 | b.Run(f.name, func(b *testing.B) { | |
57 | for _, l := range loggers { | |
58 | b.Run(l.name, func(b *testing.B) { | |
59 | logger := c.context(f.filter(l.logger)) | |
60 | b.ResetTimer() | |
61 | b.ReportAllocs() | |
62 | for i := 0; i < b.N; i++ { | |
63 | level.Debug(logger).Log("foo", "bar") | |
64 | } | |
65 | }) | |
66 | } | |
67 | }) | |
68 | } | |
69 | }) | |
70 | } | |
71 | } |
0 | // Package level is an EXPERIMENTAL levelled logging package. The API will | |
1 | // definitely have breaking changes and may be deleted altogether. Be warned! | |
2 | // | |
3 | // To use the level package, create a logger as per normal in your func main, | |
4 | // and wrap it with level.NewFilter. | |
5 | // | |
6 | // var logger log.Logger | |
7 | // logger = log.NewLogfmtLogger(os.Stderr) | |
8 | // logger = level.NewFilter(logger, level.AllowInfoAndAbove()) // <-- | |
9 | // logger = log.NewContext(logger).With("ts", log.DefaultTimestampUTC) | |
10 | // | |
11 | // Then, at the callsites, use one of the level.Debug, Info, Warn, or Error | |
12 | // helper methods to emit leveled log events. | |
13 | // | |
14 | // logger.Log("foo", "bar") // as normal, no level | |
15 | // level.Debug(logger).Log("request_id", reqID, "trace_data", trace.Get()) | |
16 | // if value > 100 { | |
17 | // level.Error(logger).Log("value", value) | |
18 | // } | |
19 | // | |
20 | // NewFilter allows precise control over what happens when a log event is | |
21 | // emitted without a level key, or if a squelched level is used. Check the | |
22 | // Option functions for details. | |
23 | package level |
0 | package level_test | |
1 | ||
2 | import ( | |
3 | "errors" | |
4 | "os" | |
5 | ||
6 | "github.com/go-kit/kit/log" | |
7 | "github.com/go-kit/kit/log/experimental_level" | |
8 | ) | |
9 | ||
10 | func Example_basic() { | |
11 | // setup logger with level filter | |
12 | logger := log.NewLogfmtLogger(os.Stdout) | |
13 | logger = level.NewFilter(logger, level.AllowInfo()) | |
14 | logger = log.NewContext(logger).With("caller", log.DefaultCaller) | |
15 | ||
16 | // use level helpers to log at different levels | |
17 | level.Error(logger).Log("err", errors.New("bad data")) | |
18 | level.Info(logger).Log("event", "data saved") | |
19 | level.Debug(logger).Log("next item", 17) // filtered | |
20 | ||
21 | // Output: | |
22 | // level=error caller=example_test.go:18 err="bad data" | |
23 | // level=info caller=example_test.go:19 event="data saved" | |
24 | } |
0 | package level | |
1 | ||
2 | import "github.com/go-kit/kit/log" | |
3 | ||
4 | // Error returns a logger that includes a Key/ErrorValue pair. | |
5 | func Error(logger log.Logger) log.Logger { | |
6 | return log.NewContext(logger).WithPrefix(Key(), ErrorValue()) | |
7 | } | |
8 | ||
9 | // Warn returns a logger that includes a Key/WarnValue pair. | |
10 | func Warn(logger log.Logger) log.Logger { | |
11 | return log.NewContext(logger).WithPrefix(Key(), WarnValue()) | |
12 | } | |
13 | ||
14 | // Info returns a logger that includes a Key/InfoValue pair. | |
15 | func Info(logger log.Logger) log.Logger { | |
16 | return log.NewContext(logger).WithPrefix(Key(), InfoValue()) | |
17 | } | |
18 | ||
19 | // Debug returns a logger that includes a Key/DebugValue pair. | |
20 | func Debug(logger log.Logger) log.Logger { | |
21 | return log.NewContext(logger).WithPrefix(Key(), DebugValue()) | |
22 | } | |
23 | ||
24 | // NewFilter wraps next and implements level filtering. See the commentary on | |
25 | // the Option functions for a detailed description of how to configure levels. | |
26 | // If no options are provided, all leveled log events created with Debug, | |
27 | // Info, Warn or Error helper methods are squelched and non-leveled log | |
28 | // events are passed to next unmodified. | |
29 | func NewFilter(next log.Logger, options ...Option) log.Logger { | |
30 | l := &logger{ | |
31 | next: next, | |
32 | } | |
33 | for _, option := range options { | |
34 | option(l) | |
35 | } | |
36 | return l | |
37 | } | |
38 | ||
39 | type logger struct { | |
40 | next log.Logger | |
41 | allowed level | |
42 | squelchNoLevel bool | |
43 | errNotAllowed error | |
44 | errNoLevel error | |
45 | } | |
46 | ||
47 | func (l *logger) Log(keyvals ...interface{}) error { | |
48 | var hasLevel, levelAllowed bool | |
49 | for i := 1; i < len(keyvals); i += 2 { | |
50 | if v, ok := keyvals[i].(*levelValue); ok { | |
51 | hasLevel = true | |
52 | levelAllowed = l.allowed&v.level != 0 | |
53 | break | |
54 | } | |
55 | } | |
56 | if !hasLevel && l.squelchNoLevel { | |
57 | return l.errNoLevel | |
58 | } | |
59 | if hasLevel && !levelAllowed { | |
60 | return l.errNotAllowed | |
61 | } | |
62 | return l.next.Log(keyvals...) | |
63 | } | |
64 | ||
65 | // Option sets a parameter for the leveled logger. | |
66 | type Option func(*logger) | |
67 | ||
68 | // AllowAll is an alias for AllowDebug. | |
69 | func AllowAll() Option { | |
70 | return AllowDebug() | |
71 | } | |
72 | ||
73 | // AllowDebug allows error, warn, info and debug level log events to pass. | |
74 | func AllowDebug() Option { | |
75 | return allowed(levelError | levelWarn | levelInfo | levelDebug) | |
76 | } | |
77 | ||
78 | // AllowInfo allows error, warn and info level log events to pass. | |
79 | func AllowInfo() Option { | |
80 | return allowed(levelError | levelWarn | levelInfo) | |
81 | } | |
82 | ||
83 | // AllowWarn allows error and warn level log events to pass. | |
84 | func AllowWarn() Option { | |
85 | return allowed(levelError | levelWarn) | |
86 | } | |
87 | ||
88 | // AllowError allows only error level log events to pass. | |
89 | func AllowError() Option { | |
90 | return allowed(levelError) | |
91 | } | |
92 | ||
93 | // AllowNone allows no leveled log events to pass. | |
94 | func AllowNone() Option { | |
95 | return allowed(0) | |
96 | } | |
97 | ||
98 | func allowed(allowed level) Option { | |
99 | return func(l *logger) { l.allowed = allowed } | |
100 | } | |
101 | ||
102 | // ErrNotAllowed sets the error to return from Log when it squelches a log | |
103 | // event disallowed by the configured Allow[Level] option. By default, | |
104 | // ErrNotAllowed is nil; in this case the log event is squelched with no | |
105 | // error. | |
106 | func ErrNotAllowed(err error) Option { | |
107 | return func(l *logger) { l.errNotAllowed = err } | |
108 | } | |
109 | ||
110 | // SquelchNoLevel instructs Log to squelch log events with no level, so that | |
111 | // they don't proceed through to the wrapped logger. If SquelchNoLevel is set | |
112 | // to true and a log event is squelched in this way, the error value | |
113 | // configured with ErrNoLevel is returned to the caller. | |
114 | func SquelchNoLevel(squelch bool) Option { | |
115 | return func(l *logger) { l.squelchNoLevel = squelch } | |
116 | } | |
117 | ||
118 | // ErrNoLevel sets the error to return from Log when it squelches a log event | |
119 | // with no level. By default, ErrNoLevel is nil; in this case the log event is | |
120 | // squelched with no error. | |
121 | func ErrNoLevel(err error) Option { | |
122 | return func(l *logger) { l.errNoLevel = err } | |
123 | } | |
124 | ||
125 | // NewInjector wraps next and returns a logger that adds a Key/level pair to | |
126 | // the beginning of log events that don't already contain a level. In effect, | |
127 | // this gives a default level to logs without a level. | |
128 | func NewInjector(next log.Logger, level Value) log.Logger { | |
129 | return &injector{ | |
130 | next: next, | |
131 | level: level, | |
132 | } | |
133 | } | |
134 | ||
135 | type injector struct { | |
136 | next log.Logger | |
137 | level interface{} | |
138 | } | |
139 | ||
140 | func (l *injector) Log(keyvals ...interface{}) error { | |
141 | for i := 1; i < len(keyvals); i += 2 { | |
142 | if _, ok := keyvals[i].(*levelValue); ok { | |
143 | return l.next.Log(keyvals...) | |
144 | } | |
145 | } | |
146 | kvs := make([]interface{}, len(keyvals)+2) | |
147 | kvs[0], kvs[1] = key, l.level | |
148 | copy(kvs[2:], keyvals) | |
149 | return l.next.Log(kvs...) | |
150 | } | |
151 | ||
152 | // Value is the interface that each of the canonical level values implement. | |
153 | // It contains unexported methods that prevent types from other packages from | |
154 | // implementing it and guaranteeing that NewFilter can distinguish the levels | |
155 | // defined in this package from all other values. | |
156 | type Value interface { | |
157 | String() string | |
158 | levelVal() | |
159 | } | |
160 | ||
161 | // Key returns the unique key added to log events by the loggers in this | |
162 | // package. | |
163 | func Key() interface{} { return key } | |
164 | ||
165 | // ErrorValue returns the unique value added to log events by Error. | |
166 | func ErrorValue() Value { return errorValue } | |
167 | ||
168 | // WarnValue returns the unique value added to log events by Warn. | |
169 | func WarnValue() Value { return warnValue } | |
170 | ||
171 | // InfoValue returns the unique value added to log events by Info. | |
172 | func InfoValue() Value { return infoValue } | |
173 | ||
174 | // DebugValue returns the unique value added to log events by Warn. | |
175 | func DebugValue() Value { return debugValue } | |
176 | ||
177 | var ( | |
178 | // key is of type interfae{} so that it allocates once during package | |
179 | // initialization and avoids allocating every type the value is added to a | |
180 | // []interface{} later. | |
181 | key interface{} = "level" | |
182 | ||
183 | errorValue = &levelValue{level: levelError, name: "error"} | |
184 | warnValue = &levelValue{level: levelWarn, name: "warn"} | |
185 | infoValue = &levelValue{level: levelInfo, name: "info"} | |
186 | debugValue = &levelValue{level: levelDebug, name: "debug"} | |
187 | ) | |
188 | ||
189 | type level byte | |
190 | ||
191 | const ( | |
192 | levelDebug level = 1 << iota | |
193 | levelInfo | |
194 | levelWarn | |
195 | levelError | |
196 | ) | |
197 | ||
198 | type levelValue struct { | |
199 | name string | |
200 | level | |
201 | } | |
202 | ||
203 | func (v *levelValue) String() string { return v.name } | |
204 | func (v *levelValue) levelVal() {} |
0 | package level_test | |
1 | ||
2 | import ( | |
3 | "bytes" | |
4 | "errors" | |
5 | "io" | |
6 | "strings" | |
7 | "testing" | |
8 | ||
9 | "github.com/go-kit/kit/log" | |
10 | "github.com/go-kit/kit/log/experimental_level" | |
11 | ) | |
12 | ||
13 | func TestVariousLevels(t *testing.T) { | |
14 | testCases := []struct { | |
15 | name string | |
16 | allowed level.Option | |
17 | want string | |
18 | }{ | |
19 | { | |
20 | "AllowAll", | |
21 | level.AllowAll(), | |
22 | strings.Join([]string{ | |
23 | `{"level":"debug","this is":"debug log"}`, | |
24 | `{"level":"info","this is":"info log"}`, | |
25 | `{"level":"warn","this is":"warn log"}`, | |
26 | `{"level":"error","this is":"error log"}`, | |
27 | }, "\n"), | |
28 | }, | |
29 | { | |
30 | "AllowDebug", | |
31 | level.AllowDebug(), | |
32 | strings.Join([]string{ | |
33 | `{"level":"debug","this is":"debug log"}`, | |
34 | `{"level":"info","this is":"info log"}`, | |
35 | `{"level":"warn","this is":"warn log"}`, | |
36 | `{"level":"error","this is":"error log"}`, | |
37 | }, "\n"), | |
38 | }, | |
39 | { | |
40 | "AllowDebug", | |
41 | level.AllowInfo(), | |
42 | strings.Join([]string{ | |
43 | `{"level":"info","this is":"info log"}`, | |
44 | `{"level":"warn","this is":"warn log"}`, | |
45 | `{"level":"error","this is":"error log"}`, | |
46 | }, "\n"), | |
47 | }, | |
48 | { | |
49 | "AllowWarn", | |
50 | level.AllowWarn(), | |
51 | strings.Join([]string{ | |
52 | `{"level":"warn","this is":"warn log"}`, | |
53 | `{"level":"error","this is":"error log"}`, | |
54 | }, "\n"), | |
55 | }, | |
56 | { | |
57 | "AllowError", | |
58 | level.AllowError(), | |
59 | strings.Join([]string{ | |
60 | `{"level":"error","this is":"error log"}`, | |
61 | }, "\n"), | |
62 | }, | |
63 | { | |
64 | "AllowNone", | |
65 | level.AllowNone(), | |
66 | ``, | |
67 | }, | |
68 | } | |
69 | ||
70 | for _, tc := range testCases { | |
71 | t.Run(tc.name, func(t *testing.T) { | |
72 | var buf bytes.Buffer | |
73 | logger := level.NewFilter(log.NewJSONLogger(&buf), tc.allowed) | |
74 | ||
75 | level.Debug(logger).Log("this is", "debug log") | |
76 | level.Info(logger).Log("this is", "info log") | |
77 | level.Warn(logger).Log("this is", "warn log") | |
78 | level.Error(logger).Log("this is", "error log") | |
79 | ||
80 | if want, have := tc.want, strings.TrimSpace(buf.String()); want != have { | |
81 | t.Errorf("\nwant:\n%s\nhave:\n%s", want, have) | |
82 | } | |
83 | }) | |
84 | } | |
85 | } | |
86 | ||
87 | func TestErrNotAllowed(t *testing.T) { | |
88 | myError := errors.New("squelched!") | |
89 | opts := []level.Option{ | |
90 | level.AllowWarn(), | |
91 | level.ErrNotAllowed(myError), | |
92 | } | |
93 | logger := level.NewFilter(log.NewNopLogger(), opts...) | |
94 | ||
95 | if want, have := myError, level.Info(logger).Log("foo", "bar"); want != have { | |
96 | t.Errorf("want %#+v, have %#+v", want, have) | |
97 | } | |
98 | ||
99 | if want, have := error(nil), level.Warn(logger).Log("foo", "bar"); want != have { | |
100 | t.Errorf("want %#+v, have %#+v", want, have) | |
101 | } | |
102 | } | |
103 | ||
104 | func TestErrNoLevel(t *testing.T) { | |
105 | myError := errors.New("no level specified") | |
106 | ||
107 | var buf bytes.Buffer | |
108 | opts := []level.Option{ | |
109 | level.SquelchNoLevel(true), | |
110 | level.ErrNoLevel(myError), | |
111 | } | |
112 | logger := level.NewFilter(log.NewJSONLogger(&buf), opts...) | |
113 | ||
114 | if want, have := myError, logger.Log("foo", "bar"); want != have { | |
115 | t.Errorf("want %v, have %v", want, have) | |
116 | } | |
117 | if want, have := ``, strings.TrimSpace(buf.String()); want != have { | |
118 | t.Errorf("\nwant '%s'\nhave '%s'", want, have) | |
119 | } | |
120 | } | |
121 | ||
122 | func TestAllowNoLevel(t *testing.T) { | |
123 | var buf bytes.Buffer | |
124 | opts := []level.Option{ | |
125 | level.SquelchNoLevel(false), | |
126 | level.ErrNoLevel(errors.New("I should never be returned!")), | |
127 | } | |
128 | logger := level.NewFilter(log.NewJSONLogger(&buf), opts...) | |
129 | ||
130 | if want, have := error(nil), logger.Log("foo", "bar"); want != have { | |
131 | t.Errorf("want %v, have %v", want, have) | |
132 | } | |
133 | if want, have := `{"foo":"bar"}`, strings.TrimSpace(buf.String()); want != have { | |
134 | t.Errorf("\nwant '%s'\nhave '%s'", want, have) | |
135 | } | |
136 | } | |
137 | ||
138 | func TestLevelContext(t *testing.T) { | |
139 | var buf bytes.Buffer | |
140 | ||
141 | // Wrapping the level logger with a context allows users to use | |
142 | // log.DefaultCaller as per normal. | |
143 | var logger log.Logger | |
144 | logger = log.NewLogfmtLogger(&buf) | |
145 | logger = level.NewFilter(logger, level.AllowAll()) | |
146 | logger = log.NewContext(logger).With("caller", log.DefaultCaller) | |
147 | ||
148 | level.Info(logger).Log("foo", "bar") | |
149 | if want, have := `level=info caller=level_test.go:149 foo=bar`, strings.TrimSpace(buf.String()); want != have { | |
150 | t.Errorf("\nwant '%s'\nhave '%s'", want, have) | |
151 | } | |
152 | } | |
153 | ||
154 | func TestContextLevel(t *testing.T) { | |
155 | var buf bytes.Buffer | |
156 | ||
157 | // Wrapping a context with the level logger still works, but requires users | |
158 | // to specify a higher callstack depth value. | |
159 | var logger log.Logger | |
160 | logger = log.NewLogfmtLogger(&buf) | |
161 | logger = log.NewContext(logger).With("caller", log.Caller(5)) | |
162 | logger = level.NewFilter(logger, level.AllowAll()) | |
163 | ||
164 | level.Info(logger).Log("foo", "bar") | |
165 | if want, have := `caller=level_test.go:165 level=info foo=bar`, strings.TrimSpace(buf.String()); want != have { | |
166 | t.Errorf("\nwant '%s'\nhave '%s'", want, have) | |
167 | } | |
168 | } | |
169 | ||
170 | func TestLevelFormatting(t *testing.T) { | |
171 | testCases := []struct { | |
172 | name string | |
173 | format func(io.Writer) log.Logger | |
174 | output string | |
175 | }{ | |
176 | { | |
177 | name: "logfmt", | |
178 | format: log.NewLogfmtLogger, | |
179 | output: `level=info foo=bar`, | |
180 | }, | |
181 | { | |
182 | name: "JSON", | |
183 | format: log.NewJSONLogger, | |
184 | output: `{"foo":"bar","level":"info"}`, | |
185 | }, | |
186 | } | |
187 | ||
188 | for _, tc := range testCases { | |
189 | t.Run(tc.name, func(t *testing.T) { | |
190 | var buf bytes.Buffer | |
191 | ||
192 | logger := tc.format(&buf) | |
193 | level.Info(logger).Log("foo", "bar") | |
194 | if want, have := tc.output, strings.TrimSpace(buf.String()); want != have { | |
195 | t.Errorf("\nwant: '%s'\nhave '%s'", want, have) | |
196 | } | |
197 | }) | |
198 | } | |
199 | } | |
200 | ||
201 | func TestInjector(t *testing.T) { | |
202 | var ( | |
203 | output []interface{} | |
204 | logger log.Logger | |
205 | ) | |
206 | ||
207 | logger = log.LoggerFunc(func(keyvals ...interface{}) error { | |
208 | output = keyvals | |
209 | return nil | |
210 | }) | |
211 | logger = level.NewInjector(logger, level.InfoValue()) | |
212 | ||
213 | logger.Log("foo", "bar") | |
214 | if got, want := len(output), 4; got != want { | |
215 | t.Errorf("missing level not injected: got len==%d, want len==%d", got, want) | |
216 | } | |
217 | if got, want := output[0], level.Key(); got != want { | |
218 | t.Errorf("wrong level key: got %#v, want %#v", got, want) | |
219 | } | |
220 | if got, want := output[1], level.InfoValue(); got != want { | |
221 | t.Errorf("wrong level value: got %#v, want %#v", got, want) | |
222 | } | |
223 | ||
224 | level.Error(logger).Log("foo", "bar") | |
225 | if got, want := len(output), 4; got != want { | |
226 | t.Errorf("leveled record modified: got len==%d, want len==%d", got, want) | |
227 | } | |
228 | if got, want := output[0], level.Key(); got != want { | |
229 | t.Errorf("wrong level key: got %#v, want %#v", got, want) | |
230 | } | |
231 | if got, want := output[1], level.ErrorValue(); got != want { | |
232 | t.Errorf("wrong level value: got %#v, want %#v", got, want) | |
233 | } | |
234 | } |
0 | package level_test | |
1 | ||
2 | import ( | |
3 | "io/ioutil" | |
4 | "testing" | |
5 | ||
6 | "github.com/go-kit/kit/log" | |
7 | "github.com/go-kit/kit/log/level" | |
8 | ) | |
9 | ||
10 | func Benchmark(b *testing.B) { | |
11 | contexts := []struct { | |
12 | name string | |
13 | context func(log.Logger) log.Logger | |
14 | }{ | |
15 | {"NoContext", func(l log.Logger) log.Logger { | |
16 | return l | |
17 | }}, | |
18 | {"TimeContext", func(l log.Logger) log.Logger { | |
19 | return log.NewContext(l).With("time", log.DefaultTimestampUTC) | |
20 | }}, | |
21 | {"CallerContext", func(l log.Logger) log.Logger { | |
22 | return log.NewContext(l).With("caller", log.DefaultCaller) | |
23 | }}, | |
24 | {"TimeCallerReqIDContext", func(l log.Logger) log.Logger { | |
25 | return log.NewContext(l).With("time", log.DefaultTimestampUTC, "caller", log.DefaultCaller, "reqID", 29) | |
26 | }}, | |
27 | } | |
28 | ||
29 | loggers := []struct { | |
30 | name string | |
31 | logger log.Logger | |
32 | }{ | |
33 | {"Nop", log.NewNopLogger()}, | |
34 | {"Logfmt", log.NewLogfmtLogger(ioutil.Discard)}, | |
35 | {"JSON", log.NewJSONLogger(ioutil.Discard)}, | |
36 | } | |
37 | ||
38 | filters := []struct { | |
39 | name string | |
40 | filter func(log.Logger) log.Logger | |
41 | }{ | |
42 | {"Baseline", func(l log.Logger) log.Logger { | |
43 | return l | |
44 | }}, | |
45 | {"DisallowedLevel", func(l log.Logger) log.Logger { | |
46 | return level.NewFilter(l, level.AllowInfo()) | |
47 | }}, | |
48 | {"AllowedLevel", func(l log.Logger) log.Logger { | |
49 | return level.NewFilter(l, level.AllowAll()) | |
50 | }}, | |
51 | } | |
52 | ||
53 | for _, c := range contexts { | |
54 | b.Run(c.name, func(b *testing.B) { | |
55 | for _, f := range filters { | |
56 | b.Run(f.name, func(b *testing.B) { | |
57 | for _, l := range loggers { | |
58 | b.Run(l.name, func(b *testing.B) { | |
59 | logger := c.context(f.filter(l.logger)) | |
60 | b.ResetTimer() | |
61 | b.ReportAllocs() | |
62 | for i := 0; i < b.N; i++ { | |
63 | level.Debug(logger).Log("foo", "bar") | |
64 | } | |
65 | }) | |
66 | } | |
67 | }) | |
68 | } | |
69 | }) | |
70 | } | |
71 | } |
0 | // Package level is an EXPERIMENTAL levelled logging package. The API will | |
1 | // definitely have breaking changes and may be deleted altogether. Be warned! | |
2 | // | |
3 | // To use the level package, create a logger as per normal in your func main, | |
4 | // and wrap it with level.NewFilter. | |
5 | // | |
6 | // var logger log.Logger | |
7 | // logger = log.NewLogfmtLogger(os.Stderr) | |
8 | // logger = level.NewFilter(logger, level.AllowInfoAndAbove()) // <-- | |
9 | // logger = log.NewContext(logger).With("ts", log.DefaultTimestampUTC) | |
10 | // | |
11 | // Then, at the callsites, use one of the level.Debug, Info, Warn, or Error | |
12 | // helper methods to emit leveled log events. | |
13 | // | |
14 | // logger.Log("foo", "bar") // as normal, no level | |
15 | // level.Debug(logger).Log("request_id", reqID, "trace_data", trace.Get()) | |
16 | // if value > 100 { | |
17 | // level.Error(logger).Log("value", value) | |
18 | // } | |
19 | // | |
20 | // NewFilter allows precise control over what happens when a log event is | |
21 | // emitted without a level key, or if a squelched level is used. Check the | |
22 | // Option functions for details. | |
23 | package level |
0 | package level_test | |
1 | ||
2 | import ( | |
3 | "errors" | |
4 | "os" | |
5 | ||
6 | "github.com/go-kit/kit/log" | |
7 | "github.com/go-kit/kit/log/level" | |
8 | ) | |
9 | ||
10 | func Example_basic() { | |
11 | // setup logger with level filter | |
12 | logger := log.NewLogfmtLogger(os.Stdout) | |
13 | logger = level.NewFilter(logger, level.AllowInfo()) | |
14 | logger = log.NewContext(logger).With("caller", log.DefaultCaller) | |
15 | ||
16 | // use level helpers to log at different levels | |
17 | level.Error(logger).Log("err", errors.New("bad data")) | |
18 | level.Info(logger).Log("event", "data saved") | |
19 | level.Debug(logger).Log("next item", 17) // filtered | |
20 | ||
21 | // Output: | |
22 | // level=error caller=example_test.go:18 err="bad data" | |
23 | // level=info caller=example_test.go:19 event="data saved" | |
24 | } |
0 | package level | |
1 | ||
2 | import "github.com/go-kit/kit/log" | |
3 | ||
4 | // Error returns a logger that includes a Key/ErrorValue pair. | |
5 | func Error(logger log.Logger) log.Logger { | |
6 | return log.NewContext(logger).WithPrefix(Key(), ErrorValue()) | |
7 | } | |
8 | ||
9 | // Warn returns a logger that includes a Key/WarnValue pair. | |
10 | func Warn(logger log.Logger) log.Logger { | |
11 | return log.NewContext(logger).WithPrefix(Key(), WarnValue()) | |
12 | } | |
13 | ||
14 | // Info returns a logger that includes a Key/InfoValue pair. | |
15 | func Info(logger log.Logger) log.Logger { | |
16 | return log.NewContext(logger).WithPrefix(Key(), InfoValue()) | |
17 | } | |
18 | ||
19 | // Debug returns a logger that includes a Key/DebugValue pair. | |
20 | func Debug(logger log.Logger) log.Logger { | |
21 | return log.NewContext(logger).WithPrefix(Key(), DebugValue()) | |
22 | } | |
23 | ||
24 | // NewFilter wraps next and implements level filtering. See the commentary on | |
25 | // the Option functions for a detailed description of how to configure levels. | |
26 | // If no options are provided, all leveled log events created with Debug, | |
27 | // Info, Warn or Error helper methods are squelched and non-leveled log | |
28 | // events are passed to next unmodified. | |
29 | func NewFilter(next log.Logger, options ...Option) log.Logger { | |
30 | l := &logger{ | |
31 | next: next, | |
32 | } | |
33 | for _, option := range options { | |
34 | option(l) | |
35 | } | |
36 | return l | |
37 | } | |
38 | ||
39 | type logger struct { | |
40 | next log.Logger | |
41 | allowed level | |
42 | squelchNoLevel bool | |
43 | errNotAllowed error | |
44 | errNoLevel error | |
45 | } | |
46 | ||
47 | func (l *logger) Log(keyvals ...interface{}) error { | |
48 | var hasLevel, levelAllowed bool | |
49 | for i := 1; i < len(keyvals); i += 2 { | |
50 | if v, ok := keyvals[i].(*levelValue); ok { | |
51 | hasLevel = true | |
52 | levelAllowed = l.allowed&v.level != 0 | |
53 | break | |
54 | } | |
55 | } | |
56 | if !hasLevel && l.squelchNoLevel { | |
57 | return l.errNoLevel | |
58 | } | |
59 | if hasLevel && !levelAllowed { | |
60 | return l.errNotAllowed | |
61 | } | |
62 | return l.next.Log(keyvals...) | |
63 | } | |
64 | ||
65 | // Option sets a parameter for the leveled logger. | |
66 | type Option func(*logger) | |
67 | ||
68 | // AllowAll is an alias for AllowDebug. | |
69 | func AllowAll() Option { | |
70 | return AllowDebug() | |
71 | } | |
72 | ||
73 | // AllowDebug allows error, warn, info and debug level log events to pass. | |
74 | func AllowDebug() Option { | |
75 | return allowed(levelError | levelWarn | levelInfo | levelDebug) | |
76 | } | |
77 | ||
78 | // AllowInfo allows error, warn and info level log events to pass. | |
79 | func AllowInfo() Option { | |
80 | return allowed(levelError | levelWarn | levelInfo) | |
81 | } | |
82 | ||
83 | // AllowWarn allows error and warn level log events to pass. | |
84 | func AllowWarn() Option { | |
85 | return allowed(levelError | levelWarn) | |
86 | } | |
87 | ||
88 | // AllowError allows only error level log events to pass. | |
89 | func AllowError() Option { | |
90 | return allowed(levelError) | |
91 | } | |
92 | ||
93 | // AllowNone allows no leveled log events to pass. | |
94 | func AllowNone() Option { | |
95 | return allowed(0) | |
96 | } | |
97 | ||
98 | func allowed(allowed level) Option { | |
99 | return func(l *logger) { l.allowed = allowed } | |
100 | } | |
101 | ||
102 | // ErrNotAllowed sets the error to return from Log when it squelches a log | |
103 | // event disallowed by the configured Allow[Level] option. By default, | |
104 | // ErrNotAllowed is nil; in this case the log event is squelched with no | |
105 | // error. | |
106 | func ErrNotAllowed(err error) Option { | |
107 | return func(l *logger) { l.errNotAllowed = err } | |
108 | } | |
109 | ||
110 | // SquelchNoLevel instructs Log to squelch log events with no level, so that | |
111 | // they don't proceed through to the wrapped logger. If SquelchNoLevel is set | |
112 | // to true and a log event is squelched in this way, the error value | |
113 | // configured with ErrNoLevel is returned to the caller. | |
114 | func SquelchNoLevel(squelch bool) Option { | |
115 | return func(l *logger) { l.squelchNoLevel = squelch } | |
116 | } | |
117 | ||
118 | // ErrNoLevel sets the error to return from Log when it squelches a log event | |
119 | // with no level. By default, ErrNoLevel is nil; in this case the log event is | |
120 | // squelched with no error. | |
121 | func ErrNoLevel(err error) Option { | |
122 | return func(l *logger) { l.errNoLevel = err } | |
123 | } | |
124 | ||
125 | // NewInjector wraps next and returns a logger that adds a Key/level pair to | |
126 | // the beginning of log events that don't already contain a level. In effect, | |
127 | // this gives a default level to logs without a level. | |
128 | func NewInjector(next log.Logger, level Value) log.Logger { | |
129 | return &injector{ | |
130 | next: next, | |
131 | level: level, | |
132 | } | |
133 | } | |
134 | ||
135 | type injector struct { | |
136 | next log.Logger | |
137 | level interface{} | |
138 | } | |
139 | ||
140 | func (l *injector) Log(keyvals ...interface{}) error { | |
141 | for i := 1; i < len(keyvals); i += 2 { | |
142 | if _, ok := keyvals[i].(*levelValue); ok { | |
143 | return l.next.Log(keyvals...) | |
144 | } | |
145 | } | |
146 | kvs := make([]interface{}, len(keyvals)+2) | |
147 | kvs[0], kvs[1] = key, l.level | |
148 | copy(kvs[2:], keyvals) | |
149 | return l.next.Log(kvs...) | |
150 | } | |
151 | ||
152 | // Value is the interface that each of the canonical level values implement. | |
153 | // It contains unexported methods that prevent types from other packages from | |
154 | // implementing it and guaranteeing that NewFilter can distinguish the levels | |
155 | // defined in this package from all other values. | |
156 | type Value interface { | |
157 | String() string | |
158 | levelVal() | |
159 | } | |
160 | ||
161 | // Key returns the unique key added to log events by the loggers in this | |
162 | // package. | |
163 | func Key() interface{} { return key } | |
164 | ||
165 | // ErrorValue returns the unique value added to log events by Error. | |
166 | func ErrorValue() Value { return errorValue } | |
167 | ||
168 | // WarnValue returns the unique value added to log events by Warn. | |
169 | func WarnValue() Value { return warnValue } | |
170 | ||
171 | // InfoValue returns the unique value added to log events by Info. | |
172 | func InfoValue() Value { return infoValue } | |
173 | ||
174 | // DebugValue returns the unique value added to log events by Warn. | |
175 | func DebugValue() Value { return debugValue } | |
176 | ||
177 | var ( | |
178 | // key is of type interfae{} so that it allocates once during package | |
179 | // initialization and avoids allocating every type the value is added to a | |
180 | // []interface{} later. | |
181 | key interface{} = "level" | |
182 | ||
183 | errorValue = &levelValue{level: levelError, name: "error"} | |
184 | warnValue = &levelValue{level: levelWarn, name: "warn"} | |
185 | infoValue = &levelValue{level: levelInfo, name: "info"} | |
186 | debugValue = &levelValue{level: levelDebug, name: "debug"} | |
187 | ) | |
188 | ||
189 | type level byte | |
190 | ||
191 | const ( | |
192 | levelDebug level = 1 << iota | |
193 | levelInfo | |
194 | levelWarn | |
195 | levelError | |
196 | ) | |
197 | ||
198 | type levelValue struct { | |
199 | name string | |
200 | level | |
201 | } | |
202 | ||
203 | func (v *levelValue) String() string { return v.name } | |
204 | func (v *levelValue) levelVal() {} |
0 | package level_test | |
1 | ||
2 | import ( | |
3 | "bytes" | |
4 | "errors" | |
5 | "io" | |
6 | "strings" | |
7 | "testing" | |
8 | ||
9 | "github.com/go-kit/kit/log" | |
10 | "github.com/go-kit/kit/log/level" | |
11 | ) | |
12 | ||
13 | func TestVariousLevels(t *testing.T) { | |
14 | testCases := []struct { | |
15 | name string | |
16 | allowed level.Option | |
17 | want string | |
18 | }{ | |
19 | { | |
20 | "AllowAll", | |
21 | level.AllowAll(), | |
22 | strings.Join([]string{ | |
23 | `{"level":"debug","this is":"debug log"}`, | |
24 | `{"level":"info","this is":"info log"}`, | |
25 | `{"level":"warn","this is":"warn log"}`, | |
26 | `{"level":"error","this is":"error log"}`, | |
27 | }, "\n"), | |
28 | }, | |
29 | { | |
30 | "AllowDebug", | |
31 | level.AllowDebug(), | |
32 | strings.Join([]string{ | |
33 | `{"level":"debug","this is":"debug log"}`, | |
34 | `{"level":"info","this is":"info log"}`, | |
35 | `{"level":"warn","this is":"warn log"}`, | |
36 | `{"level":"error","this is":"error log"}`, | |
37 | }, "\n"), | |
38 | }, | |
39 | { | |
40 | "AllowDebug", | |
41 | level.AllowInfo(), | |
42 | strings.Join([]string{ | |
43 | `{"level":"info","this is":"info log"}`, | |
44 | `{"level":"warn","this is":"warn log"}`, | |
45 | `{"level":"error","this is":"error log"}`, | |
46 | }, "\n"), | |
47 | }, | |
48 | { | |
49 | "AllowWarn", | |
50 | level.AllowWarn(), | |
51 | strings.Join([]string{ | |
52 | `{"level":"warn","this is":"warn log"}`, | |
53 | `{"level":"error","this is":"error log"}`, | |
54 | }, "\n"), | |
55 | }, | |
56 | { | |
57 | "AllowError", | |
58 | level.AllowError(), | |
59 | strings.Join([]string{ | |
60 | `{"level":"error","this is":"error log"}`, | |
61 | }, "\n"), | |
62 | }, | |
63 | { | |
64 | "AllowNone", | |
65 | level.AllowNone(), | |
66 | ``, | |
67 | }, | |
68 | } | |
69 | ||
70 | for _, tc := range testCases { | |
71 | t.Run(tc.name, func(t *testing.T) { | |
72 | var buf bytes.Buffer | |
73 | logger := level.NewFilter(log.NewJSONLogger(&buf), tc.allowed) | |
74 | ||
75 | level.Debug(logger).Log("this is", "debug log") | |
76 | level.Info(logger).Log("this is", "info log") | |
77 | level.Warn(logger).Log("this is", "warn log") | |
78 | level.Error(logger).Log("this is", "error log") | |
79 | ||
80 | if want, have := tc.want, strings.TrimSpace(buf.String()); want != have { | |
81 | t.Errorf("\nwant:\n%s\nhave:\n%s", want, have) | |
82 | } | |
83 | }) | |
84 | } | |
85 | } | |
86 | ||
87 | func TestErrNotAllowed(t *testing.T) { | |
88 | myError := errors.New("squelched!") | |
89 | opts := []level.Option{ | |
90 | level.AllowWarn(), | |
91 | level.ErrNotAllowed(myError), | |
92 | } | |
93 | logger := level.NewFilter(log.NewNopLogger(), opts...) | |
94 | ||
95 | if want, have := myError, level.Info(logger).Log("foo", "bar"); want != have { | |
96 | t.Errorf("want %#+v, have %#+v", want, have) | |
97 | } | |
98 | ||
99 | if want, have := error(nil), level.Warn(logger).Log("foo", "bar"); want != have { | |
100 | t.Errorf("want %#+v, have %#+v", want, have) | |
101 | } | |
102 | } | |
103 | ||
104 | func TestErrNoLevel(t *testing.T) { | |
105 | myError := errors.New("no level specified") | |
106 | ||
107 | var buf bytes.Buffer | |
108 | opts := []level.Option{ | |
109 | level.SquelchNoLevel(true), | |
110 | level.ErrNoLevel(myError), | |
111 | } | |
112 | logger := level.NewFilter(log.NewJSONLogger(&buf), opts...) | |
113 | ||
114 | if want, have := myError, logger.Log("foo", "bar"); want != have { | |
115 | t.Errorf("want %v, have %v", want, have) | |
116 | } | |
117 | if want, have := ``, strings.TrimSpace(buf.String()); want != have { | |
118 | t.Errorf("\nwant '%s'\nhave '%s'", want, have) | |
119 | } | |
120 | } | |
121 | ||
122 | func TestAllowNoLevel(t *testing.T) { | |
123 | var buf bytes.Buffer | |
124 | opts := []level.Option{ | |
125 | level.SquelchNoLevel(false), | |
126 | level.ErrNoLevel(errors.New("I should never be returned!")), | |
127 | } | |
128 | logger := level.NewFilter(log.NewJSONLogger(&buf), opts...) | |
129 | ||
130 | if want, have := error(nil), logger.Log("foo", "bar"); want != have { | |
131 | t.Errorf("want %v, have %v", want, have) | |
132 | } | |
133 | if want, have := `{"foo":"bar"}`, strings.TrimSpace(buf.String()); want != have { | |
134 | t.Errorf("\nwant '%s'\nhave '%s'", want, have) | |
135 | } | |
136 | } | |
137 | ||
138 | func TestLevelContext(t *testing.T) { | |
139 | var buf bytes.Buffer | |
140 | ||
141 | // Wrapping the level logger with a context allows users to use | |
142 | // log.DefaultCaller as per normal. | |
143 | var logger log.Logger | |
144 | logger = log.NewLogfmtLogger(&buf) | |
145 | logger = level.NewFilter(logger, level.AllowAll()) | |
146 | logger = log.NewContext(logger).With("caller", log.DefaultCaller) | |
147 | ||
148 | level.Info(logger).Log("foo", "bar") | |
149 | if want, have := `level=info caller=level_test.go:149 foo=bar`, strings.TrimSpace(buf.String()); want != have { | |
150 | t.Errorf("\nwant '%s'\nhave '%s'", want, have) | |
151 | } | |
152 | } | |
153 | ||
154 | func TestContextLevel(t *testing.T) { | |
155 | var buf bytes.Buffer | |
156 | ||
157 | // Wrapping a context with the level logger still works, but requires users | |
158 | // to specify a higher callstack depth value. | |
159 | var logger log.Logger | |
160 | logger = log.NewLogfmtLogger(&buf) | |
161 | logger = log.NewContext(logger).With("caller", log.Caller(5)) | |
162 | logger = level.NewFilter(logger, level.AllowAll()) | |
163 | ||
164 | level.Info(logger).Log("foo", "bar") | |
165 | if want, have := `caller=level_test.go:165 level=info foo=bar`, strings.TrimSpace(buf.String()); want != have { | |
166 | t.Errorf("\nwant '%s'\nhave '%s'", want, have) | |
167 | } | |
168 | } | |
169 | ||
170 | func TestLevelFormatting(t *testing.T) { | |
171 | testCases := []struct { | |
172 | name string | |
173 | format func(io.Writer) log.Logger | |
174 | output string | |
175 | }{ | |
176 | { | |
177 | name: "logfmt", | |
178 | format: log.NewLogfmtLogger, | |
179 | output: `level=info foo=bar`, | |
180 | }, | |
181 | { | |
182 | name: "JSON", | |
183 | format: log.NewJSONLogger, | |
184 | output: `{"foo":"bar","level":"info"}`, | |
185 | }, | |
186 | } | |
187 | ||
188 | for _, tc := range testCases { | |
189 | t.Run(tc.name, func(t *testing.T) { | |
190 | var buf bytes.Buffer | |
191 | ||
192 | logger := tc.format(&buf) | |
193 | level.Info(logger).Log("foo", "bar") | |
194 | if want, have := tc.output, strings.TrimSpace(buf.String()); want != have { | |
195 | t.Errorf("\nwant: '%s'\nhave '%s'", want, have) | |
196 | } | |
197 | }) | |
198 | } | |
199 | } | |
200 | ||
201 | func TestInjector(t *testing.T) { | |
202 | var ( | |
203 | output []interface{} | |
204 | logger log.Logger | |
205 | ) | |
206 | ||
207 | logger = log.LoggerFunc(func(keyvals ...interface{}) error { | |
208 | output = keyvals | |
209 | return nil | |
210 | }) | |
211 | logger = level.NewInjector(logger, level.InfoValue()) | |
212 | ||
213 | logger.Log("foo", "bar") | |
214 | if got, want := len(output), 4; got != want { | |
215 | t.Errorf("missing level not injected: got len==%d, want len==%d", got, want) | |
216 | } | |
217 | if got, want := output[0], level.Key(); got != want { | |
218 | t.Errorf("wrong level key: got %#v, want %#v", got, want) | |
219 | } | |
220 | if got, want := output[1], level.InfoValue(); got != want { | |
221 | t.Errorf("wrong level value: got %#v, want %#v", got, want) | |
222 | } | |
223 | ||
224 | level.Error(logger).Log("foo", "bar") | |
225 | if got, want := len(output), 4; got != want { | |
226 | t.Errorf("leveled record modified: got len==%d, want len==%d", got, want) | |
227 | } | |
228 | if got, want := output[0], level.Key(); got != want { | |
229 | t.Errorf("wrong level key: got %#v, want %#v", got, want) | |
230 | } | |
231 | if got, want := output[1], level.ErrorValue(); got != want { | |
232 | t.Errorf("wrong level value: got %#v, want %#v", got, want) | |
233 | } | |
234 | } |