From bd09619b604913189c70692f890cca399f046cb8 Mon Sep 17 00:00:00 2001 From: Ayman Bagabas Date: Mon, 6 Nov 2023 09:08:53 -0800 Subject: [PATCH 1/2] feat: set custom styles (#84) Move styles to a per logger instance bases. --- logger.go | 14 +++++ pkg.go | 6 ++ styles.go | 161 ++++++++++++++++++++++++--------------------------- text.go | 40 ++++++++----- text_test.go | 75 +++++++++++++----------- 5 files changed, 163 insertions(+), 133 deletions(-) diff --git a/logger.go b/logger.go index f044ecf..a6aacf1 100644 --- a/logger.go +++ b/logger.go @@ -46,6 +46,7 @@ type Logger struct { fields []interface{} helpers *sync.Map + styles *Styles } func (l *Logger) log(level Level, msg interface{}, keyvals ...interface{}) { @@ -290,15 +291,28 @@ func (l *Logger) SetColorProfile(profile termenv.Profile) { l.re.SetColorProfile(profile) } +// SetStyles sets the logger styles for the TextFormatter. +func (l *Logger) SetStyles(s *Styles) { + if s == nil { + s = DefaultStyles() + } + l.mu.Lock() + defer l.mu.Unlock() + l.styles = s +} + // With returns a new logger with the given keyvals added. func (l *Logger) With(keyvals ...interface{}) *Logger { + var st Styles l.mu.Lock() sl := *l + st = *l.styles l.mu.Unlock() sl.b = bytes.Buffer{} sl.mu = &sync.RWMutex{} sl.helpers = &sync.Map{} sl.fields = append(l.fields, keyvals...) + sl.styles = &st return &sl } diff --git a/pkg.go b/pkg.go index 440180c..1ba635d 100644 --- a/pkg.go +++ b/pkg.go @@ -55,6 +55,7 @@ func NewWithOptions(w io.Writer, o Options) *Logger { l.SetOutput(w) l.SetLevel(Level(l.level)) + l.SetStyles(DefaultStyles()) if l.callerFormatter == nil { l.callerFormatter = ShortCallerFormatter @@ -132,6 +133,11 @@ func SetColorProfile(profile termenv.Profile) { defaultLogger.SetColorProfile(profile) } +// SetStyles sets the logger styles for the TextFormatter. +func SetStyles(s *Styles) { + defaultLogger.SetStyles(s) +} + // GetPrefix returns the prefix for the default logger. func GetPrefix() string { return defaultLogger.GetPrefix() diff --git a/styles.go b/styles.go index b2bdf1f..14d261d 100644 --- a/styles.go +++ b/styles.go @@ -6,99 +6,92 @@ import ( "github.com/charmbracelet/lipgloss" ) -var ( - // TimestampStyle is the style for timestamps. - TimestampStyle = lipgloss.NewStyle() +// Styles defines the styles for the text logger. +type Styles struct { + // Timestamp is the style for timestamps. + Timestamp lipgloss.Style - // CallerStyle is the style for caller. - CallerStyle = lipgloss.NewStyle().Faint(true) + // Caller is the style for source caller. + Caller lipgloss.Style - // PrefixStyle is the style for prefix. - PrefixStyle = lipgloss.NewStyle().Bold(true).Faint(true) + // Prefix is the style for prefix. + Prefix lipgloss.Style - // MessageStyle is the style for messages. - MessageStyle = lipgloss.NewStyle() + // Message is the style for messages. + Message lipgloss.Style - // KeyStyle is the style for keys. - KeyStyle = lipgloss.NewStyle().Faint(true) + // Key is the style for keys. + Key lipgloss.Style - // ValueStyle is the style for values. - ValueStyle = lipgloss.NewStyle() + // Value is the style for values. + Value lipgloss.Style - // SeparatorStyle is the style for separators. - SeparatorStyle = lipgloss.NewStyle().Faint(true) + // Separator is the style for separators. + Separator lipgloss.Style - // DebugLevel is the style for debug level. - DebugLevelStyle = lipgloss.NewStyle(). - SetString(strings.ToUpper(DebugLevel.String())). - Bold(true). - MaxWidth(4). - Foreground(lipgloss.AdaptiveColor{ - Light: "63", - Dark: "63", - }) + // Levels are the styles for each level. + Levels map[Level]lipgloss.Style - // InfoLevel is the style for info level. - InfoLevelStyle = lipgloss.NewStyle(). - SetString(strings.ToUpper(InfoLevel.String())). - Bold(true). - MaxWidth(4). - Foreground(lipgloss.AdaptiveColor{ - Light: "39", - Dark: "86", - }) + // Keys overrides styles for specific keys. + Keys map[string]lipgloss.Style - // WarnLevel is the style for warn level. - WarnLevelStyle = lipgloss.NewStyle(). - SetString(strings.ToUpper(WarnLevel.String())). - Bold(true). - MaxWidth(4). - Foreground(lipgloss.AdaptiveColor{ - Light: "208", - Dark: "192", - }) - - // ErrorLevel is the style for error level. - ErrorLevelStyle = lipgloss.NewStyle(). - SetString(strings.ToUpper(ErrorLevel.String())). - Bold(true). - MaxWidth(4). - Foreground(lipgloss.AdaptiveColor{ - Light: "203", - Dark: "204", - }) - - // FatalLevel is the style for error level. - FatalLevelStyle = lipgloss.NewStyle(). - SetString(strings.ToUpper(FatalLevel.String())). - Bold(true). - MaxWidth(4). - Foreground(lipgloss.AdaptiveColor{ - Light: "133", - Dark: "134", - }) - - // KeyStyles overrides styles for specific keys. - KeyStyles = map[string]lipgloss.Style{} - - // ValueStyles overrides value styles for specific keys. - ValueStyles = map[string]lipgloss.Style{} -) + // Values overrides value styles for specific keys. + Values map[string]lipgloss.Style +} -// levelStyle is a helper function to get the style for a level. -func levelStyle(level Level) lipgloss.Style { - switch level { - case DebugLevel: - return DebugLevelStyle - case InfoLevel: - return InfoLevelStyle - case WarnLevel: - return WarnLevelStyle - case ErrorLevel: - return ErrorLevelStyle - case FatalLevel: - return FatalLevelStyle - default: - return lipgloss.NewStyle() +// DefaultStyles returns the default styles. +func DefaultStyles() *Styles { + return &Styles{ + Timestamp: lipgloss.NewStyle(), + Caller: lipgloss.NewStyle().Faint(true), + Prefix: lipgloss.NewStyle().Bold(true).Faint(true), + Message: lipgloss.NewStyle(), + Key: lipgloss.NewStyle().Faint(true), + Value: lipgloss.NewStyle(), + Separator: lipgloss.NewStyle().Faint(true), + Levels: map[Level]lipgloss.Style{ + DebugLevel: lipgloss.NewStyle(). + SetString(strings.ToUpper(DebugLevel.String())). + Bold(true). + MaxWidth(4). + Foreground(lipgloss.AdaptiveColor{ + Light: "63", + Dark: "63", + }), + InfoLevel: lipgloss.NewStyle(). + SetString(strings.ToUpper(InfoLevel.String())). + Bold(true). + MaxWidth(4). + Foreground(lipgloss.AdaptiveColor{ + Light: "39", + Dark: "86", + }), + WarnLevel: lipgloss.NewStyle(). + SetString(strings.ToUpper(WarnLevel.String())). + Bold(true). + MaxWidth(4). + Foreground(lipgloss.AdaptiveColor{ + Light: "208", + Dark: "192", + }), + ErrorLevel: lipgloss.NewStyle(). + SetString(strings.ToUpper(ErrorLevel.String())). + Bold(true). + MaxWidth(4). + Foreground(lipgloss.AdaptiveColor{ + Light: "203", + Dark: "204", + }), + FatalLevel: lipgloss.NewStyle(). + SetString(strings.ToUpper(FatalLevel.String())). + Bold(true). + MaxWidth(4). + Foreground(lipgloss.AdaptiveColor{ + Light: "133", + Dark: "134", + }), + }, + Keys: map[string]lipgloss.Style{}, + Values: map[string]lipgloss.Style{}, } } diff --git a/text.go b/text.go index c508f24..f7d62e8 100644 --- a/text.go +++ b/text.go @@ -16,6 +16,8 @@ const ( ) func (l *Logger) writeIndent(w io.Writer, str string, indent string, newline bool, key string) { + st := l.styles + // kindly borrowed from hclog for { nl := strings.IndexByte(str, '\n') @@ -23,10 +25,10 @@ func (l *Logger) writeIndent(w io.Writer, str string, indent string, newline boo if str != "" { _, _ = w.Write([]byte(indent)) val := escapeStringForOutput(str, false) - if valueStyle, ok := ValueStyles[key]; ok { + if valueStyle, ok := st.Values[key]; ok { val = valueStyle.Renderer(l.re).Render(val) } else { - val = ValueStyle.Renderer(l.re).Render(val) + val = st.Value.Renderer(l.re).Render(val) } _, _ = w.Write([]byte(val)) if newline { @@ -38,7 +40,7 @@ func (l *Logger) writeIndent(w io.Writer, str string, indent string, newline boo _, _ = w.Write([]byte(indent)) val := escapeStringForOutput(str[:nl], false) - val = ValueStyle.Renderer(l.re).Render(val) + val = st.Value.Renderer(l.re).Render(val) _, _ = w.Write([]byte(val)) _, _ = w.Write([]byte{'\n'}) str = str[nl+1:] @@ -148,6 +150,7 @@ func writeSpace(w io.Writer, first bool) { } func (l *Logger) textFormatter(keyvals ...interface{}) { + st := l.styles lenKeyvals := len(keyvals) for i := 0; i < lenKeyvals; i += 2 { @@ -158,41 +161,46 @@ func (l *Logger) textFormatter(keyvals ...interface{}) { case TimestampKey: if t, ok := keyvals[i+1].(time.Time); ok { ts := t.Format(l.timeFormat) - ts = TimestampStyle.Renderer(l.re).Render(ts) + ts = st.Timestamp.Renderer(l.re).Render(ts) writeSpace(&l.b, firstKey) l.b.WriteString(ts) } case LevelKey: if level, ok := keyvals[i+1].(Level); ok { - lvl := levelStyle(level).Renderer(l.re).String() - writeSpace(&l.b, firstKey) - l.b.WriteString(lvl) + var lvl string + if lvlStyle, ok := st.Levels[level]; ok { + lvl = lvlStyle.Renderer(l.re).String() + } + if lvl != "" { + writeSpace(&l.b, firstKey) + l.b.WriteString(lvl) + } } case CallerKey: if caller, ok := keyvals[i+1].(string); ok { caller = fmt.Sprintf("<%s>", caller) - caller = CallerStyle.Renderer(l.re).Render(caller) + caller = st.Caller.Renderer(l.re).Render(caller) writeSpace(&l.b, firstKey) l.b.WriteString(caller) } case PrefixKey: if prefix, ok := keyvals[i+1].(string); ok { - prefix = PrefixStyle.Renderer(l.re).Render(prefix + ":") + prefix = st.Prefix.Renderer(l.re).Render(prefix + ":") writeSpace(&l.b, firstKey) l.b.WriteString(prefix) } case MessageKey: if msg := keyvals[i+1]; msg != nil { m := fmt.Sprint(msg) - m = MessageStyle.Renderer(l.re).Render(m) + m = st.Message.Renderer(l.re).Render(m) writeSpace(&l.b, firstKey) l.b.WriteString(m) } default: sep := separator indentSep := indentSeparator - sep = SeparatorStyle.Renderer(l.re).Render(sep) - indentSep = SeparatorStyle.Renderer(l.re).Render(indentSep) + sep = st.Separator.Renderer(l.re).Render(sep) + indentSep = st.Separator.Renderer(l.re).Render(indentSep) key := fmt.Sprint(keyvals[i]) val := fmt.Sprintf("%+v", keyvals[i+1]) raw := val == "" @@ -203,14 +211,14 @@ func (l *Logger) textFormatter(keyvals ...interface{}) { continue } actualKey := key - valueStyle := ValueStyle - if vs, ok := ValueStyles[actualKey]; ok { + valueStyle := st.Value + if vs, ok := st.Values[actualKey]; ok { valueStyle = vs } - if keyStyle, ok := KeyStyles[key]; ok { + if keyStyle, ok := st.Keys[key]; ok { key = keyStyle.Renderer(l.re).Render(key) } else { - key = KeyStyle.Renderer(l.re).Render(key) + key = st.Key.Renderer(l.re).Render(key) } // Values may contain multiple lines, and that format diff --git a/text_test.go b/text_test.go index 75c4bfc..3f4337c 100644 --- a/text_test.go +++ b/text_test.go @@ -22,6 +22,13 @@ func _zeroTime() time.Time { return time.Time{} } +func TestNilStyles(t *testing.T) { + st := DefaultStyles() + l := New(io.Discard) + l.SetStyles(nil) + assert.Equal(t, st, l.styles) +} + func TestTextCaller(t *testing.T) { var buf bytes.Buffer logger := New(&buf) @@ -237,10 +244,12 @@ func TestTextFatal(t *testing.T) { func TestTextValueStyles(t *testing.T) { var buf bytes.Buffer logger := New(&buf) - oldValueStyle := ValueStyle - defer func() { ValueStyle = oldValueStyle }() - ValueStyle = lipgloss.NewStyle().Bold(true) - ValueStyles["key3"] = ValueStyle.Copy().Underline(true) + logger.SetColorProfile(termenv.ANSI256) + lipgloss.SetColorProfile(termenv.ANSI256) + st := DefaultStyles() + st.Value = lipgloss.NewStyle().Bold(true) + st.Values["key3"] = st.Value.Copy().Underline(true) + logger.SetStyles(st) cases := []struct { name string expected string @@ -250,7 +259,7 @@ func TestTextValueStyles(t *testing.T) { }{ { name: "simple message", - expected: fmt.Sprintf("%s info\n", InfoLevelStyle), + expected: fmt.Sprintf("%s info\n", st.Levels[InfoLevel]), msg: "info", kvs: nil, f: logger.Info, @@ -266,9 +275,9 @@ func TestTextValueStyles(t *testing.T) { name: "message with keyvals", expected: fmt.Sprintf( "%s info %s%s%s %s%s%s\n", - InfoLevelStyle, - KeyStyle.Render("key1"), SeparatorStyle.Render(separator), ValueStyle.Render("val1"), - KeyStyle.Render("key2"), SeparatorStyle.Render(separator), ValueStyle.Render("val2"), + st.Levels[InfoLevel], + st.Key.Render("key1"), st.Separator.Render(separator), st.Value.Render("val1"), + st.Key.Render("key2"), st.Separator.Render(separator), st.Value.Render("val2"), ), msg: "info", kvs: []interface{}{"key1", "val1", "key2", "val2"}, @@ -278,10 +287,10 @@ func TestTextValueStyles(t *testing.T) { name: "error message with multiline", expected: fmt.Sprintf( "%s info\n %s%s\n%s%s\n%s%s\n", - ErrorLevelStyle, - KeyStyle.Render("key1"), SeparatorStyle.Render(separator), - SeparatorStyle.Render(indentSeparator), ValueStyle.Render("val1"), - SeparatorStyle.Render(indentSeparator), ValueStyle.Render("val2"), + st.Levels[ErrorLevel], + st.Key.Render("key1"), st.Separator.Render(separator), + st.Separator.Render(indentSeparator), st.Value.Render("val1"), + st.Separator.Render(indentSeparator), st.Value.Render("val2"), ), msg: "info", kvs: []interface{}{"key1", "val1\nval2"}, @@ -291,9 +300,9 @@ func TestTextValueStyles(t *testing.T) { name: "error message with keyvals", expected: fmt.Sprintf( "%s info %s%s%s %s%s%s\n", - ErrorLevelStyle, - KeyStyle.Render("key1"), SeparatorStyle.Render(separator), ValueStyle.Render("val1"), - KeyStyle.Render("key2"), SeparatorStyle.Render(separator), ValueStyle.Render("val2"), + st.Levels[ErrorLevel], + st.Key.Render("key1"), st.Separator.Render(separator), st.Value.Render("val1"), + st.Key.Render("key2"), st.Separator.Render(separator), st.Value.Render("val2"), ), msg: "info", kvs: []interface{}{"key1", "val1", "key2", "val2"}, @@ -303,10 +312,10 @@ func TestTextValueStyles(t *testing.T) { name: "odd number of keyvals", expected: fmt.Sprintf( "%s info %s%s%s %s%s%s %s%s%s\n", - ErrorLevelStyle, - KeyStyle.Render("key1"), SeparatorStyle.Render(separator), ValueStyle.Render("val1"), - KeyStyle.Render("key2"), SeparatorStyle.Render(separator), ValueStyle.Render("val2"), - KeyStyle.Render("key3"), SeparatorStyle.Render(separator), ValueStyles["key3"].Render(`"missing value"`), + st.Levels[ErrorLevel], + st.Key.Render("key1"), st.Separator.Render(separator), st.Value.Render("val1"), + st.Key.Render("key2"), st.Separator.Render(separator), st.Value.Render("val2"), + st.Key.Render("key3"), st.Separator.Render(separator), st.Values["key3"].Render(`"missing value"`), ), msg: "info", kvs: []interface{}{"key1", "val1", "key2", "val2", "key3"}, @@ -316,8 +325,8 @@ func TestTextValueStyles(t *testing.T) { name: "error field", expected: fmt.Sprintf( "%s info %s%s%s\n", - ErrorLevelStyle, - KeyStyle.Render("key1"), SeparatorStyle.Render(separator), ValueStyle.Render(`"error value"`), + st.Levels[ErrorLevel], + st.Key.Render("key1"), st.Separator.Render(separator), st.Value.Render(`"error value"`), ), msg: "info", kvs: []interface{}{"key1", errors.New("error value")}, @@ -327,8 +336,8 @@ func TestTextValueStyles(t *testing.T) { name: "struct field", expected: fmt.Sprintf( "%s info %s%s%s\n", - InfoLevelStyle, - KeyStyle.Render("key1"), SeparatorStyle.Render(separator), ValueStyle.Render("{foo:bar}"), + st.Levels[InfoLevel], + st.Key.Render("key1"), st.Separator.Render(separator), st.Value.Render("{foo:bar}"), ), msg: "info", kvs: []interface{}{"key1", struct{ foo string }{foo: "bar"}}, @@ -338,8 +347,8 @@ func TestTextValueStyles(t *testing.T) { name: "struct field quoted", expected: fmt.Sprintf( "%s info %s%s%s\n", - InfoLevelStyle, - KeyStyle.Render("key1"), SeparatorStyle.Render(separator), ValueStyle.Render(`"{foo:bar baz}"`), + st.Levels[InfoLevel], + st.Key.Render("key1"), st.Separator.Render(separator), st.Value.Render(`"{foo:bar baz}"`), ), msg: "info", kvs: []interface{}{"key1", struct{ foo string }{foo: "bar baz"}}, @@ -349,8 +358,8 @@ func TestTextValueStyles(t *testing.T) { name: "slice of strings", expected: fmt.Sprintf( "%s info %s%s%s\n", - ErrorLevelStyle, - KeyStyle.Render("key1"), SeparatorStyle.Render(separator), ValueStyle.Render(`"[foo bar]"`), + st.Levels[ErrorLevel], + st.Key.Render("key1"), st.Separator.Render(separator), st.Value.Render(`"[foo bar]"`), ), msg: "info", kvs: []interface{}{"key1", []string{"foo", "bar"}}, @@ -360,8 +369,8 @@ func TestTextValueStyles(t *testing.T) { name: "slice of structs", expected: fmt.Sprintf( "%s info %s%s%s\n", - ErrorLevelStyle, - KeyStyle.Render("key1"), SeparatorStyle.Render(separator), ValueStyle.Render(`"[{foo:bar} {foo:baz}]"`), + st.Levels[ErrorLevel], + st.Key.Render("key1"), st.Separator.Render(separator), st.Value.Render(`"[{foo:bar} {foo:baz}]"`), ), msg: "info", kvs: []interface{}{"key1", []struct{ foo string }{{foo: "bar"}, {foo: "baz"}}}, @@ -371,8 +380,8 @@ func TestTextValueStyles(t *testing.T) { name: "slice of errors", expected: fmt.Sprintf( "%s info %s%s%s\n", - ErrorLevelStyle, - KeyStyle.Render("key1"), SeparatorStyle.Render(separator), ValueStyle.Render(`"[error value1 error value2]"`), + st.Levels[ErrorLevel], + st.Key.Render("key1"), st.Separator.Render(separator), st.Value.Render(`"[error value1 error value2]"`), ), msg: "info", kvs: []interface{}{"key1", []error{errors.New("error value1"), errors.New("error value2")}}, @@ -382,8 +391,8 @@ func TestTextValueStyles(t *testing.T) { name: "map of strings", expected: fmt.Sprintf( "%s info %s%s%s\n", - ErrorLevelStyle, - KeyStyle.Render("key1"), SeparatorStyle.Render(separator), ValueStyle.Render(`"map[baz:qux foo:bar]"`), + st.Levels[ErrorLevel], + st.Key.Render("key1"), st.Separator.Render(separator), st.Value.Render(`"map[baz:qux foo:bar]"`), ), msg: "info", kvs: []interface{}{"key1", map[string]string{"foo": "bar", "baz": "qux"}}, From d96bea283b892d8efe64843ea3b8b0e93d0213bf Mon Sep 17 00:00:00 2001 From: Drewry Pope Date: Tue, 7 Nov 2023 07:52:51 -0600 Subject: [PATCH 2/2] refactor!: return Error from ParseLevel (#83) * add parse level error * Apply suggestions from code review will fix rest of file in follow-up commit Co-authored-by: Ayman Bagabas * align code with suggestions * align * use default logger for default level * expected * Apply suggestions from code review Co-authored-by: Ayman Bagabas * Update level.go * Update level.go --------- Co-authored-by: Ayman Bagabas --- level.go | 23 +++++++++++++++-------- level_test.go | 46 +++++++++++++++++++++++++++++----------------- 2 files changed, 44 insertions(+), 25 deletions(-) diff --git a/level.go b/level.go index ae07909..d62949d 100644 --- a/level.go +++ b/level.go @@ -1,6 +1,10 @@ package log -import "strings" +import ( + "errors" + "fmt" + "strings" +) // Level is a logging level. type Level int32 @@ -38,20 +42,23 @@ func (l Level) String() string { } } +// ErrInvalidLevel is an error returned when parsing an invalid level string. +var ErrInvalidLevel = errors.New("invalid level") + // ParseLevel converts level in string to Level type. Default level is InfoLevel. -func ParseLevel(level string) Level { +func ParseLevel(level string) (Level, error) { switch strings.ToLower(level) { case DebugLevel.String(): - return DebugLevel + return DebugLevel, nil case InfoLevel.String(): - return InfoLevel + return InfoLevel, nil case WarnLevel.String(): - return WarnLevel + return WarnLevel, nil case ErrorLevel.String(): - return ErrorLevel + return ErrorLevel, nil case FatalLevel.String(): - return FatalLevel + return FatalLevel, nil default: - return InfoLevel + return 0, fmt.Errorf("%w: %q", ErrInvalidLevel, level) } } diff --git a/level_test.go b/level_test.go index 95d7bb5..5669e0e 100644 --- a/level_test.go +++ b/level_test.go @@ -1,6 +1,7 @@ package log import ( + "fmt" "testing" "github.com/stretchr/testify/assert" @@ -10,52 +11,63 @@ func TestDefaultLevel(t *testing.T) { var level Level assert.Equal(t, InfoLevel, level) } + func TestParseLevel(t *testing.T) { testCases := []struct { name string - level string - expLevel Level + input string + expected Level + error error }{ { name: "Parse debug", - level: "debug", - expLevel: DebugLevel, + input: "debug", + expected: DebugLevel, + error: nil, }, { name: "Parse info", - level: "Info", - expLevel: InfoLevel, + input: "Info", + expected: InfoLevel, + error: nil, }, { name: "Parse warn", - level: "WARN", - expLevel: WarnLevel, + input: "WARN", + expected: WarnLevel, + error: nil, }, { name: "Parse error", - level: "error", - expLevel: ErrorLevel, + input: "error", + expected: ErrorLevel, + error: nil, }, { name: "Parse fatal", - level: "FATAL", - expLevel: FatalLevel, + input: "FATAL", + expected: FatalLevel, + error: nil, }, { name: "Default", - level: "", - expLevel: InfoLevel, + input: "", + expected: InfoLevel, + error: fmt.Errorf("%w: %q", ErrInvalidLevel, ""), }, { name: "Wrong level, set INFO", - level: "WRONG_LEVEL", - expLevel: InfoLevel, + input: "WRONG_LEVEL", + expected: InfoLevel, + error: fmt.Errorf("%w: %q", ErrInvalidLevel, "WRONG_LEVEL"), }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - assert.Equal(t, tc.expLevel, ParseLevel(tc.level)) + lvl, err := ParseLevel(tc.input) + assert.Equal(t, tc.expected, lvl) + assert.Equal(t, tc.error, err) }) } }