diff --git a/log/alt_experimental_level/benchmark_test.go b/log/alt_experimental_level/benchmark_test.go new file mode 100644 index 000000000..314cd50bb --- /dev/null +++ b/log/alt_experimental_level/benchmark_test.go @@ -0,0 +1,59 @@ +package level_test + +import ( + "io/ioutil" + "testing" + + "github.com/go-kit/kit/log" + "github.com/go-kit/kit/log/alt_experimental_level" +) + +func BenchmarkNopBaseline(b *testing.B) { + benchmarkRunner(b, log.NewNopLogger()) +} + +func BenchmarkNopDisallowedLevel(b *testing.B) { + level.AllowInfoAndAbove() + benchmarkRunner(b, log.NewNopLogger()) +} + +func BenchmarkNopAllowedLevel(b *testing.B) { + level.AllowAll() + benchmarkRunner(b, log.NewNopLogger()) +} + +func BenchmarkJSONBaseline(b *testing.B) { + benchmarkRunner(b, log.NewJSONLogger(ioutil.Discard)) +} + +func BenchmarkJSONDisallowedLevel(b *testing.B) { + level.AllowInfoAndAbove() + benchmarkRunner(b, log.NewJSONLogger(ioutil.Discard)) +} + +func BenchmarkJSONAllowedLevel(b *testing.B) { + level.AllowAll() + benchmarkRunner(b, log.NewJSONLogger(ioutil.Discard)) +} + +func BenchmarkLogfmtBaseline(b *testing.B) { + benchmarkRunner(b, log.NewLogfmtLogger(ioutil.Discard)) +} + +func BenchmarkLogfmtDisallowedLevel(b *testing.B) { + level.AllowInfoAndAbove() + benchmarkRunner(b, log.NewLogfmtLogger(ioutil.Discard)) +} + +func BenchmarkLogfmtAllowedLevel(b *testing.B) { + level.AllowAll() + benchmarkRunner(b, log.NewLogfmtLogger(ioutil.Discard)) +} + +func benchmarkRunner(b *testing.B, logger log.Logger) { + b.ResetTimer() + b.ReportAllocs() + for i := 0; i < b.N; i++ { + level.Debug(logger).Log("foo", "bar") + } +} diff --git a/log/alt_experimental_level/global.go b/log/alt_experimental_level/global.go new file mode 100644 index 000000000..f37a790fc --- /dev/null +++ b/log/alt_experimental_level/global.go @@ -0,0 +1,73 @@ +package level + +import ( + "github.com/go-kit/kit/log" +) + +var ( + global = AllowingNone() + /* + Alternately: + global atomic.Value + */ +) + +/* Alternately: +func init() { + global.Store(errorOnly{}) +} +*/ + +func resetGlobal(proposed Leveler) { + global = proposed + /* Alternately: + global.Store(proposed) + */ +} + +func AllowAll() { + AllowDebugAndAbove() +} + +func AllowDebugAndAbove() { + resetGlobal(AllowingDebugAndAbove()) +} + +func AllowInfoAndAbove() { + resetGlobal(AllowingInfoAndAbove()) +} + +func AllowWarnAndAbove() { + resetGlobal(AllowingWarnAndAbove()) +} + +func AllowErrorOnly() { + resetGlobal(AllowingErrorOnly()) +} + +func AllowNone() { + resetGlobal(AllowingNone()) +} + +func getGlobal() Leveler { + return global + /* Alternately: + return global.Load().(Leveler) + */ +} + +func Debug(logger log.Logger) log.Logger { + return getGlobal().Debug(logger) +} + +func Info(logger log.Logger) log.Logger { + return getGlobal().Info(logger) +} + +func Warn(logger log.Logger) log.Logger { + return getGlobal().Warn(logger) +} + +func Error(logger log.Logger) log.Logger { + return getGlobal().Error(logger) +} diff --git a/log/alt_experimental_level/level.go b/log/alt_experimental_level/level.go new file mode 100644 index 000000000..0c5f62934 --- /dev/null +++ b/log/alt_experimental_level/level.go @@ -0,0 +1,96 @@ +package level + +import ( + "github.com/go-kit/kit/log" +) + +var ( + // Alternately, we could use a similarly inert logger that does nothing but + // return a given error value. + nop = log.NewNopLogger() +) + +type Leveler interface { + Debug(logger log.Logger) log.Logger + Info(logger log.Logger) log.Logger + Warn(logger log.Logger) log.Logger + Error(logger log.Logger) log.Logger +} + +func withLevel(level string, logger log.Logger) log.Logger { + return log.NewContext(logger).With("level", level) +} + +type debugAndAbove struct{} + +func (debugAndAbove) Debug(logger log.Logger) log.Logger { + return withLevel("debug", logger) +} + +func (debugAndAbove) Info(logger log.Logger) log.Logger { + return withLevel("info", logger) +} + +func (debugAndAbove) Warn(logger log.Logger) log.Logger { + return withLevel("warn", logger) +} + +func (debugAndAbove) Error(logger log.Logger) log.Logger { + return withLevel("error", logger) +} + +type infoAndAbove struct { + debugAndAbove +} + +func (infoAndAbove) Debug(logger log.Logger) log.Logger { + return nop +} + +type warnAndAbove struct { + infoAndAbove +} + +func (warnAndAbove) Info(logger log.Logger) log.Logger { + return nop +} + +type errorOnly struct { + warnAndAbove +} + +func (errorOnly) Warn(logger log.Logger) log.Logger { + return nop +} + +type none struct { + errorOnly +} + +func (none) Error(logger log.Logger) log.Logger { + return nop +} + +func AllowingAll() Leveler { + return AllowingDebugAndAbove() +} + +func AllowingDebugAndAbove() Leveler { + return debugAndAbove{} +} + +func AllowingInfoAndAbove() Leveler { + return infoAndAbove{} +} + +func AllowingWarnAndAbove() Leveler { + return warnAndAbove{} +} + +func AllowingErrorOnly() Leveler { + return errorOnly{} +} + +func AllowingNone() Leveler { + return none{} +} diff --git a/log/alt_experimental_level/level_test.go b/log/alt_experimental_level/level_test.go new file mode 100644 index 000000000..ded26b323 --- /dev/null +++ b/log/alt_experimental_level/level_test.go @@ -0,0 +1,186 @@ +package level_test + +import ( + "bytes" + "strings" + "testing" + + "github.com/go-kit/kit/log" + "github.com/go-kit/kit/log/alt_experimental_level" +) + +func TestGlobalLevels(t *testing.T) { + for _, testcase := range []struct { + allowed string + allow func() + want string + }{ + { + "all", + level.AllowAll, + strings.Join([]string{ + `{"level":"debug","this is":"debug log"}`, + `{"level":"info","this is":"info log"}`, + `{"level":"warn","this is":"warn log"}`, + `{"level":"error","this is":"error log"}`, + }, "\n"), + }, + { + "debug+", + level.AllowDebugAndAbove, + strings.Join([]string{ + `{"level":"debug","this is":"debug log"}`, + `{"level":"info","this is":"info log"}`, + `{"level":"warn","this is":"warn log"}`, + `{"level":"error","this is":"error log"}`, + }, "\n"), + }, + { + "info+", + level.AllowInfoAndAbove, + strings.Join([]string{ + `{"level":"info","this is":"info log"}`, + `{"level":"warn","this is":"warn log"}`, + `{"level":"error","this is":"error log"}`, + }, "\n"), + }, + { + "warn+", + level.AllowWarnAndAbove, + strings.Join([]string{ + `{"level":"warn","this is":"warn log"}`, + `{"level":"error","this is":"error log"}`, + }, "\n"), + }, + { + "error", + level.AllowErrorOnly, + strings.Join([]string{ + `{"level":"error","this is":"error log"}`, + }, "\n"), + }, + { + "none", + level.AllowNone, + ``, + }, + } { + var buf bytes.Buffer + logger := log.NewJSONLogger(&buf) + + testcase.allow() + + level.Debug(logger).Log("this is", "debug log") + level.Info(logger).Log("this is", "info log") + level.Warn(logger).Log("this is", "warn log") + level.Error(logger).Log("this is", "error log") + + if want, have := testcase.want, strings.TrimSpace(buf.String()); want != have { + t.Errorf("given Allowed=%s: want\n%s\nhave\n%s", testcase.allowed, want, have) + } + } +} + +func TestInstanceLevels(t *testing.T) { + for _, testcase := range []struct { + allowed string + leveler level.Leveler + want string + }{ + { + "all", + level.AllowingAll(), + strings.Join([]string{ + `{"level":"debug","this is":"debug log"}`, + `{"level":"info","this is":"info log"}`, + `{"level":"warn","this is":"warn log"}`, + `{"level":"error","this is":"error log"}`, + }, "\n"), + }, + { + "debug+", + level.AllowingDebugAndAbove(), + strings.Join([]string{ + `{"level":"debug","this is":"debug log"}`, + `{"level":"info","this is":"info log"}`, + `{"level":"warn","this is":"warn log"}`, + `{"level":"error","this is":"error log"}`, + }, "\n"), + }, + { + "info+", + level.AllowingInfoAndAbove(), + strings.Join([]string{ + `{"level":"info","this is":"info log"}`, + `{"level":"warn","this is":"warn log"}`, + `{"level":"error","this is":"error log"}`, + }, "\n"), + }, + { + "warn+", + level.AllowingWarnAndAbove(), + strings.Join([]string{ + `{"level":"warn","this is":"warn log"}`, + `{"level":"error","this is":"error log"}`, + }, "\n"), + }, + { + "error", + level.AllowingErrorOnly(), + strings.Join([]string{ + `{"level":"error","this is":"error log"}`, + }, "\n"), + }, + { + "none", + level.AllowingNone(), + ``, + }, + } { + var buf bytes.Buffer + logger := log.NewJSONLogger(&buf) + + l := testcase.leveler + + l.Debug(logger).Log("this is", "debug log") + l.Info(logger).Log("this is", "info log") + l.Warn(logger).Log("this is", "warn log") + l.Error(logger).Log("this is", "error log") + + if want, have := testcase.want, strings.TrimSpace(buf.String()); want != have { + t.Errorf("given Allowed=%s: want\n%s\nhave\n%s", testcase.allowed, want, have) + } + } +} +func TestLevelContext(t *testing.T) { + var buf bytes.Buffer + + // Wrapping the level logger with a context allows users to use + // log.DefaultCaller as per normal. + var logger log.Logger + logger = log.NewLogfmtLogger(&buf) + level.AllowAll() + logger = level.Info(logger) + logger = log.NewContext(logger).With("caller", log.DefaultCaller) + + logger.Log("foo", "bar") + if want, have := `level=info caller=level_test.go:166 foo=bar`, strings.TrimSpace(buf.String()); want != have { + t.Errorf("want %q, have %q", want, have) + } +} + +func TestContextLevel(t *testing.T) { + var buf bytes.Buffer + + // Wrapping a context with the level logger allows users to use + // log.DefaultCaller as per normal. + var logger log.Logger + logger = log.NewLogfmtLogger(&buf) + logger = log.NewContext(logger).With("caller", log.DefaultCaller) + + level.AllowAll() + level.Info(logger).Log("foo", "bar") + if want, have := `caller=level_test.go:182 level=info foo=bar`, strings.TrimSpace(buf.String()); want != have { + t.Errorf("want %q, have %q", want, have) + } +}