package logx import ( "bytes" "context" "encoding/json" "io" "log/slog" "os" "path/filepath" "strings" "sync" "testing" "time" ) func TestLoggerTimeZoneIsolation(t *testing.T) { originalLocal := time.Local stamp := time.Date(2026, 7, 1, 0, 0, 0, 123456000, time.UTC) for _, format := range []string{"json", "text"} { t.Run(format, func(t *testing.T) { var a, b bytes.Buffer sh, err := New(Config{TimeZone: "Asia/Shanghai", Format: format, Writer: &a}) if err != nil { t.Fatal(err) } defer sh.Close() utc, err := New(Config{TimeZone: "UTC", Format: format, Writer: &b}) if err != nil { t.Fatal(err) } defer utc.Close() r := slog.NewRecord(stamp, slog.LevelInfo, "timestamp", 0) r.AddAttrs(slog.Time("eventTime", stamp), slog.String("password", "secret")) if err := sh.With("service", "first").WithGroup("data").logger.Handler().Handle(context.Background(), r); err != nil { t.Fatal(err) } if err := utc.logger.Handler().Handle(context.Background(), r); err != nil { t.Fatal(err) } if !strings.Contains(a.String(), "08:00:00") || !strings.Contains(a.String(), "+08:00") || !strings.Contains(b.String(), "00:00:00") || strings.Contains(b.String(), "+08:00") { t.Fatalf("a=%s b=%s", a.String(), b.String()) } if strings.Contains(a.String(), "secret") || strings.Contains(b.String(), "secret") { t.Fatal("redaction lost") } if format == "json" { var out map[string]any if err := json.Unmarshal(a.Bytes(), &out); err != nil { t.Fatal(err) } if out["time"] != "2026-07-01T08:00:00.123456+08:00" || out["data"].(map[string]any)["eventTime"] != "2026-07-01T00:00:00.123456Z" { t.Fatal(out) } } }) } if time.Local != originalLocal { t.Fatal("global time.Local changed") } } func TestLoggerTimeZoneValidationAndDST(t *testing.T) { path := filepath.Join(t.TempDir(), "not-created", "app.log") if _, err := New(Config{TimeZone: "Secret/Not_A_Zone", File: path}); err == nil || strings.Contains(err.Error(), "Secret") { t.Fatal(err) } if _, err := os.Stat(filepath.Dir(path)); !os.IsNotExist(err) { t.Fatal("invalid zone opened writer") } var out bytes.Buffer l, err := New(Config{TimeZone: "America/New_York", Writer: &out}) if err != nil { t.Fatal(err) } defer l.Close() for _, month := range []time.Month{time.January, time.July} { r := slog.NewRecord(time.Date(2026, month, 1, 12, 0, 0, 0, time.UTC), slog.LevelInfo, "dst", 0) if err := l.logger.Handler().Handle(context.Background(), r); err != nil { t.Fatal(err) } } if !strings.Contains(out.String(), "-05:00") || !strings.Contains(out.String(), "-04:00") { t.Fatal(out.String()) } // Explicit empty location preserves an incoming record's original zone. out.Reset() plain, _ := New(Config{Writer: &out}) defer plain.Close() r := slog.NewRecord(time.Date(2026, 1, 1, 0, 0, 0, 0, time.FixedZone("original", 7200)), slog.LevelInfo, "unchanged", 0) plain.logger.Handler().Handle(context.Background(), r) if !strings.Contains(out.String(), "+02:00") { t.Fatal(out.String()) } } func TestLoggerTimeZoneConcurrentChildren(t *testing.T) { var out bytes.Buffer l, err := New(Config{TimeZone: "Asia/Shanghai", Writer: &out}) if err != nil { t.Fatal(err) } defer l.Close() var wg sync.WaitGroup for i := 0; i < 20; i++ { wg.Add(1) go func(i int) { defer wg.Done() child := l.With("worker", i).WithGroup("event") for n := 0; n < 10; n++ { child.Standard(slog.LevelInfo).Print("message") } }(i) } wg.Wait() if strings.Count(out.String(), "+08:00") != 200 { t.Fatal("timestamps missing") } } func BenchmarkLoggerTimeZone(b *testing.B) { for _, zone := range []string{"", "UTC", "Asia/Shanghai"} { b.Run(zone, func(b *testing.B) { l, err := New(Config{TimeZone: zone, Writer: io.Discard}) if err != nil { b.Fatal(err) } defer l.Close() b.ReportAllocs() b.ResetTimer() for i := 0; i < b.N; i++ { l.InfoContext(context.Background(), "benchmark", "count", 1) } }) } }