Skip to content

Commit

Permalink
Allow to configure allowed levels by string value (#22)
Browse files Browse the repository at this point in the history
* feat: allow to configure level from string

* fix staticcheck violation

* fix UTs

* fix: apply suggested changes

* fix: apply some of the suggested changes

* fix: more idiomatic code

* Update level/level.go

Co-authored-by: Márk Sági-Kazár <sagikazarmark@users.noreply.github.com>

Co-authored-by: Márk Sági-Kazár <sagikazarmark@users.noreply.github.com>
  • Loading branch information
mcosta74 and sagikazarmark committed Apr 27, 2022
1 parent 3752ef7 commit 0b69c70
Show file tree
Hide file tree
Showing 4 changed files with 246 additions and 5 deletions.
11 changes: 11 additions & 0 deletions level/doc.go
Expand Up @@ -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.
//
Expand Down
25 changes: 23 additions & 2 deletions level/example_test.go
Expand Up @@ -2,6 +2,7 @@ package level_test

import (
"errors"
"flag"
"os"

"github.com/go-kit/log"
Expand Down Expand Up @@ -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"
}
53 changes: 52 additions & 1 deletion 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 {
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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
Expand Down
162 changes: 160 additions & 2 deletions level/level_test.go
Expand Up @@ -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(),
Expand Down Expand Up @@ -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)
}
}
Expand All @@ -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)
}
}
Expand Down Expand Up @@ -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)
}
})
}
}

0 comments on commit 0b69c70

Please sign in to comment.