Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow to configure allowed levels by string value #22

Merged
merged 7 commits into from Apr 27, 2022
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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"
}
52 changes: 51 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
mcosta74 marked this conversation as resolved.
Show resolved Hide resolved
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:
ChrisHines marked this conversation as resolved.
Show resolved Hide resolved
return AllowNone()
}
}

// AllowAll is an alias for AllowDebug.
func AllowAll() Option {
return AllowDebug()
Expand Down Expand Up @@ -100,6 +124,32 @@ 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) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the only returned error is invalid level, then maybe the second argument could simply be a boolran?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isn't more idiomatic to return an error? maybe in the future we might return different types of errors and we'll not need to introduce breaking changes in the API

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, error is a bit more future proof and IMO not meaningfully different for callers.

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:
ChrisHines marked this conversation as resolved.
Show resolved Hide resolved
return nil, ErrInvalidLevelString
}
}

// ParseDefault calls Parse and returns the default Value on error.
func ParseDefault(level string, def Value) Value {
ChrisHines marked this conversation as resolved.
Show resolved Hide resolved
if v, err := Parse(level); err == nil {
ChrisHines marked this conversation as resolved.
Show resolved Hide resolved
return v
}
return def
}

// 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)
}
})
}
}