From a42e1f3e7aca0b150c9ef320c886006db7b7a949 Mon Sep 17 00:00:00 2001 From: nhyatt Date: Thu, 13 Feb 2025 21:45:06 -0600 Subject: [PATCH] Adds custom slog functions and two additional slog levels, trace and fatal. --- internal/config/logging.go | 132 ++++++++++++++++++++++++++ internal/config/logging_test.go | 103 ++++++++++++++++++++ internal/config/struct-config.go | 75 +++++---------- internal/config/struct-config_test.go | 33 +------ 4 files changed, 258 insertions(+), 85 deletions(-) create mode 100644 internal/config/logging.go create mode 100644 internal/config/logging_test.go diff --git a/internal/config/logging.go b/internal/config/logging.go new file mode 100644 index 0000000..6779e14 --- /dev/null +++ b/internal/config/logging.go @@ -0,0 +1,132 @@ +package config + +import ( + "context" + "log/slog" + "reflect" + "strconv" +) + +const ( + LevelTrace = slog.Level(-8) + LevelFatal = slog.Level(12) +) + +type Log struct { + Ctx context.Context + Log *slog.Logger + SLogLevel *slog.LevelVar +} + +var LevelNames = map[slog.Leveler]string{ + LevelTrace: "TRACE", + LevelFatal: "FATAL", +} + +func setLogLevel(cfg *Config) { + switch { + // fatal + case cfg.LogLevel <= 20: + cfg.Log.SLogLevel.Set(LevelFatal) + cfg.Log.Info("Log level updated", slog.Any("level", LevelFatal)) + // error + case cfg.LogLevel > 20 && cfg.LogLevel <= 40: + cfg.Log.SLogLevel.Set(slog.LevelError) + cfg.Log.Info("Log level updated", slog.Any("level", slog.LevelError)) + // warning + case cfg.LogLevel > 40 && cfg.LogLevel <= 60: + cfg.Log.SLogLevel.Set(slog.LevelWarn) + cfg.Log.Info("Log level updated", slog.Any("level", slog.LevelWarn)) + // info + case cfg.LogLevel > 60 && cfg.LogLevel <= 80: + cfg.Log.SLogLevel.Set(slog.LevelInfo) + cfg.Log.Info("Log level updated", slog.Any("level", slog.LevelInfo)) + // debug + case cfg.LogLevel > 80 && cfg.LogLevel <= 100: + cfg.Log.SLogLevel.Set(slog.LevelDebug) + cfg.Log.Info("Log level updated", slog.Any("level", slog.LevelDebug)) + // trace + case cfg.LogLevel > 100: + cfg.Log.SLogLevel.Set(LevelTrace) + cfg.Log.Info("Log level updated", slog.Any("level", LevelTrace)) + } + + // set default logger + slog.SetDefault(cfg.Log.Log) +} + +func printRunningConfig(cfg *Config, cfgInfo []structInfo) { + var logRunningConfiguration string = "Running Configuration" + + for _, info := range cfgInfo { + if info.Secret { + cfg.Log.Debug(logRunningConfiguration, info.Name, "REDACTED") + } else { + switch info.Type.String() { + case "string": + p := reflect.ValueOf(cfg).Elem().FieldByName(info.Name).Addr().Interface().(*string) + cfg.Log.Debug(logRunningConfiguration, info.Alt, *p) + case "bool": + p := reflect.ValueOf(cfg).Elem().FieldByName(info.Name).Addr().Interface().(*bool) + cfg.Log.Debug(logRunningConfiguration, info.Alt, strconv.FormatBool(*p)) + case "int": + p := reflect.ValueOf(cfg).Elem().FieldByName(info.Name).Addr().Interface().(*int) + cfg.Log.Debug(logRunningConfiguration, info.Alt, strconv.FormatInt(int64(*p), 10)) + } + } + } +} + +func (log *Log) Fatal(msg string, attrs ...interface{}) { + log.Log.Log( + log.Ctx, + LevelFatal, + msg, + attrs..., + ) +} + +func (log Log) Error(msg string, attrs ...interface{}) { + log.Log.Log( + log.Ctx, + slog.LevelError, + msg, + attrs..., + ) +} + +func (log Log) Warn(msg string, attrs ...interface{}) { + log.Log.Log( + log.Ctx, + slog.LevelWarn, + msg, + attrs..., + ) +} + +func (log Log) Info(msg string, attrs ...interface{}) { + log.Log.Log( + log.Ctx, + slog.LevelInfo, + msg, + attrs..., + ) +} + +func (log Log) Debug(msg string, attrs ...interface{}) { + log.Log.Log( + log.Ctx, + slog.LevelDebug, + msg, + attrs..., + ) +} + +func (log Log) Trace(msg string, attrs ...interface{}) { + log.Log.Log( + log.Ctx, + LevelTrace, + msg, + attrs..., + ) +} diff --git a/internal/config/logging_test.go b/internal/config/logging_test.go new file mode 100644 index 0000000..40dcbf1 --- /dev/null +++ b/internal/config/logging_test.go @@ -0,0 +1,103 @@ +package config + +import ( + "log/slog" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestSetLogLevel(t *testing.T) { + c := New() + + for _, i := range []int{0, 21, 41, 61, 81, 101} { + c.LogLevel = i + + setLogLevel(&c) + switch i { + case 0: + assert.Equal(t, LevelFatal, c.Log.SLogLevel.Level()) + case 21: + assert.Equal(t, slog.LevelError, c.Log.SLogLevel.Level()) + case 41: + assert.Equal(t, slog.LevelWarn, c.Log.SLogLevel.Level()) + case 61: + assert.Equal(t, slog.LevelInfo, c.Log.SLogLevel.Level()) + case 81: + assert.Equal(t, slog.LevelDebug, c.Log.SLogLevel.Level()) + case 101: + assert.Equal(t, LevelTrace, c.Log.SLogLevel.Level()) + } + } +} + +func TestPrintRunningConfig(t *testing.T) { + buf, log := slogToBuffer() + c := New() + c.Log.Log = log + cfgInfo, err := getStructInfo(&c) + assert.NoError(t, err) + printRunningConfig(&c, cfgInfo) + + assert.Contains(t, buf.String(), "Running Configuration") +} + +func TestFatal(t *testing.T) { + c := New() + buf, log := slogToBuffer() + c.Log.Log = log + + c.Log.Fatal("TEST Message") + assert.Contains(t, buf.String(), "TEST Message") + assert.Contains(t, buf.String(), "level=ERROR+4") +} + +func TestError(t *testing.T) { + c := New() + buf, log := slogToBuffer() + c.Log.Log = log + + c.Log.Error("TEST Message") + assert.Contains(t, buf.String(), "TEST Message") + assert.Contains(t, buf.String(), "level=ERROR") +} + +func TestWarn(t *testing.T) { + c := New() + buf, log := slogToBuffer() + c.Log.Log = log + + c.Log.Warn("TEST Message") + assert.Contains(t, buf.String(), "TEST Message") + assert.Contains(t, buf.String(), "level=WARN") +} + +func TestInfo(t *testing.T) { + c := New() + buf, log := slogToBuffer() + c.Log.Log = log + + c.Log.Info("TEST Message") + assert.Contains(t, buf.String(), "TEST Message") + assert.Contains(t, buf.String(), "level=INFO") +} + +func TestDebug(t *testing.T) { + c := New() + buf, log := slogToBuffer() + c.Log.Log = log + + c.Log.Debug("TEST Message") + assert.Contains(t, buf.String(), "TEST Message") + assert.Contains(t, buf.String(), "level=DEBUG") +} + +func TestTrace(t *testing.T) { + c := New() + buf, log := slogToBuffer() + c.Log.Log = log + + c.Log.Trace("TEST Message") + assert.Contains(t, buf.String(), "TEST Message") + assert.Contains(t, buf.String(), "level=DEBUG-4") +} diff --git a/internal/config/struct-config.go b/internal/config/struct-config.go index bd1800d..1fcf4c4 100644 --- a/internal/config/struct-config.go +++ b/internal/config/struct-config.go @@ -1,10 +1,9 @@ package config import ( + "context" "log/slog" "os" - "reflect" - "strconv" "time" ) @@ -16,9 +15,8 @@ type Config struct { TZUTC *time.Location `ignored:"true"` // logging - LogLevel int `default:"50" env:"log_level"` - Log *slog.Logger `ignored:"true"` - SLogLevel *slog.LevelVar `ignored:"true"` + LogLevel int `default:"50" env:"log_level"` + Log *Log `ignored:"true"` // webserver WebServerPort int `default:"8080" env:"webserver_port"` @@ -31,57 +29,28 @@ type Config struct { // New initializes the config variable for use with a prepared set of defaults. func New() Config { cfg := Config{ - SLogLevel: new(slog.LevelVar), + Log: &Log{ + SLogLevel: new(slog.LevelVar), + }, } - cfg.Log = slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{ - Level: cfg.SLogLevel, + // Initialize Logger + cfg.Log.Log = slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{ + Level: cfg.Log.SLogLevel, + ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr { + if a.Key == slog.LevelKey { + level := a.Value.Any().(slog.Level) + levelLabel, exists := LevelNames[level] + if !exists { + levelLabel = level.String() + } + + a.Value = slog.StringValue(levelLabel) + } + return a + }, })) + cfg.Log.Ctx = context.Background() return cfg } - -func setLogLevel(cfg *Config) { - switch { - // error - case cfg.LogLevel <= 20: - cfg.SLogLevel.Set(slog.LevelError) - cfg.Log.Info("Log level updated", "level", slog.LevelError) - // warning - case cfg.LogLevel > 20 && cfg.LogLevel <= 40: - cfg.SLogLevel.Set(slog.LevelWarn) - cfg.Log.Info("Log level updated", "level", slog.LevelWarn) - // info - case cfg.LogLevel > 40 && cfg.LogLevel <= 60: - cfg.SLogLevel.Set(slog.LevelInfo) - cfg.Log.Info("Log level updated", "level", slog.LevelInfo) - // debug - case cfg.LogLevel > 60: - cfg.SLogLevel.Set(slog.LevelDebug) - cfg.Log.Info("Log level updated", "level", slog.LevelDebug) - } - // set default logger - slog.SetDefault(cfg.Log) -} - -func printRunningConfig(cfg *Config, cfgInfo []structInfo) { - var logRunningConfiguration = "Running Configuration" - - for _, info := range cfgInfo { - if info.Secret { - cfg.Log.Debug(logRunningConfiguration, info.Name, "REDACTED") - } else { - switch info.Type.String() { - case "string": - p := reflect.ValueOf(cfg).Elem().FieldByName(info.Name).Addr().Interface().(*string) - cfg.Log.Debug("Running Configuration", info.Alt, *p) - case "bool": - p := reflect.ValueOf(cfg).Elem().FieldByName(info.Name).Addr().Interface().(*bool) - cfg.Log.Debug("Running Configuration", info.Alt, strconv.FormatBool(*p)) - case "int": - p := reflect.ValueOf(cfg).Elem().FieldByName(info.Name).Addr().Interface().(*int) - cfg.Log.Debug("Running Configuration", info.Alt, strconv.FormatInt(int64(*p), 10)) - } - } - } -} diff --git a/internal/config/struct-config_test.go b/internal/config/struct-config_test.go index b566cc0..9db640c 100644 --- a/internal/config/struct-config_test.go +++ b/internal/config/struct-config_test.go @@ -15,7 +15,7 @@ func slogToBuffer() (*bytes.Buffer, *slog.Logger) { slog.NewTextHandler( buf, &slog.HandlerOptions{ - Level: slog.LevelDebug, + Level: LevelTrace, }, ), ) @@ -25,34 +25,3 @@ func TestNew(t *testing.T) { c := New() assert.Equal(t, "config.Config", reflect.TypeOf(c).String()) } - -func TestSetLogLevel(t *testing.T) { - c := New() - - for _, i := range []int{0, 21, 41, 61} { - c.LogLevel = i - - setLogLevel(&c) - switch i { - case 0: - assert.Equal(t, slog.LevelError, c.SLogLevel.Level()) - case 21: - assert.Equal(t, slog.LevelWarn, c.SLogLevel.Level()) - case 41: - assert.Equal(t, slog.LevelInfo, c.SLogLevel.Level()) - case 61: - assert.Equal(t, slog.LevelDebug, c.SLogLevel.Level()) - } - } -} - -func TestPrintRunningConfig(t *testing.T) { - buf, log := slogToBuffer() - c := New() - c.Log = log - cfgInfo, err := getStructInfo(&c) - assert.NoError(t, err) - printRunningConfig(&c, cfgInfo) - - assert.Contains(t, buf.String(), "Running Configuration") -}