From 4bb09063fcfcb8a58984ad6c478012f8d5f17d0f Mon Sep 17 00:00:00 2001
From: Jack Robertson <jack.robertson@nexusmods.com>
Date: Tue, 11 Jul 2023 17:03:33 +0100
Subject: [PATCH] refactor: move slog package to root, add configuration
 options and tests

---
 options.go       | 25 ++++++++++++++++++++++++
 slog.go          | 51 ++++++++++++++++++++++++++++++++++++++++++++++++
 slog_test.go     | 34 ++++++++++++++++++++++++++++++++
 src/main/main.go | 10 ----------
 src/slog/slog.go | 41 --------------------------------------
 5 files changed, 110 insertions(+), 51 deletions(-)
 create mode 100644 options.go
 create mode 100644 slog.go
 create mode 100644 slog_test.go
 delete mode 100644 src/main/main.go
 delete mode 100644 src/slog/slog.go

diff --git a/options.go b/options.go
new file mode 100644
index 0000000..fd76bee
--- /dev/null
+++ b/options.go
@@ -0,0 +1,25 @@
+package slog
+
+import (
+	"golang.org/x/exp/slog"
+	"io"
+)
+
+type Options struct {
+	Leveler slog.Leveler
+	Writer  io.Writer
+}
+
+type Option func(*Options)
+
+func WithLeveler(leveler slog.Leveler) Option {
+	return func(o *Options) {
+		o.Leveler = leveler
+	}
+}
+
+func WithWriter(writer io.Writer) Option {
+	return func(o *Options) {
+		o.Writer = writer
+	}
+}
diff --git a/slog.go b/slog.go
new file mode 100644
index 0000000..603449a
--- /dev/null
+++ b/slog.go
@@ -0,0 +1,51 @@
+package slog
+
+import (
+	"golang.org/x/exp/slog"
+	"os"
+	"strings"
+)
+
+func New(opts ...Option) *slog.Logger {
+	options := &Options{
+		Leveler: slog.LevelDebug,
+		Writer:  os.Stdout,
+	}
+	for _, opt := range opts {
+		opt(options)
+	}
+
+	// Attributes modified for datadog schema via HandlerOptions.ReplaceAttr
+	// Per recommendation at https://pkg.go.dev/golang.org/x/exp/slog#JSONHandler
+	// See datadog: https://docs.datadoghq.com/logs/log_configuration/attributes_naming_convention/#reserved-attributes
+	replaceAttr := func(groups []string, attr slog.Attr) slog.Attr {
+		if attr.Key == "msg" {
+			attr.Key = "message"
+		}
+		if attr.Key == "time" {
+			attr.Key = "date"
+		}
+		if attr.Key == "level" {
+			attr.Key = "status"
+			attr.Value = slog.StringValue(strings.ToLower(attr.Value.String()))
+		}
+		return attr
+	}
+
+	handlerOptions := &slog.HandlerOptions{
+		Level:       options.Leveler,
+		AddSource:   true,
+		ReplaceAttr: replaceAttr,
+	}
+	logHandler := slog.NewJSONHandler(options.Writer, handlerOptions)
+	logger := slog.New(logHandler)
+
+	hostname, err := os.Hostname()
+	if err != nil {
+		logger.Error("Failed to get hostname.", slog.String("error", err.Error()))
+	} else {
+		logger = logger.With("host", hostname)
+	}
+
+	return logger
+}
diff --git a/slog_test.go b/slog_test.go
new file mode 100644
index 0000000..f1007b4
--- /dev/null
+++ b/slog_test.go
@@ -0,0 +1,34 @@
+package slog
+
+import (
+	"bufio"
+	"bytes"
+	"encoding/json"
+	"golang.org/x/exp/slog"
+	"testing"
+)
+
+func TestNew(t *testing.T) {
+	var buf bytes.Buffer
+
+	logger := New(WithWriter(&buf))
+	logger.Info("Foo", slog.String("bar", "baz"))
+	logger.Error("Bar", slog.String("baz", "qux"))
+
+	scanner := bufio.NewScanner(&buf)
+
+	// Scan the first log.
+	scanner.Scan()
+	infoLog := scanner.Bytes()
+	var infoLogMap map[string]any
+	if err := json.Unmarshal(infoLog, &infoLogMap); err != nil {
+		t.Errorf("Failed to unmarshal info log: %s", err.Error())
+	}
+	t.Log(infoLogMap)
+	if infoLogMap["status"] != "info" {
+		t.Errorf("Expected info log level, got %s", infoLogMap["level"])
+	}
+	if infoLogMap["bar"] != "baz" {
+		t.Errorf("Expected bar=baz, got %s", infoLogMap["bar"])
+	}
+}
diff --git a/src/main/main.go b/src/main/main.go
deleted file mode 100644
index c2e8c45..0000000
--- a/src/main/main.go
+++ /dev/null
@@ -1,10 +0,0 @@
-package main
-
-import (
-	"gitlab.nexdev.uk/pub/go-slog/src/slog"
-)
-
-func main() {
-	logger := slog.InitLogger()
-	logger.Info("Logger test.")
-}
diff --git a/src/slog/slog.go b/src/slog/slog.go
deleted file mode 100644
index 8c58241..0000000
--- a/src/slog/slog.go
+++ /dev/null
@@ -1,41 +0,0 @@
-package slog
-
-import (
-	"golang.org/x/exp/slog"
-	"os"
-	"strings"
-)
-
-func InitLogger() *slog.Logger {
-	// Could set logger level via ENV vars in future.
-	programLevel := new(slog.LevelVar)
-	programLevel.Set(slog.LevelDebug)
-
-	// Attributes modified for datadog schema via HandlerOptions.ReplaceAttr
-	// Per recommendation at https://pkg.go.dev/golang.org/x/exp/slog#JSONHandler
-	// See datadog: https://docs.datadoghq.com/logs/log_configuration/attributes_naming_convention/#reserved-attributes
-	loggerReplace := func(groups []string, a slog.Attr) slog.Attr {
-		if a.Key == "msg" {
-			a.Key = "message"
-		}
-		if a.Key == "time" {
-			a.Key = "date"
-		}
-		if a.Key == "level" {
-			a.Key = "status"
-			a.Value = slog.StringValue(strings.ToLower(a.Value.String()))
-		}
-		return a
-	}
-	logger := slog.New(slog.NewJSONHandler(os.Stdout,
-		&slog.HandlerOptions{Level: programLevel, AddSource: true, ReplaceAttr: loggerReplace}))
-	hostname, hostnameErr := os.Hostname()
-	if hostnameErr == nil {
-		logger = logger.With("host", hostname)
-	} else {
-		logger.Error("Failed to get hostname.", slog.String("error", hostnameErr.Error()))
-	}
-
-	logger.Info("Initialised.")
-	return logger
-}
-- 
GitLab