Merge pull request #357 from go-kit/experimental-levels
log/experimental_level
Peter Bourgon authored 7 years ago
GitHub committed 7 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 BenchmarkNopBaseline(b *testing.B) { | |
11 | benchmarkRunner(b, log.NewNopLogger()) | |
12 | } | |
13 | ||
14 | func BenchmarkNopDisallowedLevel(b *testing.B) { | |
15 | benchmarkRunner(b, level.New(log.NewNopLogger(), level.Config{ | |
16 | Allowed: level.AllowInfoAndAbove(), | |
17 | })) | |
18 | } | |
19 | ||
20 | func BenchmarkNopAllowedLevel(b *testing.B) { | |
21 | benchmarkRunner(b, level.New(log.NewNopLogger(), level.Config{ | |
22 | Allowed: level.AllowAll(), | |
23 | })) | |
24 | } | |
25 | ||
26 | func BenchmarkJSONBaseline(b *testing.B) { | |
27 | benchmarkRunner(b, log.NewJSONLogger(ioutil.Discard)) | |
28 | } | |
29 | ||
30 | func BenchmarkJSONDisallowedLevel(b *testing.B) { | |
31 | benchmarkRunner(b, level.New(log.NewJSONLogger(ioutil.Discard), level.Config{ | |
32 | Allowed: level.AllowInfoAndAbove(), | |
33 | })) | |
34 | } | |
35 | ||
36 | func BenchmarkJSONAllowedLevel(b *testing.B) { | |
37 | benchmarkRunner(b, level.New(log.NewJSONLogger(ioutil.Discard), level.Config{ | |
38 | Allowed: level.AllowAll(), | |
39 | })) | |
40 | } | |
41 | ||
42 | func BenchmarkLogfmtBaseline(b *testing.B) { | |
43 | benchmarkRunner(b, log.NewLogfmtLogger(ioutil.Discard)) | |
44 | } | |
45 | ||
46 | func BenchmarkLogfmtDisallowedLevel(b *testing.B) { | |
47 | benchmarkRunner(b, level.New(log.NewLogfmtLogger(ioutil.Discard), level.Config{ | |
48 | Allowed: level.AllowInfoAndAbove(), | |
49 | })) | |
50 | } | |
51 | ||
52 | func BenchmarkLogfmtAllowedLevel(b *testing.B) { | |
53 | benchmarkRunner(b, level.New(log.NewLogfmtLogger(ioutil.Discard), level.Config{ | |
54 | Allowed: level.AllowAll(), | |
55 | })) | |
56 | } | |
57 | ||
58 | func benchmarkRunner(b *testing.B, logger log.Logger) { | |
59 | b.ResetTimer() | |
60 | b.ReportAllocs() | |
61 | for i := 0; i < b.N; i++ { | |
62 | level.Debug(logger).Log("foo", "bar") | |
63 | } | |
64 | } |
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.New. | |
5 | // | |
6 | // var logger log.Logger | |
7 | // logger = log.NewLogfmtLogger(os.Stderr) | |
8 | // logger = level.New(logger, level.Config{Allowed: 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 | // The leveled logger allows precise control over what should happen if a log | |
21 | // event is emitted without a level key, or if a squelched level is used. Check | |
22 | // the Config struct for details. And, you can easily use non-default level | |
23 | // values: create new string constants for whatever you want to change, pass | |
24 | // them explicitly to the Config struct, and write your own level.Foo-style | |
25 | // helper methods. | |
26 | package level |
0 | package level | |
1 | ||
2 | import ( | |
3 | "github.com/go-kit/kit/log" | |
4 | ) | |
5 | ||
6 | var ( | |
7 | levelKey = "level" | |
8 | errorLevelValue = "error" | |
9 | warnLevelValue = "warn" | |
10 | infoLevelValue = "info" | |
11 | debugLevelValue = "debug" | |
12 | ) | |
13 | ||
14 | // AllowAll is an alias for AllowDebugAndAbove. | |
15 | func AllowAll() []string { | |
16 | return AllowDebugAndAbove() | |
17 | } | |
18 | ||
19 | // AllowDebugAndAbove allows all of the four default log levels. | |
20 | // Its return value may be provided as the Allowed parameter in the Config. | |
21 | func AllowDebugAndAbove() []string { | |
22 | return []string{errorLevelValue, warnLevelValue, infoLevelValue, debugLevelValue} | |
23 | } | |
24 | ||
25 | // AllowInfoAndAbove allows the default info, warn, and error log levels. | |
26 | // Its return value may be provided as the Allowed parameter in the Config. | |
27 | func AllowInfoAndAbove() []string { | |
28 | return []string{errorLevelValue, warnLevelValue, infoLevelValue} | |
29 | } | |
30 | ||
31 | // AllowWarnAndAbove allows the default warn and error log levels. | |
32 | // Its return value may be provided as the Allowed parameter in the Config. | |
33 | func AllowWarnAndAbove() []string { | |
34 | return []string{errorLevelValue, warnLevelValue} | |
35 | } | |
36 | ||
37 | // AllowErrorOnly allows only the default error log level. | |
38 | // Its return value may be provided as the Allowed parameter in the Config. | |
39 | func AllowErrorOnly() []string { | |
40 | return []string{errorLevelValue} | |
41 | } | |
42 | ||
43 | // AllowNone allows none of the default log levels. | |
44 | // Its return value may be provided as the Allowed parameter in the Config. | |
45 | func AllowNone() []string { | |
46 | return []string{} | |
47 | } | |
48 | ||
49 | // Error returns a logger with the level key set to ErrorLevelValue. | |
50 | func Error(logger log.Logger) log.Logger { | |
51 | return log.NewContext(logger).With(levelKey, errorLevelValue) | |
52 | } | |
53 | ||
54 | // Warn returns a logger with the level key set to WarnLevelValue. | |
55 | func Warn(logger log.Logger) log.Logger { | |
56 | return log.NewContext(logger).With(levelKey, warnLevelValue) | |
57 | } | |
58 | ||
59 | // Info returns a logger with the level key set to InfoLevelValue. | |
60 | func Info(logger log.Logger) log.Logger { | |
61 | return log.NewContext(logger).With(levelKey, infoLevelValue) | |
62 | } | |
63 | ||
64 | // Debug returns a logger with the level key set to DebugLevelValue. | |
65 | func Debug(logger log.Logger) log.Logger { | |
66 | return log.NewContext(logger).With(levelKey, debugLevelValue) | |
67 | } | |
68 | ||
69 | // Config parameterizes the leveled logger. | |
70 | type Config struct { | |
71 | // Allowed enumerates the accepted log levels. If a log event is encountered | |
72 | // with a level key set to a value that isn't explicitly allowed, the event | |
73 | // will be squelched, and ErrNotAllowed returned. | |
74 | Allowed []string | |
75 | ||
76 | // ErrNotAllowed is returned to the caller when Log is invoked with a level | |
77 | // key that hasn't been explicitly allowed. By default, ErrNotAllowed is | |
78 | // nil; in this case, the log event is squelched with no error. | |
79 | ErrNotAllowed error | |
80 | ||
81 | // SquelchNoLevel will squelch log events with no level key, so that they | |
82 | // don't proceed through to the wrapped logger. If SquelchNoLevel is set to | |
83 | // true and a log event is squelched in this way, ErrNoLevel is returned to | |
84 | // the caller. | |
85 | SquelchNoLevel bool | |
86 | ||
87 | // ErrNoLevel is returned to the caller when SquelchNoLevel is true, and Log | |
88 | // is invoked without a level key. By default, ErrNoLevel is nil; in this | |
89 | // case, the log event is squelched with no error. | |
90 | ErrNoLevel error | |
91 | } | |
92 | ||
93 | // New wraps the logger and implements level checking. See the commentary on the | |
94 | // Config object for a detailed description of how to configure levels. | |
95 | func New(next log.Logger, config Config) log.Logger { | |
96 | return &logger{ | |
97 | next: next, | |
98 | allowed: makeSet(config.Allowed), | |
99 | errNotAllowed: config.ErrNotAllowed, | |
100 | squelchNoLevel: config.SquelchNoLevel, | |
101 | errNoLevel: config.ErrNoLevel, | |
102 | } | |
103 | } | |
104 | ||
105 | type logger struct { | |
106 | next log.Logger | |
107 | allowed map[string]struct{} | |
108 | errNotAllowed error | |
109 | squelchNoLevel bool | |
110 | errNoLevel error | |
111 | } | |
112 | ||
113 | func (l *logger) Log(keyvals ...interface{}) error { | |
114 | var hasLevel, levelAllowed bool | |
115 | for i := 0; i < len(keyvals); i += 2 { | |
116 | if k, ok := keyvals[i].(string); !ok || k != levelKey { | |
117 | continue | |
118 | } | |
119 | hasLevel = true | |
120 | if i >= len(keyvals) { | |
121 | continue | |
122 | } | |
123 | v, ok := keyvals[i+1].(string) | |
124 | if !ok { | |
125 | continue | |
126 | } | |
127 | _, levelAllowed = l.allowed[v] | |
128 | break | |
129 | } | |
130 | if !hasLevel && l.squelchNoLevel { | |
131 | return l.errNoLevel | |
132 | } | |
133 | if hasLevel && !levelAllowed { | |
134 | return l.errNotAllowed | |
135 | } | |
136 | return l.next.Log(keyvals...) | |
137 | } | |
138 | ||
139 | func makeSet(a []string) map[string]struct{} { | |
140 | m := make(map[string]struct{}, len(a)) | |
141 | for _, s := range a { | |
142 | m[s] = struct{}{} | |
143 | } | |
144 | return m | |
145 | } |
0 | package level_test | |
1 | ||
2 | import ( | |
3 | "bytes" | |
4 | "errors" | |
5 | "strings" | |
6 | "testing" | |
7 | ||
8 | "github.com/go-kit/kit/log" | |
9 | "github.com/go-kit/kit/log/experimental_level" | |
10 | ) | |
11 | ||
12 | func TestVariousLevels(t *testing.T) { | |
13 | for _, testcase := range []struct { | |
14 | allowed []string | |
15 | want string | |
16 | }{ | |
17 | { | |
18 | level.AllowAll(), | |
19 | strings.Join([]string{ | |
20 | `{"level":"debug","this is":"debug log"}`, | |
21 | `{"level":"info","this is":"info log"}`, | |
22 | `{"level":"warn","this is":"warn log"}`, | |
23 | `{"level":"error","this is":"error log"}`, | |
24 | }, "\n"), | |
25 | }, | |
26 | { | |
27 | level.AllowDebugAndAbove(), | |
28 | strings.Join([]string{ | |
29 | `{"level":"debug","this is":"debug log"}`, | |
30 | `{"level":"info","this is":"info log"}`, | |
31 | `{"level":"warn","this is":"warn log"}`, | |
32 | `{"level":"error","this is":"error log"}`, | |
33 | }, "\n"), | |
34 | }, | |
35 | { | |
36 | level.AllowInfoAndAbove(), | |
37 | strings.Join([]string{ | |
38 | `{"level":"info","this is":"info log"}`, | |
39 | `{"level":"warn","this is":"warn log"}`, | |
40 | `{"level":"error","this is":"error log"}`, | |
41 | }, "\n"), | |
42 | }, | |
43 | { | |
44 | level.AllowWarnAndAbove(), | |
45 | strings.Join([]string{ | |
46 | `{"level":"warn","this is":"warn log"}`, | |
47 | `{"level":"error","this is":"error log"}`, | |
48 | }, "\n"), | |
49 | }, | |
50 | { | |
51 | level.AllowErrorOnly(), | |
52 | strings.Join([]string{ | |
53 | `{"level":"error","this is":"error log"}`, | |
54 | }, "\n"), | |
55 | }, | |
56 | { | |
57 | level.AllowNone(), | |
58 | ``, | |
59 | }, | |
60 | } { | |
61 | var buf bytes.Buffer | |
62 | logger := level.New(log.NewJSONLogger(&buf), level.Config{Allowed: testcase.allowed}) | |
63 | ||
64 | level.Debug(logger).Log("this is", "debug log") | |
65 | level.Info(logger).Log("this is", "info log") | |
66 | level.Warn(logger).Log("this is", "warn log") | |
67 | level.Error(logger).Log("this is", "error log") | |
68 | ||
69 | if want, have := testcase.want, strings.TrimSpace(buf.String()); want != have { | |
70 | t.Errorf("given Allowed=%v: want\n%s\nhave\n%s", testcase.allowed, want, have) | |
71 | } | |
72 | } | |
73 | } | |
74 | ||
75 | func TestErrNotAllowed(t *testing.T) { | |
76 | myError := errors.New("squelched!") | |
77 | logger := level.New(log.NewNopLogger(), level.Config{ | |
78 | Allowed: level.AllowWarnAndAbove(), | |
79 | ErrNotAllowed: myError, | |
80 | }) | |
81 | ||
82 | if want, have := myError, level.Info(logger).Log("foo", "bar"); want != have { | |
83 | t.Errorf("want %#+v, have %#+v", want, have) | |
84 | } | |
85 | ||
86 | if want, have := error(nil), level.Warn(logger).Log("foo", "bar"); want != have { | |
87 | t.Errorf("want %#+v, have %#+v", want, have) | |
88 | } | |
89 | } | |
90 | ||
91 | func TestErrNoLevel(t *testing.T) { | |
92 | myError := errors.New("no level specified") | |
93 | ||
94 | var buf bytes.Buffer | |
95 | logger := level.New(log.NewJSONLogger(&buf), level.Config{ | |
96 | SquelchNoLevel: true, | |
97 | ErrNoLevel: myError, | |
98 | }) | |
99 | ||
100 | if want, have := myError, logger.Log("foo", "bar"); want != have { | |
101 | t.Errorf("want %v, have %v", want, have) | |
102 | } | |
103 | if want, have := ``, strings.TrimSpace(buf.String()); want != have { | |
104 | t.Errorf("want %q, have %q", want, have) | |
105 | } | |
106 | } | |
107 | ||
108 | func TestAllowNoLevel(t *testing.T) { | |
109 | var buf bytes.Buffer | |
110 | logger := level.New(log.NewJSONLogger(&buf), level.Config{ | |
111 | SquelchNoLevel: false, | |
112 | ErrNoLevel: errors.New("I should never be returned!"), | |
113 | }) | |
114 | ||
115 | if want, have := error(nil), logger.Log("foo", "bar"); want != have { | |
116 | t.Errorf("want %v, have %v", want, have) | |
117 | } | |
118 | if want, have := `{"foo":"bar"}`, strings.TrimSpace(buf.String()); want != have { | |
119 | t.Errorf("want %q, have %q", want, have) | |
120 | } | |
121 | } | |
122 | ||
123 | func TestLevelContext(t *testing.T) { | |
124 | var buf bytes.Buffer | |
125 | ||
126 | // Wrapping the level logger with a context allows users to use | |
127 | // log.DefaultCaller as per normal. | |
128 | var logger log.Logger | |
129 | logger = log.NewLogfmtLogger(&buf) | |
130 | logger = level.New(logger, level.Config{Allowed: level.AllowAll()}) | |
131 | logger = log.NewContext(logger).With("caller", log.DefaultCaller) | |
132 | ||
133 | level.Info(logger).Log("foo", "bar") | |
134 | if want, have := `caller=level_test.go:134 level=info foo=bar`, strings.TrimSpace(buf.String()); want != have { | |
135 | t.Errorf("want %q, have %q", want, have) | |
136 | } | |
137 | } | |
138 | ||
139 | func TestContextLevel(t *testing.T) { | |
140 | var buf bytes.Buffer | |
141 | ||
142 | // Wrapping a context with the level logger still works, but requires users | |
143 | // to specify a higher callstack depth value. | |
144 | var logger log.Logger | |
145 | logger = log.NewLogfmtLogger(&buf) | |
146 | logger = log.NewContext(logger).With("caller", log.Caller(5)) | |
147 | logger = level.New(logger, level.Config{Allowed: level.AllowAll()}) | |
148 | ||
149 | level.Info(logger).Log("foo", "bar") | |
150 | if want, have := `caller=level_test.go:150 level=info foo=bar`, strings.TrimSpace(buf.String()); want != have { | |
151 | t.Errorf("want %q, have %q", want, have) | |
152 | } | |
153 | } |