Codebase list golang-github-go-kit-kit / d3b614b
Merge pull request #357 from go-kit/experimental-levels log/experimental_level Peter Bourgon authored 7 years ago GitHub committed 7 years ago
5 changed file(s) with 400 addition(s) and 0 deletion(s). Raw diff Collapse all Expand all
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 }
1515 t.Error(err)
1616 }
1717 }
18
19 func BenchmarkNopLoggerSimple(b *testing.B) {
20 benchmarkRunner(b, log.NewNopLogger(), baseMessage)
21 }
22
23 func BenchmarkNopLoggerContextual(b *testing.B) {
24 benchmarkRunner(b, log.NewNopLogger(), withMessage)
25 }