starlog/structured_test.go

202 lines
5.2 KiB
Go
Raw Normal View History

2026-03-19 16:37:57 +08:00
package starlog
import (
"bytes"
"context"
"encoding/json"
"errors"
"io"
"os"
"regexp"
"strings"
"testing"
)
var ansiRegex = regexp.MustCompile(`\x1b\[[0-9;]*m`)
func newStructuredTestLogger(output io.Writer) *StarLogger {
logger := NewStarlog(output)
logger.SetShowStd(false)
logger.SetShowColor(false)
logger.SetShowOriginFile(false)
logger.SetShowFuncName(false)
logger.SetShowFlag(false)
return logger
}
func TestWithFieldAndWithFields(t *testing.T) {
var buf bytes.Buffer
logger := newStructuredTestLogger(&buf)
logger.WithField("user_id", 42).WithFields(Fields{
"module": "auth",
"ip": "127.0.0.1",
}).Info("login ok")
logStr := buf.String()
if !strings.Contains(logStr, "login ok") {
t.Fatalf("expected message in log, got %q", logStr)
}
if !strings.Contains(logStr, "user_id=42") || !strings.Contains(logStr, "module=auth") || !strings.Contains(logStr, "ip=127.0.0.1") {
t.Fatalf("expected structured fields in log, got %q", logStr)
}
}
func TestWithFieldIsolation(t *testing.T) {
var buf bytes.Buffer
logger := newStructuredTestLogger(&buf)
logger.Info("base")
baseLog := buf.String()
if strings.Contains(baseLog, "req_id=") {
t.Fatalf("base logger should not include req_id field, got %q", baseLog)
}
buf.Reset()
logger.WithField("req_id", "r-1").Info("child")
childLog := buf.String()
if !strings.Contains(childLog, "req_id=r-1") {
t.Fatalf("child logger should include req_id field, got %q", childLog)
}
buf.Reset()
logger.Info("base-again")
baseAgain := buf.String()
if strings.Contains(baseAgain, "req_id=r-1") {
t.Fatalf("base logger should remain clean after WithField, got %q", baseAgain)
}
}
func TestWithError(t *testing.T) {
var buf bytes.Buffer
logger := newStructuredTestLogger(&buf)
logger.WithError(errors.New("boom")).Error("request failed")
logStr := buf.String()
if !strings.Contains(logStr, "request failed") || !strings.Contains(logStr, "error=boom") {
t.Fatalf("expected error details in log, got %q", logStr)
}
}
func TestWithContextExtractor(t *testing.T) {
var buf bytes.Buffer
logger := newStructuredTestLogger(&buf)
logger.SetContextFieldExtractor(func(ctx context.Context) Fields {
traceID, _ := ctx.Value("trace_id").(string)
if traceID == "" {
return nil
}
return Fields{"trace_id": traceID}
})
ctx := context.WithValue(context.Background(), "trace_id", "trace-001")
logger.WithContext(ctx).Info("context log")
logStr := buf.String()
if !strings.Contains(logStr, "context log") || !strings.Contains(logStr, "trace_id=trace-001") {
t.Fatalf("expected context extracted fields in log, got %q", logStr)
}
}
func TestJSONFormatterWithStructuredFields(t *testing.T) {
var buf bytes.Buffer
logger := newStructuredTestLogger(&buf)
logger.SetFormatter(NewJSONFormatter())
logger.SetShowColor(false)
logger.WithField("user_id", 7).WithError(errors.New("db down")).Error("save failed")
payload := make(map[string]interface{})
if err := json.Unmarshal(buf.Bytes(), &payload); err != nil {
t.Fatalf("json unmarshal failed: %v, raw=%q", err, buf.String())
}
if payload["msg"] != "save failed" {
t.Fatalf("unexpected msg: %v", payload["msg"])
}
if payload["error"] != "db down" {
t.Fatalf("unexpected error field: %v", payload["error"])
}
fieldsObj, ok := payload["fields"].(map[string]interface{})
if !ok {
t.Fatalf("fields should be object, got %T", payload["fields"])
}
if fieldsObj["user_id"] != float64(7) {
t.Fatalf("unexpected user_id value: %v", fieldsObj["user_id"])
}
}
func TestLevelOnlyFieldColorRender(t *testing.T) {
oldNoColor := NoColor
NoColor = false
defer func() {
NoColor = oldNoColor
}()
logger := NewStarlog(nil)
logger.SetShowStd(true)
logger.SetShowColor(true)
logger.SetColorMode(ColorModeLevelOnly)
logger.SetShowOriginFile(false)
logger.SetShowFuncName(false)
logger.SetShowFlag(false)
logger.SetShowFieldColor(true)
var out bytes.Buffer
oldStd := stdScreen
oldErr := errScreen
stdScreen = &out
errScreen = io.Discard
defer func() {
stdScreen = oldStd
errScreen = oldErr
}()
logger.WithFields(Fields{
"user": "alice",
"ok": true,
"cnt": 3,
}).Info("login")
rendered := out.String()
if !strings.Contains(rendered, "\x1b[") {
t.Fatalf("expected ansi colors in rendered log, got %q", rendered)
}
clean := ansiRegex.ReplaceAllString(rendered, "")
if !strings.Contains(clean, "user=alice") || !strings.Contains(clean, "ok=true") || !strings.Contains(clean, "cnt=3") {
t.Fatalf("expected fields in rendered log, got %q", clean)
}
}
func TestDisableFieldColorRender(t *testing.T) {
oldNoColor := NoColor
NoColor = false
defer func() {
NoColor = oldNoColor
}()
logger := NewStarlog(nil)
logger.SetShowStd(true)
logger.SetShowColor(true)
logger.SetColorMode(ColorModeLevelOnly)
logger.SetShowOriginFile(false)
logger.SetShowFuncName(false)
logger.SetShowFlag(false)
logger.SetShowFieldColor(false)
var out bytes.Buffer
oldStd := stdScreen
oldErr := errScreen
stdScreen = &out
errScreen = os.Stderr
defer func() {
stdScreen = oldStd
errScreen = oldErr
}()
logger.WithField("user", "alice").Info("login")
rendered := out.String()
if strings.Count(rendered, "\x1b[") > 2 {
t.Fatalf("field color should be disabled, got %q", rendered)
}
}