505 lines
15 KiB
Go
505 lines
15 KiB
Go
package compat
|
|
|
|
import (
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/lixenwraith/log"
|
|
)
|
|
|
|
func eq[T comparable](tb testing.TB, got, want T, ctx string) {
|
|
tb.Helper()
|
|
if got != want {
|
|
tb.Errorf("%s: got %#v, want %#v", ctx, got, want)
|
|
}
|
|
}
|
|
|
|
func mustNoErr(tb testing.TB, err error, ctx string) {
|
|
tb.Helper()
|
|
if err != nil {
|
|
tb.Fatalf("%s: unexpected error: %v", ctx, err)
|
|
}
|
|
}
|
|
|
|
func errContains(tb testing.TB, err error, sub, ctx string) {
|
|
tb.Helper()
|
|
switch {
|
|
case err == nil:
|
|
tb.Errorf("%s: expected error containing %q, got nil", ctx, sub)
|
|
case !strings.Contains(err.Error(), sub):
|
|
tb.Errorf("%s: error %q does not contain %q", ctx, err, sub)
|
|
}
|
|
}
|
|
|
|
// newTestBuilder returns a builder bound to a started json-format logger.
|
|
func newTestBuilder(tb testing.TB) (*Builder, *log.Logger, string) {
|
|
tb.Helper()
|
|
tmpDir := tb.TempDir()
|
|
|
|
appLogger, err := log.NewBuilder().
|
|
Directory(tmpDir).
|
|
Format("json").
|
|
LevelString("debug").
|
|
EnableConsole(false).
|
|
EnableFile(true).
|
|
Build()
|
|
mustNoErr(tb, err, "Build")
|
|
mustNoErr(tb, appLogger.Start(), "Start")
|
|
tb.Cleanup(func() { _ = appLogger.Shutdown() })
|
|
|
|
return NewBuilder().WithLogger(appLogger), appLogger, tmpDir
|
|
}
|
|
|
|
// readLogLines polls the active log file until it holds at least want records.
|
|
func readLogLines(tb testing.TB, dir string, want int) []string {
|
|
tb.Helper()
|
|
path := filepath.Join(dir, "log.log")
|
|
deadline := time.Now().Add(2 * time.Second)
|
|
|
|
for {
|
|
if data, err := os.ReadFile(path); err == nil {
|
|
trimmed := strings.TrimRight(string(data), "\n")
|
|
if trimmed != "" {
|
|
lines := strings.Split(trimmed, "\n")
|
|
if len(lines) >= want {
|
|
return lines
|
|
}
|
|
}
|
|
}
|
|
if time.Now().After(deadline) {
|
|
tb.Fatalf("did not read %d log lines from %s", want, dir)
|
|
}
|
|
time.Sleep(5 * time.Millisecond)
|
|
}
|
|
}
|
|
|
|
// recordOf parses one json record into its level and flat fields array.
|
|
func recordOf(tb testing.TB, line string) (string, []any) {
|
|
tb.Helper()
|
|
var entry map[string]any
|
|
if err := json.Unmarshal([]byte(line), &entry); err != nil {
|
|
tb.Fatalf("parse log line %q: %v", line, err)
|
|
}
|
|
level, _ := entry["level"].(string)
|
|
fields, ok := entry["fields"].([]any)
|
|
if !ok {
|
|
tb.Fatalf("record has no fields array: %s", line)
|
|
}
|
|
return level, fields
|
|
}
|
|
|
|
// checkFields compares the leading elements of a fields array.
|
|
func checkFields(tb testing.TB, fields []any, want []any, ctx string) {
|
|
tb.Helper()
|
|
if len(fields) < len(want) {
|
|
tb.Fatalf("%s: got %d fields, want at least %d: %v", ctx, len(fields), len(want), fields)
|
|
}
|
|
for i, w := range want {
|
|
if fields[i] != w {
|
|
tb.Errorf("%s: field %d = %#v, want %#v", ctx, i, fields[i], w)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestBuilderSources verifies logger resolution from an instance, a config, or defaults.
|
|
func TestBuilderSources(t *testing.T) {
|
|
t.Run("existing logger", func(t *testing.T) {
|
|
builder, logger, _ := newTestBuilder(t)
|
|
|
|
adapter, err := builder.BuildGnet()
|
|
mustNoErr(t, err, "BuildGnet")
|
|
if adapter == nil {
|
|
t.Fatal("BuildGnet returned nil")
|
|
}
|
|
if adapter.logger != logger {
|
|
t.Error("adapter must reuse the provided logger")
|
|
}
|
|
})
|
|
|
|
t.Run("config creates and caches a logger", func(t *testing.T) {
|
|
cfg := log.DefaultConfig()
|
|
cfg.Directory = t.TempDir()
|
|
cfg.EnableConsole = false
|
|
|
|
builder := NewBuilder().WithConfig(cfg)
|
|
adapter, err := builder.BuildFastHTTP()
|
|
mustNoErr(t, err, "BuildFastHTTP")
|
|
if adapter == nil {
|
|
t.Fatal("BuildFastHTTP returned nil")
|
|
}
|
|
|
|
logger, err := builder.GetLogger()
|
|
mustNoErr(t, err, "GetLogger")
|
|
t.Cleanup(func() { _ = logger.Shutdown() })
|
|
|
|
// Subsequent builds reuse the cached instance
|
|
second, err := builder.GetLogger()
|
|
mustNoErr(t, err, "GetLogger second call")
|
|
if second != logger {
|
|
t.Error("builder must cache the created logger")
|
|
}
|
|
eq(t, logger.GetConfig().Directory, cfg.Directory, "applied directory")
|
|
})
|
|
|
|
t.Run("nil config falls back to defaults", func(t *testing.T) {
|
|
logger, err := NewBuilder().WithConfig(nil).GetLogger()
|
|
mustNoErr(t, err, "GetLogger")
|
|
t.Cleanup(func() { _ = logger.Shutdown() })
|
|
eq(t, logger.GetConfig().Format, log.DefaultConfig().Format, "default format")
|
|
})
|
|
|
|
t.Run("nil logger is rejected", func(t *testing.T) {
|
|
builder := NewBuilder().WithLogger(nil)
|
|
_, err := builder.BuildGnet()
|
|
errContains(t, err, "provided logger cannot be nil", "BuildGnet")
|
|
|
|
// The deferred error persists across build calls
|
|
_, err = builder.BuildFiber()
|
|
errContains(t, err, "provided logger cannot be nil", "BuildFiber")
|
|
})
|
|
|
|
t.Run("invalid config propagates", func(t *testing.T) {
|
|
cfg := log.DefaultConfig()
|
|
cfg.Directory = t.TempDir()
|
|
cfg.Format = "yaml"
|
|
|
|
_, err := NewBuilder().WithConfig(cfg).BuildGnet()
|
|
errContains(t, err, "invalid format", "BuildGnet")
|
|
})
|
|
|
|
t.Run("all adapters build from one logger", func(t *testing.T) {
|
|
builder, logger, _ := newTestBuilder(t)
|
|
|
|
gnetAdapter, err := builder.BuildGnet()
|
|
mustNoErr(t, err, "BuildGnet")
|
|
structuredAdapter, err := builder.BuildStructuredGnet()
|
|
mustNoErr(t, err, "BuildStructuredGnet")
|
|
fasthttpAdapter, err := builder.BuildFastHTTP()
|
|
mustNoErr(t, err, "BuildFastHTTP")
|
|
fiberAdapter, err := builder.BuildFiber()
|
|
mustNoErr(t, err, "BuildFiber")
|
|
|
|
if gnetAdapter.logger != logger || structuredAdapter.logger != logger ||
|
|
fasthttpAdapter.logger != logger || fiberAdapter.logger != logger {
|
|
t.Error("every adapter must share the provided logger")
|
|
}
|
|
})
|
|
}
|
|
|
|
// TestGnetAdapter verifies level mapping and the fatal handler override.
|
|
func TestGnetAdapter(t *testing.T) {
|
|
builder, logger, tmpDir := newTestBuilder(t)
|
|
|
|
var fatalCalled bool
|
|
adapter, err := builder.BuildGnet(WithFatalHandler(func(msg string) {
|
|
fatalCalled = true
|
|
}))
|
|
mustNoErr(t, err, "BuildGnet")
|
|
|
|
adapter.Debugf("gnet debug id=%d", 1)
|
|
adapter.Infof("gnet info id=%d", 2)
|
|
adapter.Warnf("gnet warn id=%d", 3)
|
|
adapter.Errorf("gnet error id=%d", 4)
|
|
adapter.Fatalf("gnet fatal id=%d", 5)
|
|
|
|
mustNoErr(t, logger.Flush(time.Second), "Flush")
|
|
lines := readLogLines(t, tmpDir, 5)
|
|
eq(t, len(lines), 5, "record count")
|
|
|
|
expected := []struct{ level, msg string }{
|
|
{"DEBUG", "gnet debug id=1"},
|
|
{"INFO", "gnet info id=2"},
|
|
{"WARN", "gnet warn id=3"},
|
|
{"ERROR", "gnet error id=4"},
|
|
{"ERROR", "gnet fatal id=5"},
|
|
}
|
|
|
|
for i, line := range lines {
|
|
level, fields := recordOf(t, line)
|
|
eq(t, level, expected[i].level, "level")
|
|
checkFields(t, fields, []any{"msg", expected[i].msg, "source", "gnet"}, expected[i].msg)
|
|
}
|
|
|
|
// The fatal record carries a marker beyond the common prefix
|
|
_, fatalFields := recordOf(t, lines[4])
|
|
checkFields(t, fatalFields, []any{"msg", "gnet fatal id=5", "source", "gnet", "fatal", true}, "fatal marker")
|
|
if !fatalCalled {
|
|
t.Error("custom fatal handler was not invoked")
|
|
}
|
|
}
|
|
|
|
// TestStructuredGnetAdapter verifies key/value extraction from printf formats.
|
|
func TestStructuredGnetAdapter(t *testing.T) {
|
|
builder, logger, tmpDir := newTestBuilder(t)
|
|
|
|
adapter, err := builder.BuildStructuredGnet()
|
|
mustNoErr(t, err, "BuildStructuredGnet")
|
|
|
|
adapter.Infof("request served status=%d client_ip=%s", 200, "127.0.0.1")
|
|
// No key=verb pattern: the whole message collapses into a msg field
|
|
adapter.Warnf("plain message %d", 42)
|
|
|
|
mustNoErr(t, logger.Flush(time.Second), "Flush")
|
|
lines := readLogLines(t, tmpDir, 2)
|
|
eq(t, len(lines), 2, "record count")
|
|
|
|
level, fields := recordOf(t, lines[0])
|
|
eq(t, level, "INFO", "level")
|
|
// JSON numbers decode as float64
|
|
checkFields(t, fields, []any{
|
|
"msg", "request served",
|
|
"status", 200.0,
|
|
"client_ip", "127.0.0.1",
|
|
"source", "gnet",
|
|
}, "extracted fields")
|
|
|
|
level, fields = recordOf(t, lines[1])
|
|
eq(t, level, "WARN", "level")
|
|
checkFields(t, fields, []any{"msg", "plain message 42", "source", "gnet"}, "fallback")
|
|
}
|
|
|
|
// TestFastHTTPAdapter verifies content-based level detection.
|
|
func TestFastHTTPAdapter(t *testing.T) {
|
|
builder, logger, tmpDir := newTestBuilder(t)
|
|
|
|
adapter, err := builder.BuildFastHTTP()
|
|
mustNoErr(t, err, "BuildFastHTTP")
|
|
|
|
messages := []string{
|
|
"this is some informational message",
|
|
"a debug message for the developers",
|
|
"warning: something might be wrong",
|
|
"an error occurred while processing",
|
|
}
|
|
for _, msg := range messages {
|
|
adapter.Printf("%s", msg)
|
|
}
|
|
|
|
mustNoErr(t, logger.Flush(time.Second), "Flush")
|
|
lines := readLogLines(t, tmpDir, len(messages))
|
|
eq(t, len(lines), len(messages), "record count")
|
|
|
|
levels := []string{"INFO", "DEBUG", "WARN", "ERROR"}
|
|
for i, line := range lines {
|
|
level, fields := recordOf(t, line)
|
|
eq(t, level, levels[i], "detected level")
|
|
checkFields(t, fields, []any{"msg", messages[i], "source", "fasthttp"}, messages[i])
|
|
}
|
|
}
|
|
|
|
// TestDetectLogLevel covers the keyword table directly.
|
|
func TestDetectLogLevel(t *testing.T) {
|
|
tests := []struct {
|
|
msg string
|
|
want int64
|
|
}{
|
|
{"connection failed", log.LevelError},
|
|
{"FATAL condition", log.LevelError},
|
|
{"panic recovered", log.LevelError},
|
|
{"Error: bad input", log.LevelError},
|
|
{"deprecated call site", log.LevelWarn},
|
|
{"WARNING: retrying", log.LevelWarn},
|
|
{"trace enabled", log.LevelDebug},
|
|
{"debug output", log.LevelDebug},
|
|
{"server started", log.LevelInfo},
|
|
{"", log.LevelInfo},
|
|
// Error keywords are matched before warning keywords
|
|
{"warning: request failed", log.LevelError},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
if got := DetectLogLevel(tt.msg); got != tt.want {
|
|
t.Errorf("DetectLogLevel(%q) = %d, want %d", tt.msg, got, tt.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestFastHTTPOptions verifies the default level and detector overrides.
|
|
// Note: LevelInfo is zero, which the adapter treats as "not detected", so a
|
|
// detector cannot force Info over a non-Info default.
|
|
func TestFastHTTPDefaultLevel(t *testing.T) {
|
|
builder, logger, tmpDir := newTestBuilder(t)
|
|
|
|
adapter, err := builder.BuildFastHTTP(
|
|
WithDefaultLevel(log.LevelWarn),
|
|
WithLevelDetector(func(msg string) int64 {
|
|
if strings.Contains(msg, "boom") {
|
|
return log.LevelError
|
|
}
|
|
return log.LevelInfo // indistinguishable from "no detection"
|
|
}),
|
|
)
|
|
mustNoErr(t, err, "BuildFastHTTP")
|
|
|
|
adapter.Printf("undetected message")
|
|
adapter.Printf("boom happened")
|
|
|
|
mustNoErr(t, logger.Flush(time.Second), "Flush")
|
|
lines := readLogLines(t, tmpDir, 2)
|
|
|
|
level, _ := recordOf(t, lines[0])
|
|
eq(t, level, "WARN", "default level applies when detection yields Info")
|
|
level, _ = recordOf(t, lines[1])
|
|
eq(t, level, "ERROR", "detector overrides the default")
|
|
}
|
|
|
|
// TestFiberAdapter verifies the FormatLogger surface and both handler overrides.
|
|
func TestFiberAdapter(t *testing.T) {
|
|
builder, logger, tmpDir := newTestBuilder(t)
|
|
|
|
var fatalCalled, panicCalled bool
|
|
adapter, err := builder.BuildFiber(
|
|
WithFiberFatalHandler(func(msg string) { fatalCalled = true }),
|
|
WithFiberPanicHandler(func(msg string) { panicCalled = true }),
|
|
)
|
|
mustNoErr(t, err, "BuildFiber")
|
|
|
|
adapter.Tracef("fiber trace id=%d", 1)
|
|
adapter.Debugf("fiber debug id=%d", 2)
|
|
adapter.Infof("fiber info id=%d", 3)
|
|
adapter.Warnf("fiber warn id=%d", 4)
|
|
adapter.Errorf("fiber error id=%d", 5)
|
|
adapter.Fatalf("fiber fatal id=%d", 6)
|
|
adapter.Panicf("fiber panic id=%d", 7)
|
|
|
|
mustNoErr(t, logger.Flush(time.Second), "Flush")
|
|
lines := readLogLines(t, tmpDir, 7)
|
|
eq(t, len(lines), 7, "record count")
|
|
|
|
expected := []struct{ level, msg string }{
|
|
{"DEBUG", "fiber trace id=1"},
|
|
{"DEBUG", "fiber debug id=2"},
|
|
{"INFO", "fiber info id=3"},
|
|
{"WARN", "fiber warn id=4"},
|
|
{"ERROR", "fiber error id=5"},
|
|
{"ERROR", "fiber fatal id=6"},
|
|
{"ERROR", "fiber panic id=7"},
|
|
}
|
|
|
|
for i, line := range lines {
|
|
level, fields := recordOf(t, line)
|
|
eq(t, level, expected[i].level, "level")
|
|
checkFields(t, fields, []any{"msg", expected[i].msg, "source", "fiber"}, expected[i].msg)
|
|
}
|
|
|
|
// Trace maps onto debug and is distinguished by an extra field
|
|
_, traceFields := recordOf(t, lines[0])
|
|
checkFields(t, traceFields, []any{"msg", "fiber trace id=1", "source", "fiber", "level", "trace"}, "trace marker")
|
|
|
|
if !fatalCalled {
|
|
t.Error("custom fatal handler was not invoked")
|
|
}
|
|
if !panicCalled {
|
|
t.Error("custom panic handler was not invoked")
|
|
}
|
|
}
|
|
|
|
// TestFiberAdapterPlain verifies the Logger surface built from fmt.Sprint.
|
|
func TestFiberAdapterPlain(t *testing.T) {
|
|
builder, logger, tmpDir := newTestBuilder(t)
|
|
|
|
adapter, err := builder.BuildFiber()
|
|
mustNoErr(t, err, "BuildFiber")
|
|
|
|
adapter.Info("plain ", "info")
|
|
adapter.Error("plain ", "error")
|
|
|
|
mustNoErr(t, logger.Flush(time.Second), "Flush")
|
|
lines := readLogLines(t, tmpDir, 2)
|
|
|
|
level, fields := recordOf(t, lines[0])
|
|
eq(t, level, "INFO", "level")
|
|
checkFields(t, fields, []any{"msg", "plain info", "source", "fiber"}, "Info")
|
|
|
|
level, fields = recordOf(t, lines[1])
|
|
eq(t, level, "ERROR", "level")
|
|
checkFields(t, fields, []any{"msg", "plain error", "source", "fiber"}, "Error")
|
|
}
|
|
|
|
// TestFiberAdapterStructured verifies the WithLogger surface.
|
|
func TestFiberAdapterStructured(t *testing.T) {
|
|
builder, logger, tmpDir := newTestBuilder(t)
|
|
|
|
adapter, err := builder.BuildFiber()
|
|
mustNoErr(t, err, "BuildFiber")
|
|
|
|
adapter.Infow("request served", "status", 200, "client_ip", "127.0.0.1", "method", "GET")
|
|
adapter.Debugw("query executed", "duration_ms", 42, "query", "SELECT")
|
|
adapter.Warnw("slow response", "duration_ms", 900)
|
|
|
|
mustNoErr(t, logger.Flush(time.Second), "Flush")
|
|
lines := readLogLines(t, tmpDir, 3)
|
|
eq(t, len(lines), 3, "record count")
|
|
|
|
// Adapter-owned fields precede caller-supplied pairs
|
|
level, fields := recordOf(t, lines[0])
|
|
eq(t, level, "INFO", "level")
|
|
checkFields(t, fields, []any{
|
|
"msg", "request served", "source", "fiber",
|
|
"status", 200.0, "client_ip", "127.0.0.1", "method", "GET",
|
|
}, "Infow")
|
|
|
|
level, fields = recordOf(t, lines[1])
|
|
eq(t, level, "DEBUG", "level")
|
|
checkFields(t, fields, []any{
|
|
"msg", "query executed", "source", "fiber",
|
|
"duration_ms", 42.0, "query", "SELECT",
|
|
}, "Debugw")
|
|
|
|
level, fields = recordOf(t, lines[2])
|
|
eq(t, level, "WARN", "level")
|
|
checkFields(t, fields, []any{
|
|
"msg", "slow response", "source", "fiber", "duration_ms", 900.0,
|
|
}, "Warnw")
|
|
}
|
|
|
|
// TestFiberAdapterStructuredFatal verifies Fatalw ordering and handler dispatch.
|
|
func TestFiberAdapterStructuredFatal(t *testing.T) {
|
|
builder, logger, tmpDir := newTestBuilder(t)
|
|
|
|
var fatalCalled bool
|
|
adapter, err := builder.BuildFiber(
|
|
WithFiberFatalHandler(func(msg string) { fatalCalled = true }),
|
|
)
|
|
mustNoErr(t, err, "BuildFiber")
|
|
|
|
adapter.Fatalw("shutting down", "code", 3)
|
|
|
|
mustNoErr(t, logger.Flush(time.Second), "Flush")
|
|
lines := readLogLines(t, tmpDir, 1)
|
|
|
|
level, fields := recordOf(t, lines[0])
|
|
eq(t, level, "ERROR", "level")
|
|
checkFields(t, fields, []any{
|
|
"msg", "shutting down", "source", "fiber", "fatal", true, "code", 3.0,
|
|
}, "Fatalw")
|
|
if !fatalCalled {
|
|
t.Error("custom fatal handler was not invoked")
|
|
}
|
|
}
|
|
|
|
// TestFiberAdapterWriter verifies the io.Writer implementation.
|
|
func TestFiberAdapterWriter(t *testing.T) {
|
|
builder, logger, tmpDir := newTestBuilder(t)
|
|
|
|
adapter, err := builder.BuildFiber()
|
|
mustNoErr(t, err, "BuildFiber")
|
|
|
|
payload := []byte("writer output\n")
|
|
n, err := adapter.Write(payload)
|
|
mustNoErr(t, err, "Write")
|
|
eq(t, n, len(payload), "byte count includes the trimmed newline")
|
|
|
|
mustNoErr(t, logger.Flush(time.Second), "Flush")
|
|
lines := readLogLines(t, tmpDir, 1)
|
|
|
|
level, fields := recordOf(t, lines[0])
|
|
eq(t, level, "INFO", "level")
|
|
checkFields(t, fields, []any{"msg", "writer output", "source", "fiber"}, "Write")
|
|
}
|
|
|