202 lines
5.2 KiB
Go
202 lines
5.2 KiB
Go
|
|
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)
|
||
|
|
}
|
||
|
|
}
|