diff --git a/logutil/slogutil/defer_example_test.go b/logutil/slogutil/defer_example_test.go index 8801de6..e1a5723 100644 --- a/logutil/slogutil/defer_example_test.go +++ b/logutil/slogutil/defer_example_test.go @@ -13,7 +13,7 @@ import ( func ExampleCloseAndLog() { ctx := context.Background() l := slogutil.New(&slogutil.Config{ - Verbose: true, + Level: slog.LevelDebug, }) func() { diff --git a/logutil/slogutil/legacy.go b/logutil/slogutil/legacy.go index 9e56152..796ea89 100644 --- a/logutil/slogutil/legacy.go +++ b/logutil/slogutil/legacy.go @@ -157,6 +157,8 @@ type logFunction func(format string, args ...any) // returned. func logFuncForLevel(lvl slog.Level) (logFunc logFunction, warnStr string, err error) { switch lvl { + case LevelTrace: + return aglog.Debug, "trace: ", nil case slog.LevelDebug: return aglog.Debug, "", nil case slog.LevelInfo: diff --git a/logutil/slogutil/slogutil.go b/logutil/slogutil/slogutil.go index 576aa96..4b52008 100644 --- a/logutil/slogutil/slogutil.go +++ b/logutil/slogutil/slogutil.go @@ -29,11 +29,11 @@ type Config struct { // If set, it must be valid. Format Format + // Level is the minimum record level that will be logged. + Level slog.Level + // AddTimestamp, if true, adds a timestamp to every record. AddTimestamp bool - - // Verbose, if true, enables verbose logging. - Verbose bool } // New creates a slog logger with the given parameters. If c is nil, the @@ -49,11 +49,7 @@ func New(c *Config) (l *slog.Logger) { } } - lvl := slog.LevelInfo - if c.Verbose { - lvl = slog.LevelDebug - } - + lvl := c.Level format := cmp.Or(c.Format, FormatDefault) output := cmp.Or[io.Writer](c.Output, os.Stdout) if format == FormatDefault { @@ -61,10 +57,7 @@ func New(c *Config) (l *slog.Logger) { return newDefault(output, lvl, c.AddTimestamp) } - var replaceAttr func(groups []string, a slog.Attr) (res slog.Attr) - if !c.AddTimestamp { - replaceAttr = RemoveTime - } + replaceAttr := newReplaceAttr(!c.AddTimestamp) var h slog.Handler switch format { @@ -95,6 +88,8 @@ func New(c *Config) (l *slog.Logger) { } // newDefault returns a new default slog logger set up with the given options. +// +// TODO(d.kolyshev): Replace log level name for [LevelTrace]. func newDefault(output io.Writer, lvl slog.Level, addTimestamp bool) (l *slog.Logger) { h := NewLevelHandler(lvl, slog.Default().Handler()) log.SetOutput(output) @@ -107,6 +102,38 @@ func newDefault(output io.Writer, lvl slog.Level, addTimestamp bool) (l *slog.Lo return slog.New(h) } +// newReplaceAttr is a function that returns [slog.HandlerOptions.ReplaceAttr] +// function for provided parameters. +func newReplaceAttr(removeTime bool) func(groups []string, a slog.Attr) (res slog.Attr) { + if !removeTime { + return ReplaceLevel + } + + return func(groups []string, a slog.Attr) (res slog.Attr) { + return ReplaceLevel(groups, RemoveTime(groups, a)) + } +} + +// traceAttrValue is a [LevelTrace] value under the [slog.LevelKey] key. +var traceAttrValue = slog.StringValue("TRACE") + +// ReplaceLevel is a function for [slog.HandlerOptions.ReplaceAttr] that adds +// [LevelTrace] custom name for level attribute. +func ReplaceLevel(groups []string, a slog.Attr) (res slog.Attr) { + if len(groups) > 0 { + return a + } + + if a.Key == slog.LevelKey { + lvl := a.Value.Any().(slog.Level) + if lvl == LevelTrace { + a.Value = traceAttrValue + } + } + + return a +} + // RemoveTime is a function for [slog.HandlerOptions.ReplaceAttr] that removes // the "time" attribute. func RemoveTime(groups []string, a slog.Attr) (res slog.Attr) { diff --git a/logutil/slogutil/slogutil_example_test.go b/logutil/slogutil/slogutil_example_test.go index a46f754..4f710f9 100644 --- a/logutil/slogutil/slogutil_example_test.go +++ b/logutil/slogutil/slogutil_example_test.go @@ -9,7 +9,7 @@ import ( func ExampleNew_default() { l := slogutil.New(&slogutil.Config{ - Verbose: true, + Level: slog.LevelDebug, }) l.Info("test info") @@ -22,8 +22,8 @@ func ExampleNew_default() { func ExampleNew_json() { l := slogutil.New(&slogutil.Config{ - Format: slogutil.FormatJSON, - Verbose: true, + Format: slogutil.FormatJSON, + Level: slog.LevelDebug, }) l.Info("test info") @@ -41,8 +41,8 @@ func ExampleNew_json() { func ExampleNew_text() { l := slogutil.New(&slogutil.Config{ - Format: slogutil.FormatText, - Verbose: true, + Format: slogutil.FormatText, + Level: slog.LevelDebug, }) l.Info("test info") @@ -72,3 +72,19 @@ This is a very long text with many lines.` // INFO my text line_num=2 line="" // INFO my text line_num=3 line="This is a very long text with many lines." } + +func ExampleNew_trace() { + l := slogutil.New(&slogutil.Config{ + Format: slogutil.FormatText, + Level: slogutil.LevelTrace, + }) + + l.Log(context.Background(), slogutil.LevelTrace, "test trace") + l.Info("test info") + l.Debug("test debug") + + // Output: + // level=TRACE msg="test trace" + // level=INFO msg="test info" + // level=DEBUG msg="test debug" +} diff --git a/logutil/slogutil/verbosity.go b/logutil/slogutil/verbosity.go new file mode 100644 index 0000000..05ba4e2 --- /dev/null +++ b/logutil/slogutil/verbosity.go @@ -0,0 +1,31 @@ +package slogutil + +import ( + "fmt" + "log/slog" + + "github.com/AdguardTeam/golibs/errors" +) + +// Acceptable [slog.Level] levels. +const ( + LevelTrace = slog.Level(-8) + LevelDebug = slog.LevelDebug + LevelInfo = slog.LevelInfo + LevelWarn = slog.LevelWarn + LevelError = slog.LevelError +) + +// VerbosityToLevel returns log level for given verbosity. +func VerbosityToLevel(l uint8) (lvl slog.Level, err error) { + switch l { + case 0: + return LevelInfo, nil + case 1: + return LevelDebug, nil + case 2: + return LevelTrace, nil + default: + return lvl, fmt.Errorf("%w: %d", errors.ErrBadEnumValue, l) + } +}