diff --git a/internal/logger/REAME.md b/internal/logger/REAME.md new file mode 100644 index 0000000..00de7bc --- /dev/null +++ b/internal/logger/REAME.md @@ -0,0 +1,11 @@ +# Logger + +This package includes a custom logger for the project. + +Functions are exposed for logging messages easily; it is imperative that +these functions are used throughout the project to maintain a standard in +log messages. + +If necessary, the `logWriter` can be edited so that in addition to writing to +stdout, it can also write to a file or send the logs to a server like +Better Stack or New Relic. diff --git a/internal/logger/kv.go b/internal/logger/kv.go new file mode 100644 index 0000000..f18d14a --- /dev/null +++ b/internal/logger/kv.go @@ -0,0 +1,39 @@ +package logger + +import "github.com/eduardolat/pgbackweb/internal/util/maputil" + +// KV is a record of key-value pair to be logged +type KV map[string]any + +// kvToArgs converts a slice of KV to a slice of any +func kvToArgs(kv ...KV) []any { + pickedKv := KV{} + if len(kv) > 0 { + pickedKv = kv[0] + } + + sortedKeys := maputil.GetSortedStringKeys(pickedKv) + args := make([]any, 0, len(sortedKeys)*2) + + for _, k := range sortedKeys { + args = append(args, k, pickedKv[k]) + } + return args +} + +// kvToArgsNs converts a slice of KV to a slice of any +// and adds a namespace to the resulting slice +func kvToArgsNs(ns string, kv ...KV) []any { + pickedKv := KV{} + if len(kv) > 0 { + pickedKv = kv[0] + } + + sortedKeys := maputil.GetSortedStringKeys(pickedKv) + args := make([]any, 0, len(sortedKeys)*2+2) + args = append(args, "ns", ns) + for _, k := range sortedKeys { + args = append(args, k, pickedKv[k]) + } + return args +} diff --git a/internal/logger/kv_test.go b/internal/logger/kv_test.go new file mode 100644 index 0000000..836345a --- /dev/null +++ b/internal/logger/kv_test.go @@ -0,0 +1,69 @@ +package logger + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestKvToArgsNoArgs(t *testing.T) { + result := kvToArgs() + assert.Equal(t, []any{}, result) +} + +func TestKvToArgsOneArg(t *testing.T) { + kv := KV{"key": "value"} + result := kvToArgs(kv) + assert.Equal(t, []any{"key", "value"}, result) +} + +func TestKvToArgsMultipleArgs(t *testing.T) { + kv1 := KV{"key1": "value1", "key2": "value2"} + kv2 := KV{"key3": "value3"} + result := kvToArgs(kv1, kv2) + assert.Equal(t, []any{"key1", "value1", "key2", "value2"}, result) +} + +func TestKvToArgsNsNoArgs(t *testing.T) { + result := kvToArgsNs("namespace") + assert.Equal(t, []any{"ns", "namespace"}, result) +} + +func TestKvToArgsNsOneArg(t *testing.T) { + kv := KV{"key": "value"} + result := kvToArgsNs("namespace", kv) + assert.Equal(t, []any{"ns", "namespace", "key", "value"}, result) +} + +func TestKvToArgsNsMultipleArgs(t *testing.T) { + kv1 := KV{"key1": "value1", "key2": "value2"} + kv2 := KV{"key3": "value3"} + result := kvToArgsNs("namespace", kv1, kv2) + assert.Equal(t, []any{"ns", "namespace", "key1", "value1", "key2", "value2"}, result) +} + +func TestKvToArgsPickOnlyFirst(t *testing.T) { + kv1 := KV{"key1": "value1"} + kv2 := KV{"key2": "value2"} + result := kvToArgs(kv1, kv2) + assert.Equal(t, []any{"key1", "value1"}, result) +} + +func TestKvToArgsNsPickOnlyFirst(t *testing.T) { + kv1 := KV{"key1": "value1"} + kv2 := KV{"key2": "value2"} + result := kvToArgsNs("namespace", kv1, kv2) + assert.Equal(t, []any{"ns", "namespace", "key1", "value1"}, result) +} + +func TestKvToArgsOrder(t *testing.T) { + kv := KV{"z": "value1", "a": "value2"} + result := kvToArgs(kv) + assert.Equal(t, []any{"a", "value2", "z", "value1"}, result) +} + +func TestKvToArgsNsOrder(t *testing.T) { + kv := KV{"z": "value1", "a": "value2"} + result := kvToArgsNs("namespace", kv) + assert.Equal(t, []any{"ns", "namespace", "a", "value2", "z", "value1"}, result) +} diff --git a/internal/logger/logger.go b/internal/logger/logger.go new file mode 100644 index 0000000..86f5e82 --- /dev/null +++ b/internal/logger/logger.go @@ -0,0 +1,79 @@ +package logger + +import ( + "log/slog" + "os" + "sync" +) + +var logger *slog.Logger +var loggerOnce sync.Once + +// getLogger returns a logger singleton configured to log to +// custom writer in JSON format +func getLogger() *slog.Logger { + loggerOnce.Do(func() { + w := &logWriter{} + logger = slog.New(slog.NewJSONHandler( + w, nil, + )) + }) + + return logger +} + +// Debug logs a debug message +func Debug(msg string, args ...KV) { + getLogger().Debug(msg, kvToArgs(args...)...) +} + +// DebugNs logs a debug message with a namespace +// It's for grouping logs consistently +func DebugNs(ns string, msg string, args ...KV) { + getLogger().Debug(msg, kvToArgsNs(ns, args...)...) +} + +// Info logs a info message +func Info(msg string, args ...KV) { + getLogger().Info(msg, kvToArgs(args...)...) +} + +// InfoNs logs a info message with a namespace +// It's for grouping logs consistently +func InfoNs(ns string, msg string, args ...KV) { + getLogger().Info(msg, kvToArgsNs(ns, args...)...) +} + +// Warn logs a warn message +func Warn(msg string, args ...KV) { + getLogger().Warn(msg, kvToArgs(args...)...) +} + +// WarnNs logs a warn message with a namespace +// It's for grouping logs consistently +func WarnNs(ns string, msg string, args ...KV) { + getLogger().Warn(msg, kvToArgsNs(ns, args...)...) +} + +// Error logs a error message +func Error(msg string, args ...KV) { + getLogger().Error(msg, kvToArgs(args...)...) +} + +// ErrorNs logs a error message with a namespace +// It's for grouping logs consistently +func ErrorNs(ns string, msg string, args ...KV) { + getLogger().Error(msg, kvToArgsNs(ns, args...)...) +} + +// FatalError is equivalent to Error() followed by a call to os.Exit(1) +func FatalError(msg string, args ...KV) { + Error(msg, args...) + os.Exit(1) +} + +// FatalErrorNs is equivalent to FatalError() with a namespace +func FatalErrorNs(ns string, msg string, args ...KV) { + ErrorNs(ns, msg, args...) + os.Exit(1) +} diff --git a/internal/logger/writer.go b/internal/logger/writer.go new file mode 100644 index 0000000..944079a --- /dev/null +++ b/internal/logger/writer.go @@ -0,0 +1,12 @@ +package logger + +import "os" + +// logWriter is a simple io.Writer that can redirect logs to os.Stdout +// or any other required place +type logWriter struct{} + +// Write writes the log message to os.Stdout or any other required place +func (w *logWriter) Write(p []byte) (n int, err error) { + return os.Stdout.Write(p) +}