From dd6b899df35825aa8f9d93cfe07c0ba5b8b5b786 Mon Sep 17 00:00:00 2001 From: nhyatt Date: Sun, 23 Feb 2025 09:56:16 -0600 Subject: [PATCH] Initial commit --- constants.go | 9 +++ globals.go | 14 ++++ go.mod | 11 +++ go.sum | 10 +++ logging.go | 186 ++++++++++++++++++++++++++++++++++++++++++++++++ logging_test.go | 97 +++++++++++++++++++++++++ structs.go | 26 +++++++ 7 files changed, 353 insertions(+) create mode 100644 constants.go create mode 100644 globals.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 logging.go create mode 100644 logging_test.go create mode 100644 structs.go diff --git a/constants.go b/constants.go new file mode 100644 index 0000000..20eac3d --- /dev/null +++ b/constants.go @@ -0,0 +1,9 @@ +package log + +import "log/slog" + +// custom logging levels not included with slog +const ( + LevelTrace = slog.Level(-8) // trace + LevelFatal = slog.Level(12) // fatal +) diff --git a/globals.go b/globals.go new file mode 100644 index 0000000..bb2396a --- /dev/null +++ b/globals.go @@ -0,0 +1,14 @@ +package log + +import "log/slog" + +var ( + // LevelNames set the names associated with custom logging levels. + LevelNames = map[slog.Leveler]string{ + LevelTrace: "TRACE", + LevelFatal: "FATAL", + } + + // L is the global interface used for calling the logger subfunctions. + L = Log{} +) diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..80ff3f3 --- /dev/null +++ b/go.mod @@ -0,0 +1,11 @@ +module gitea.smoothnet.org/nhyatt/log + +go 1.23.5 + +require github.com/stretchr/testify v1.10.0 + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..713a0b4 --- /dev/null +++ b/go.sum @@ -0,0 +1,10 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/logging.go b/logging.go new file mode 100644 index 0000000..f0ab491 --- /dev/null +++ b/logging.go @@ -0,0 +1,186 @@ +package log + +import ( + "context" + "errors" + "log/slog" + "os" + "regexp" +) + +func New(c Customization) { + // check configuration values and set defaults for missing items + setDefaults(&c) + + slogOptions := &slog.HandlerOptions{ + Level: &L.SLogLevel, + ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr { + if a.Key == slog.TimeKey { + // set time key + a.Value = slog.StringValue(c.TimeStamp.Key) + // set time format + a.Value = slog.StringValue(a.Value.Time().Format(c.TimeStamp.Format)) + } + 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 + }, + } + + // Initialize SLog and translate new logging levels + switch c.Format { + case "json": + L.Log = slog.New(slog.NewJSONHandler(os.Stdout, slogOptions)) + default: + L.Log = slog.New(slog.NewTextHandler(os.Stdout, slogOptions)) + } + + // create context + L.Ctx = context.Background() +} + +// SetNumericLevel will set the log level based on a number from 1-100. +// The larger the number the more verbose the logs. +// +// 1-20 = Fatal, 21-40 = Error, 41-60 = Warn, 61-80 = Info, 81-99 = Debug, +// and 100 = Trace. +func SetNumericLevel(level int) { + var llu string = "Log Level Updated" + + switch { + // fatal + case level <= 20: + L.SLogLevel.Set(LevelFatal) + Info(llu, "level", LevelFatal) + // error + case level > 20 && level <= 40: + L.SLogLevel.Set(slog.LevelError) + Info(llu, "level", slog.LevelError) + // warning + case level > 40 && level <= 60: + L.SLogLevel.Set(slog.LevelWarn) + Info(llu, "level", slog.LevelWarn) + // info + case level > 60 && level <= 80: + L.SLogLevel.Set(slog.LevelInfo) + Info(llu, "level", slog.LevelInfo) + // debug + case level > 80 && level <= 99: + L.SLogLevel.Set(slog.LevelDebug) + Info(llu, "level", slog.LevelDebug) + // trace + case level > 99: + L.SLogLevel.Set(LevelTrace) + Info(llu, "level", LevelTrace) + } + + // set default logger + slog.SetDefault(L.Log) +} + +func setDefaults(c *Customization) error { + // \/ \/ \/ REQUIRED FIELDS \/ \/ \/ // + + // format, can be one of "json" or "text" + switch { + case len(c.Format) == 0: + c.Format = "json" + case c.Format == "json" || c.Format == "text": + break + default: + return errors.New("Invalid format requested.") + } + + // set default timestamp identifier + if len(c.TimeStamp.Key) == 0 { + c.TimeStamp.Key = "ts" + } + + // set default timestamp format + if len(c.TimeStamp.Format) == 0 { + c.TimeStamp.Format = "2006-01-02T15:04:05.000-0700" + } + + // set default for type of event + switch { + case len(c.Type) == 0: + c.Type = "Business" + case c.Type == "Business" || c.Type == "Performance" || c.Type == "Security": + break + default: + return errors.New("Invalid type of event, must be one of \"Business\", \"Performance\", or \"Security\".") + } + + // set default for application + if !regexp.MustCompile(`^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$`).MatchString(string(c.Application)) { + return errors.New("Invalid application id, must be a valid uuid string.") + } + + + + // /\ /\ /\ REQUIRED FIELDS /\ /\ /\ // + + return nil +} + +func Fatal(msg string, attrs ...interface{}) { + L.Log.Log( + L.Ctx, + LevelFatal, + msg, + attrs..., + ) +} + +func Error(msg string, attrs ...interface{}) { + L.Log.Log( + L.Ctx, + slog.LevelError, + msg, + attrs..., + ) +} + +func Warn(msg string, attrs ...interface{}) { + L.Log.Log( + L.Ctx, + slog.LevelWarn, + msg, + attrs..., + ) +} + +func Info(msg string, attrs ...interface{}) { + L.Log.Log( + L.Ctx, + slog.LevelInfo, + msg, + attrs..., + ) +} + +func Debug(msg string, attrs ...interface{}) { + L.Log.Log( + L.Ctx, + slog.LevelDebug, + msg, + attrs..., + ) +} + +func Trace(msg string, attrs ...interface{}) { + L.Log.Log( + L.Ctx, + LevelTrace, + msg, + attrs..., + ) +} diff --git a/logging_test.go b/logging_test.go new file mode 100644 index 0000000..ca6c10f --- /dev/null +++ b/logging_test.go @@ -0,0 +1,97 @@ +package log +import ( + "bytes" + "log/slog" + "testing" + + "github.com/stretchr/testify/assert" +) + +func slogToBuffer() (*bytes.Buffer, *slog.Logger) { + buf := new(bytes.Buffer) + return buf, slog.New( + slog.NewTextHandler( + buf, + &slog.HandlerOptions{ + Level: LevelTrace, + }, + ), + ) +} + +func TestSetLogLevel(t *testing.T) { + Init("text") + + for _, i := range []int{0, 21, 41, 61, 81, 101} { + SetNumericLevel(i) + + switch i { + case 0: + assert.Equal(t, LevelFatal, L.SLogLevel.Level()) + case 21: + assert.Equal(t, slog.LevelError, L.SLogLevel.Level()) + case 41: + assert.Equal(t, slog.LevelWarn, L.SLogLevel.Level()) + case 61: + assert.Equal(t, slog.LevelInfo, L.SLogLevel.Level()) + case 81: + assert.Equal(t, slog.LevelDebug, L.SLogLevel.Level()) + case 101: + assert.Equal(t, LevelTrace, L.SLogLevel.Level()) + } + } +} + +func TestFatal(t *testing.T) { + buf, log := slogToBuffer() + L.Log = log + + Fatal("TEST Message") + assert.Contains(t, buf.String(), "TEST Message") + assert.Contains(t, buf.String(), "level=ERROR+4") +} + +func TestError(t *testing.T) { + buf, log := slogToBuffer() + L.Log = log + + Error("TEST Message") + assert.Contains(t, buf.String(), "TEST Message") + assert.Contains(t, buf.String(), "level=ERROR") +} + +func TestWarn(t *testing.T) { + buf, log := slogToBuffer() + L.Log = log + + Warn("TEST Message") + assert.Contains(t, buf.String(), "TEST Message") + assert.Contains(t, buf.String(), "level=WARN") +} + +func TestInfo(t *testing.T) { + buf, log := slogToBuffer() + L.Log = log + + Info("TEST Message") + assert.Contains(t, buf.String(), "TEST Message") + assert.Contains(t, buf.String(), "level=INFO") +} + +func TestDebug(t *testing.T) { + buf, log := slogToBuffer() + L.Log = log + + Debug("TEST Message") + assert.Contains(t, buf.String(), "TEST Message") + assert.Contains(t, buf.String(), "level=DEBUG") +} + +func TestTrace(t *testing.T) { + buf, log := slogToBuffer() + L.Log = log + + Trace("TEST Message") + assert.Contains(t, buf.String(), "TEST Message") + assert.Contains(t, buf.String(), "level=DEBUG-4") +} diff --git a/structs.go b/structs.go new file mode 100644 index 0000000..97759b7 --- /dev/null +++ b/structs.go @@ -0,0 +1,26 @@ +package log + +import ( + "context" + "log/slog" +) + +// primary struct +type Log struct { + Ctx context.Context // context + Log *slog.Logger // slog logger + SLogLevel slog.LevelVar // level +} + +// field customization +type Customization struct { + Application CustomizationValue + Format string + TimeStamp struct { + Key string + Format string + } + Type CustomizationValue +} + +type CustomizationValue string