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) } }