diff --git a/level/doc.go b/level/doc.go index 505d307..fd681dc 100644 --- a/level/doc.go +++ b/level/doc.go @@ -7,6 +7,17 @@ // logger = level.NewFilter(logger, level.AllowInfo()) // <-- // logger = log.With(logger, "ts", log.DefaultTimestampUTC) // +// It's also possible to configure log level from a string. For instance from +// a flag, environment variable or configuration file. +// +// fs := flag.NewFlagSet("myprogram") +// lvl := fs.String("log", "info", "debug, info, warn, error") +// +// var logger log.Logger +// logger = log.NewLogfmtLogger(os.Stderr) +// logger = level.NewFilter(logger, level.Allow(level.ParseDefault(*lvl, level.InfoValue()))) // <-- +// logger = log.With(logger, "ts", log.DefaultTimestampUTC) +// // Then, at the callsites, use one of the level.Debug, Info, Warn, or Error // helper methods to emit leveled log events. // diff --git a/level/example_test.go b/level/example_test.go index 5f59f21..d3d3e16 100644 --- a/level/example_test.go +++ b/level/example_test.go @@ -2,6 +2,7 @@ package level_test import ( "errors" + "flag" "os" "github.com/go-kit/log" @@ -34,6 +35,26 @@ func Example_filtered() { level.Debug(logger).Log("next item", 17) // filtered // Output: - // level=error caller=example_test.go:32 err="bad data" - // level=info caller=example_test.go:33 event="data saved" + // level=error caller=example_test.go:33 err="bad data" + // level=info caller=example_test.go:34 event="data saved" +} + +func Example_parsed() { + fs := flag.NewFlagSet("example", flag.ExitOnError) + lvl := fs.String("log-level", "", `"debug", "info", "warn" or "error"`) + fs.Parse([]string{"-log-level", "info"}) + + // Set up logger with level filter. + logger := log.NewLogfmtLogger(os.Stdout) + logger = level.NewFilter(logger, level.Allow(level.ParseDefault(*lvl, level.DebugValue()))) + logger = log.With(logger, "caller", log.DefaultCaller) + + // Use level helpers to log at different levels. + level.Error(logger).Log("err", errors.New("bad data")) + level.Info(logger).Log("event", "data saved") + level.Debug(logger).Log("next item", 17) // filtered + + // Output: + // level=error caller=example_test.go:53 err="bad data" + // level=info caller=example_test.go:54 event="data saved" } diff --git a/level/level.go b/level/level.go index c94756c..c641d98 100644 --- a/level/level.go +++ b/level/level.go @@ -1,6 +1,14 @@ package level -import "github.com/go-kit/log" +import ( + "errors" + "strings" + + "github.com/go-kit/log" +) + +// ErrInvalidLevelString is returned whenever an invalid string is passed to Parse. +var ErrInvalidLevelString = errors.New("invalid level string") // Error returns a logger that includes a Key/ErrorValue pair. func Error(logger log.Logger) log.Logger { @@ -66,6 +74,22 @@ func (l *logger) Log(keyvals ...interface{}) error { // Option sets a parameter for the leveled logger. type Option func(*logger) +// Allow the provided log level to pass. +func Allow(v Value) Option { + switch v { + case debugValue: + return AllowDebug() + case infoValue: + return AllowInfo() + case warnValue: + return AllowWarn() + case errorValue: + return AllowError() + default: + return AllowNone() + } +} + // AllowAll is an alias for AllowDebug. func AllowAll() Option { return AllowDebug() @@ -100,6 +124,33 @@ func allowed(allowed level) Option { return func(l *logger) { l.allowed = allowed } } +// Parse a string to its corresponding level value. Valid strings are "debug", +// "info", "warn", and "error". Strings are normalized via strings.TrimSpace and +// strings.ToLower. +func Parse(level string) (Value, error) { + switch strings.TrimSpace(strings.ToLower(level)) { + case debugValue.name: + return debugValue, nil + case infoValue.name: + return infoValue, nil + case warnValue.name: + return warnValue, nil + case errorValue.name: + return errorValue, nil + default: + return nil, ErrInvalidLevelString + } +} + +// ParseDefault calls Parse and returns the default Value on error. +func ParseDefault(level string, def Value) Value { + v, err := Parse(level) + if err != nil { + return def + } + return v +} + // ErrNotAllowed sets the error to return from Log when it squelches a log // event disallowed by the configured Allow[Level] option. By default, // ErrNotAllowed is nil; in this case the log event is squelched with no diff --git a/level/level_test.go b/level/level_test.go index 035fa5d..b42bc19 100644 --- a/level/level_test.go +++ b/level/level_test.go @@ -17,6 +17,45 @@ func TestVariousLevels(t *testing.T) { allowed level.Option want string }{ + { + "Allow(DebugValue)", + level.Allow(level.DebugValue()), + 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"), + }, + { + "Allow(InfoValue)", + level.Allow(level.InfoValue()), + strings.Join([]string{ + `{"level":"info","this is":"info log"}`, + `{"level":"warn","this is":"warn log"}`, + `{"level":"error","this is":"error log"}`, + }, "\n"), + }, + { + "Allow(WarnValue)", + level.Allow(level.WarnValue()), + strings.Join([]string{ + `{"level":"warn","this is":"warn log"}`, + `{"level":"error","this is":"error log"}`, + }, "\n"), + }, + { + "Allow(ErrorValue)", + level.Allow(level.ErrorValue()), + strings.Join([]string{ + `{"level":"error","this is":"error log"}`, + }, "\n"), + }, + { + "Allow(nil)", + level.Allow(nil), + strings.Join([]string{}, "\n"), + }, { "AllowAll", level.AllowAll(), @@ -147,7 +186,7 @@ func TestLevelContext(t *testing.T) { logger = log.With(logger, "caller", log.DefaultCaller) level.Info(logger).Log("foo", "bar") - if want, have := `level=info caller=level_test.go:149 foo=bar`, strings.TrimSpace(buf.String()); want != have { + if want, have := `level=info caller=level_test.go:188 foo=bar`, strings.TrimSpace(buf.String()); want != have { t.Errorf("\nwant '%s'\nhave '%s'", want, have) } } @@ -163,7 +202,7 @@ func TestContextLevel(t *testing.T) { logger = level.NewFilter(logger, level.AllowAll()) level.Info(logger).Log("foo", "bar") - if want, have := `caller=level_test.go:165 level=info foo=bar`, strings.TrimSpace(buf.String()); want != have { + if want, have := `caller=level_test.go:204 level=info foo=bar`, strings.TrimSpace(buf.String()); want != have { t.Errorf("\nwant '%s'\nhave '%s'", want, have) } } @@ -233,3 +272,122 @@ func TestInjector(t *testing.T) { t.Errorf("wrong level value: got %#v, want %#v", got, want) } } + +func TestParse(t *testing.T) { + testCases := []struct { + name string + level string + want level.Value + wantErr error + }{ + { + name: "Debug", + level: "debug", + want: level.DebugValue(), + wantErr: nil, + }, + { + name: "Info", + level: "info", + want: level.InfoValue(), + wantErr: nil, + }, + { + name: "Warn", + level: "warn", + want: level.WarnValue(), + wantErr: nil, + }, + { + name: "Error", + level: "error", + want: level.ErrorValue(), + wantErr: nil, + }, + { + name: "Case Insensitive", + level: "ErRoR", + want: level.ErrorValue(), + wantErr: nil, + }, + { + name: "Trimmed", + level: " Error ", + want: level.ErrorValue(), + wantErr: nil, + }, + { + name: "Invalid", + level: "invalid", + want: nil, + wantErr: level.ErrInvalidLevelString, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got, err := level.Parse(tc.level) + if err != tc.wantErr { + t.Errorf("got unexpected error %#v", err) + } + + if got != tc.want { + t.Errorf("wrong value: got=%#v, want=%#v", got, tc.want) + } + }) + } +} + +func TestParseDefault(t *testing.T) { + testCases := []struct { + name string + level string + want level.Value + }{ + { + name: "Debug", + level: "debug", + want: level.DebugValue(), + }, + { + name: "Info", + level: "info", + want: level.InfoValue(), + }, + { + name: "Warn", + level: "warn", + want: level.WarnValue(), + }, + { + name: "Error", + level: "error", + want: level.ErrorValue(), + }, + { + name: "Case Insensitive", + level: "ErRoR", + want: level.ErrorValue(), + }, + { + name: "Trimmed", + level: " Error ", + want: level.ErrorValue(), + }, + { + name: "Invalid", + level: "invalid", + want: level.InfoValue(), + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got := level.ParseDefault(tc.level, level.InfoValue()) + + if got != tc.want { + t.Errorf("wrong value: got=%#v, want=%#v", got, tc.want) + } + }) + } +}