package inspur import ( "bufio" "bytes" "regexp" "sort" "strings" "time" "git.mchus.pro/mchus/logpile/internal/parser" ) var explicitInspurTimestampRegex = regexp.MustCompile(`\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?[+-]\d{2}:\d{2}`) type inspurOffsetSample struct { local time.Time offset int } // inspurTimezoneResolver handles dumps where timezone.conf is stale or the BMC // timezone changed during the retained SEL history. Offset-bearing IDL/syslog // timestamps are authoritative; timezone.conf remains the fallback. type inspurTimezoneResolver struct { fallback *time.Location samples []inspurOffsetSample } func inferInspurTimezoneResolver(files []parser.ExtractedFile, fallback *time.Location) *inspurTimezoneResolver { if fallback == nil { fallback = parser.DefaultArchiveLocation() } r := &inspurTimezoneResolver{fallback: fallback} seen := make(map[string]struct{}) preferIDL := false for _, f := range files { if strings.Contains(strings.ToLower(f.Path), "idl") { preferIDL = true break } } for pass := 0; pass < 2; pass++ { idlOnly := preferIDL && pass == 0 if pass == 1 && (!preferIDL || len(r.samples) > 0) { break } for _, f := range files { path := strings.ToLower(f.Path) if idlOnly && !strings.Contains(path, "idl") { continue } if !strings.Contains(path, "/log/") && !strings.Contains(path, "idl") { continue } scanner := bufio.NewScanner(bytes.NewReader(f.Content)) scanner.Buffer(make([]byte, 64*1024), 1024*1024) for scanner.Scan() { matches := explicitInspurTimestampRegex.FindAllString(scanner.Text(), -1) if len(matches) == 0 { continue } // idl.log is itself a syslog stream. Its outer timestamp can retain // an obsolete BMC offset while the embedded IDL record has the offset // that was active for the hardware event. Prefer the embedded value. raw := matches[len(matches)-1] ts, err := time.Parse(time.RFC3339Nano, raw) if err != nil || ts.Year() < 2000 { continue } _, offset := ts.Zone() local := time.Date(ts.Year(), ts.Month(), ts.Day(), ts.Hour(), ts.Minute(), ts.Second(), 0, time.UTC) // One sample per local day and offset is sufficient to identify BMC // timezone eras while bounding memory on very large rotated logs. key := local.Format("2006-01-02") + "|" + strings.TrimSpace(raw[len(raw)-6:]) if _, ok := seen[key]; ok { continue } seen[key] = struct{}{} r.samples = append(r.samples, inspurOffsetSample{local: local, offset: offset}) } } } sort.Slice(r.samples, func(i, j int) bool { return r.samples[i].local.Before(r.samples[j].local) }) return r } func (r *inspurTimezoneResolver) locationFor(local time.Time) *time.Location { if r == nil || len(r.samples) == 0 { if r != nil && r.fallback != nil { return r.fallback } return parser.DefaultArchiveLocation() } best := r.samples[0] bestDistance := absDuration(local.Sub(best.local)) for _, sample := range r.samples[1:] { distance := absDuration(local.Sub(sample.local)) if distance < bestDistance { best = sample bestDistance = distance } } return time.FixedZone(formatUTCOffset(best.offset), best.offset) } func absDuration(v time.Duration) time.Duration { if v < 0 { return -v } return v } func formatUTCOffset(offset int) string { sign := "+" if offset < 0 { sign = "-" offset = -offset } return "UTC" + sign + twoDigits(offset/3600) + ":" + twoDigits((offset%3600)/60) } func twoDigits(v int) string { return string([]byte{'0' + byte(v/10), '0' + byte(v%10)}) }