Add logger package with key-value logging functionality

This commit is contained in:
Luis Eduardo Jeréz Girón
2024-07-19 23:39:58 -06:00
parent 3425bf67bd
commit 4297c2881f
5 changed files with 210 additions and 0 deletions
+11
View File
@@ -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.
+39
View File
@@ -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
}
+69
View File
@@ -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)
}
+79
View File
@@ -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)
}
+12
View File
@@ -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)
}