Log messages are lower-case event names with their details as key/value
attributes ("error" for errors), in place of formatted strings.
Loggable package take a WithLogger option as part of their "New*" call,
defaulting to slog.Default(). Each tags its lines with its `component`.
Some debug loggers print directly rather than using slog when in
terminal logging mode.
209 lines
5.1 KiB
Go
209 lines
5.1 KiB
Go
package logging
|
|
|
|
import (
|
|
"context"
|
|
"io"
|
|
"log/slog"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/lmittmann/tint"
|
|
)
|
|
|
|
// saveGlobals restores the package level and the default slog logger after a
|
|
// test that changes them.
|
|
func saveGlobals(t *testing.T) {
|
|
t.Helper()
|
|
level := logLevel.Level()
|
|
def := slog.Default()
|
|
out, detect := terminal, isTerminal
|
|
t.Cleanup(func() {
|
|
logLevel.Set(level)
|
|
slog.SetDefault(def)
|
|
terminal, isTerminal = out, detect
|
|
Shutdown()
|
|
})
|
|
}
|
|
|
|
func TestLevels(t *testing.T) {
|
|
saveGlobals(t)
|
|
slog.SetDefault(slog.New(slog.NewTextHandler(io.Discard, nil)))
|
|
|
|
SetLevel(slog.LevelInfo)
|
|
if IsDebugEnabled() {
|
|
t.Fatal("debug enabled at info level")
|
|
}
|
|
EnableDebug()
|
|
if !IsDebugEnabled() {
|
|
t.Fatal("EnableDebug did not enable debug")
|
|
}
|
|
|
|
// EnableDebug must not raise a level that is already more verbose.
|
|
SetLevel(slog.LevelDebug - 4)
|
|
EnableDebug()
|
|
if got := logLevel.Level(); got != slog.LevelDebug-4 {
|
|
t.Fatalf("level = %v, want %v", got, slog.LevelDebug-4)
|
|
}
|
|
|
|
SetLevel(slog.LevelError)
|
|
if IsDebugEnabled() {
|
|
t.Fatal("debug enabled at error level")
|
|
}
|
|
}
|
|
|
|
// wrapper is a handler that wraps another, as middleware handlers do.
|
|
type wrapper struct{ slog.Handler }
|
|
|
|
func TestIsJSONHandler(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
h slog.Handler
|
|
want bool
|
|
}{
|
|
{"json", slog.NewJSONHandler(io.Discard, nil), true},
|
|
{"text", slog.NewTextHandler(io.Discard, nil), false},
|
|
{"tint", tint.NewTextHandler(io.Discard, nil), false},
|
|
{"nil", nil, false},
|
|
{"json with attrs", slog.NewJSONHandler(io.Discard, nil).WithAttrs(
|
|
[]slog.Attr{slog.String("k", "v")}), true},
|
|
// Only slog's own JSON handler is recognised, not one wrapped in another.
|
|
{"wrapped json", wrapper{slog.NewJSONHandler(io.Discard, nil)}, false},
|
|
}
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
if got := IsJSONHandler(tc.h); got != tc.want {
|
|
t.Fatalf("IsJSONHandler = %v, want %v", got, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestLogToFileWritesJSON(t *testing.T) {
|
|
saveGlobals(t)
|
|
SetLevel(slog.LevelInfo)
|
|
path := filepath.Join(t.TempDir(), "app.log")
|
|
|
|
if err := LogToFile(path); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if st, err := os.Stat(path); err != nil {
|
|
t.Fatal(err)
|
|
} else if perm := st.Mode().Perm(); perm != 0o600 {
|
|
t.Errorf("log file permissions = %o, want 600", perm)
|
|
}
|
|
if !IsJSONHandler(slog.Default().Handler()) {
|
|
t.Fatal("default handler is not JSON after LogToFile")
|
|
}
|
|
slog.Info("hello file", "k", "v")
|
|
slog.Debug("hidden")
|
|
|
|
//nolint:gosec // G304: reading a file the test created in t.TempDir
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
s := string(data)
|
|
if !strings.Contains(s, `"msg":"hello file"`) || !strings.Contains(s, `"k":"v"`) {
|
|
t.Fatalf("log file missing entry: %s", s)
|
|
}
|
|
if strings.Contains(s, "hidden") {
|
|
t.Fatalf("debug line written at info level: %s", s)
|
|
}
|
|
}
|
|
|
|
func TestLogToFileBadPath(t *testing.T) {
|
|
saveGlobals(t)
|
|
if err := LogToFile(filepath.Join(t.TempDir(), "missing", "app.log")); err == nil {
|
|
t.Fatal("expected an error for a path in a missing directory")
|
|
}
|
|
}
|
|
|
|
func TestLogToFileTracksFile(t *testing.T) {
|
|
saveGlobals(t)
|
|
if err := LogToFile(filepath.Join(t.TempDir(), "app.log")); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !IsFileLogger() {
|
|
t.Fatal("IsFileLogger() = false after LogToFile")
|
|
}
|
|
if err := Shutdown(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if IsFileLogger() {
|
|
t.Fatal("IsFileLogger() = true after Shutdown")
|
|
}
|
|
}
|
|
|
|
func TestShutdownWithoutFile(t *testing.T) {
|
|
if err := Shutdown(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if IsFileLogger() {
|
|
t.Fatal("IsFileLogger() = true with no file")
|
|
}
|
|
}
|
|
|
|
func TestLogToTerminal(t *testing.T) {
|
|
saveGlobals(t)
|
|
LogToTerminal()
|
|
h := slog.Default().Handler()
|
|
if IsJSONHandler(h) {
|
|
t.Fatal("terminal handler reported as JSON")
|
|
}
|
|
SetLevel(slog.LevelWarn)
|
|
if h.Enabled(context.Background(), slog.LevelInfo) {
|
|
t.Fatal("terminal handler ignores the package level")
|
|
}
|
|
}
|
|
|
|
// On an interactive terminal the handler, and loggers derived from it, report
|
|
// that they write to the terminal; when stdout is a file or pipe they don't.
|
|
func TestWritesToTerminal(t *testing.T) {
|
|
for _, interactive := range []bool{true, false} {
|
|
saveGlobals(t)
|
|
isTerminal = func(*os.File) bool {
|
|
return interactive
|
|
}
|
|
|
|
LogToTerminal()
|
|
|
|
derived := slog.Default().With("component", "test").WithGroup("g")
|
|
for name, h := range map[string]slog.Handler{
|
|
"default": slog.Default().Handler(),
|
|
"derived": derived.Handler(),
|
|
} {
|
|
if got := WritesToTerminal(h); got != interactive {
|
|
t.Errorf(
|
|
"interactive %v, %s: WritesToTerminal = %v",
|
|
interactive, name, got,
|
|
)
|
|
}
|
|
}
|
|
}
|
|
if WritesToTerminal(slog.DiscardHandler) {
|
|
t.Error("another handler reported as the terminal")
|
|
}
|
|
}
|
|
|
|
func TestPrintToTerminal(t *testing.T) {
|
|
saveGlobals(t)
|
|
out, err := os.Create(filepath.Join(t.TempDir(), "terminal"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer out.Close()
|
|
terminal = out
|
|
|
|
PrintToTerminal("line one\nline two")
|
|
|
|
got, err := os.ReadFile(out.Name())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if string(got) != "line one\nline two\n" {
|
|
t.Errorf("printed %q; want the text as it is, then a newline", got)
|
|
}
|
|
}
|