From 0252264ddce35d34b87105c5848c84a160dda5f1 Mon Sep 17 00:00:00 2001 From: Michael Chus Date: Sat, 28 Feb 2026 22:17:00 +0300 Subject: [PATCH] parser: fallback zone-less source timestamps to Europe/Moscow --- internal/parser/timezone.go | 33 +++++++++++++++++++ internal/parser/vendors/inspur/sel.go | 3 +- .../vendors/nvidia/component_status_time.go | 6 ++-- .../nvidia/component_status_time_test.go | 22 ++++++------- internal/parser/vendors/nvidia/parser_test.go | 4 +-- internal/parser/vendors/unraid/parser.go | 2 +- internal/parser/vendors/xigmanas/parser.go | 2 +- 7 files changed, 53 insertions(+), 19 deletions(-) create mode 100644 internal/parser/timezone.go diff --git a/internal/parser/timezone.go b/internal/parser/timezone.go new file mode 100644 index 0000000..0ed43f0 --- /dev/null +++ b/internal/parser/timezone.go @@ -0,0 +1,33 @@ +package parser + +import ( + "sync" + "time" +) + +const fallbackTimezoneName = "Europe/Moscow" + +var ( + fallbackTimezoneOnce sync.Once + fallbackTimezone *time.Location +) + +// DefaultArchiveLocation returns the timezone used for source timestamps +// that do not contain an explicit offset. +func DefaultArchiveLocation() *time.Location { + fallbackTimezoneOnce.Do(func() { + loc, err := time.LoadLocation(fallbackTimezoneName) + if err != nil { + fallbackTimezone = time.FixedZone("MSK", 3*60*60) + return + } + fallbackTimezone = loc + }) + return fallbackTimezone +} + +// ParseInDefaultArchiveLocation parses timestamps without timezone information +// using Europe/Moscow as the assumed source timezone. +func ParseInDefaultArchiveLocation(layout, value string) (time.Time, error) { + return time.ParseInLocation(layout, value, DefaultArchiveLocation()) +} diff --git a/internal/parser/vendors/inspur/sel.go b/internal/parser/vendors/inspur/sel.go index 9770996..7443d45 100644 --- a/internal/parser/vendors/inspur/sel.go +++ b/internal/parser/vendors/inspur/sel.go @@ -6,6 +6,7 @@ import ( "time" "git.mchus.pro/mchus/logpile/internal/models" + "git.mchus.pro/mchus/logpile/internal/parser" ) // ParseSELList parses selelist.csv file with SEL events @@ -81,7 +82,7 @@ func parseSELTimestamp(dateStr, timeStr string) time.Time { timestampStr := dateStr + " " + timeStr // Try parsing with MM/DD/YYYY format - t, err := time.Parse("01/02/2006 15:04:05", timestampStr) + t, err := parser.ParseInDefaultArchiveLocation("01/02/2006 15:04:05", timestampStr) if err != nil { // Fallback to current time return time.Now() diff --git a/internal/parser/vendors/nvidia/component_status_time.go b/internal/parser/vendors/nvidia/component_status_time.go index 9d6ce9d..ed19c4d 100644 --- a/internal/parser/vendors/nvidia/component_status_time.go +++ b/internal/parser/vendors/nvidia/component_status_time.go @@ -112,7 +112,7 @@ func parseVerboseRunTestStartTimes(content []byte) map[string]time.Time { continue } - ts, err := time.ParseInLocation("2006-01-02 15:04:05", strings.TrimSpace(matches[1]), time.UTC) + ts, err := parser.ParseInDefaultArchiveLocation("2006-01-02 15:04:05", strings.TrimSpace(matches[1])) if err != nil { continue } @@ -135,7 +135,7 @@ func parseRunLogTestStartTimes(content []byte) map[string]time.Time { if len(matches) != 2 { continue } - parsed, err := time.ParseInLocation("Mon, 02 Jan 2006 15:04:05", strings.TrimSpace(matches[1]), time.UTC) + parsed, err := parser.ParseInDefaultArchiveLocation("Mon, 02 Jan 2006 15:04:05", strings.TrimSpace(matches[1])) if err != nil { continue } @@ -178,7 +178,7 @@ func parseModsStartTime(content []byte) time.Time { if tsRaw == "" { return time.Time{} } - ts, err := time.ParseInLocation("Mon Jan 2 15:04:05 2006", tsRaw, time.UTC) + ts, err := parser.ParseInDefaultArchiveLocation("Mon Jan 2 15:04:05 2006", tsRaw) if err != nil { return time.Time{} } diff --git a/internal/parser/vendors/nvidia/component_status_time_test.go b/internal/parser/vendors/nvidia/component_status_time_test.go index 00796dd..e2a20d6 100644 --- a/internal/parser/vendors/nvidia/component_status_time_test.go +++ b/internal/parser/vendors/nvidia/component_status_time_test.go @@ -23,10 +23,10 @@ func TestParseVerboseRunTestStartTimes(t *testing.T) { if gpu.IsZero() { t.Fatalf("expected gpu_fieldiag timestamp") } - if nvs.Format(time.RFC3339) != "2026-01-22T09:11:32Z" { + if nvs.UTC().Format(time.RFC3339) != "2026-01-22T06:11:32Z" { t.Fatalf("unexpected nvswitch timestamp: %s", nvs.Format(time.RFC3339)) } - if gpu.Format(time.RFC3339) != "2026-01-22T09:45:36Z" { + if gpu.UTC().Format(time.RFC3339) != "2026-01-22T06:45:36Z" { t.Fatalf("unexpected gpu_fieldiag timestamp: %s", gpu.Format(time.RFC3339)) } } @@ -40,13 +40,13 @@ Testing nvswitch OK [ 9:25s ] `) got := parseRunLogTestStartTimes(content) - if got["gpumem"].Format(time.RFC3339) != "2026-01-22T07:42:26Z" { + if got["gpumem"].UTC().Format(time.RFC3339) != "2026-01-22T04:42:26Z" { t.Fatalf("unexpected gpumem start: %s", got["gpumem"].Format(time.RFC3339)) } - if got["gpustress"].Format(time.RFC3339) != "2026-01-22T08:08:38Z" { + if got["gpustress"].UTC().Format(time.RFC3339) != "2026-01-22T05:08:38Z" { t.Fatalf("unexpected gpustress start: %s", got["gpustress"].Format(time.RFC3339)) } - if got["nvswitch"].Format(time.RFC3339) != "2026-01-22T08:15:48Z" { + if got["nvswitch"].UTC().Format(time.RFC3339) != "2026-01-22T05:15:48Z" { t.Fatalf("unexpected nvswitch start: %s", got["nvswitch"].Format(time.RFC3339)) } } @@ -101,10 +101,10 @@ func TestCollectGPUAndNVSwitchCheckTimes_FromVerboseRun(t *testing.T) { } got := CollectGPUAndNVSwitchCheckTimes(files) - if got.GPUDefault.Format(time.RFC3339) != "2026-01-22T09:45:36Z" { + if got.GPUDefault.UTC().Format(time.RFC3339) != "2026-01-22T06:45:36Z" { t.Fatalf("unexpected GPU check time: %s", got.GPUDefault.Format(time.RFC3339)) } - if got.NVSwitchDefault.Format(time.RFC3339) != "2026-01-22T09:11:32Z" { + if got.NVSwitchDefault.UTC().Format(time.RFC3339) != "2026-01-22T06:11:32Z" { t.Fatalf("unexpected NVSwitch check time: %s", got.NVSwitchDefault.Format(time.RFC3339)) } } @@ -128,16 +128,16 @@ MODS start: Thu Jan 22 09:11:32 2026 } got := CollectGPUAndNVSwitchCheckTimes(files) - if got.GPUBySerial["1653925025497"].Format(time.RFC3339) != "2026-01-22T09:45:36Z" { + if got.GPUBySerial["1653925025497"].UTC().Format(time.RFC3339) != "2026-01-22T06:45:36Z" { t.Fatalf("unexpected GPU serial check time: %s", got.GPUBySerial["1653925025497"].Format(time.RFC3339)) } - if got.GPUBySlot["GPUSXM5"].Format(time.RFC3339) != "2026-01-22T09:45:36Z" { + if got.GPUBySlot["GPUSXM5"].UTC().Format(time.RFC3339) != "2026-01-22T06:45:36Z" { t.Fatalf("unexpected GPU slot check time: %s", got.GPUBySlot["GPUSXM5"].Format(time.RFC3339)) } - if got.NVSwitchBySlot["NVSWITCH0"].Format(time.RFC3339) != "2026-01-22T09:11:32Z" { + if got.NVSwitchBySlot["NVSWITCH0"].UTC().Format(time.RFC3339) != "2026-01-22T06:11:32Z" { t.Fatalf("unexpected NVSwitch0 check time: %s", got.NVSwitchBySlot["NVSWITCH0"].Format(time.RFC3339)) } - if got.NVSwitchBySlot["NVSWITCH3"].Format(time.RFC3339) != "2026-01-22T09:11:32Z" { + if got.NVSwitchBySlot["NVSWITCH3"].UTC().Format(time.RFC3339) != "2026-01-22T06:11:32Z" { t.Fatalf("unexpected NVSwitch3 check time: %s", got.NVSwitchBySlot["NVSWITCH3"].Format(time.RFC3339)) } } diff --git a/internal/parser/vendors/nvidia/parser_test.go b/internal/parser/vendors/nvidia/parser_test.go index fc33aff..2441254 100644 --- a/internal/parser/vendors/nvidia/parser_test.go +++ b/internal/parser/vendors/nvidia/parser_test.go @@ -235,8 +235,8 @@ func TestNVIDIAParser_ComponentCheckTimes_RealArchive07900(t *testing.T) { t.Fatalf("expected hardware in parsed result") } - expectedGPU := time.Date(2026, 1, 22, 9, 45, 36, 0, time.UTC) - expectedNVSwitch := time.Date(2026, 1, 22, 9, 11, 32, 0, time.UTC) + expectedGPU := time.Date(2026, 1, 22, 6, 45, 36, 0, time.UTC) + expectedNVSwitch := time.Date(2026, 1, 22, 6, 11, 32, 0, time.UTC) if len(result.Hardware.GPUs) == 0 { t.Fatalf("expected GPUs in parsed result") diff --git a/internal/parser/vendors/unraid/parser.go b/internal/parser/vendors/unraid/parser.go index cee2752..a36a5ed 100644 --- a/internal/parser/vendors/unraid/parser.go +++ b/internal/parser/vendors/unraid/parser.go @@ -559,7 +559,7 @@ func parseSyslogLine(line string) (time.Time, string, models.Severity) { // Parse timestamp (add current year) year := time.Now().Year() - if ts, err := time.Parse("Jan 2 15:04:05 2006", timeStr+" "+strconv.Itoa(year)); err == nil { + if ts, err := parser.ParseInDefaultArchiveLocation("Jan 2 15:04:05 2006", timeStr+" "+strconv.Itoa(year)); err == nil { timestamp = ts } } diff --git a/internal/parser/vendors/xigmanas/parser.go b/internal/parser/vendors/xigmanas/parser.go index 759215d..d663cd5 100644 --- a/internal/parser/vendors/xigmanas/parser.go +++ b/internal/parser/vendors/xigmanas/parser.go @@ -431,7 +431,7 @@ func parseEventTimestamp(line string) time.Time { prefixRe := regexp.MustCompile(`^[A-Z][a-z]{2}\s+\d{1,2}\s+\d{2}:\d{2}:\d{2}`) if prefix := prefixRe.FindString(line); prefix != "" { year := time.Now().Year() - if ts, err := time.Parse("Jan 2 15:04:05 2006", prefix+" "+strconv.Itoa(year)); err == nil { + if ts, err := parser.ParseInDefaultArchiveLocation("Jan 2 15:04:05 2006", prefix+" "+strconv.Itoa(year)); err == nil { return ts } }