Files
logpile/internal/parser/vendors/inspur/timezone.go
T
Mikhail ChusavitinandClaude Sonnet 5 e7b8a8badc feat(inspur): source-PRI timezone timeline, PPIN serial, NVMe fault storage status
Event ingestion now uses the source syslog PRI and an explicit-offset
timezone timeline instead of assuming host-local time. CPU PPIN is
exported as the source-backed CPU serial, and an active NVMe fault SEL
event promotes the matching drive's storage status.

See ADL-056, ADL-057.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-27 17:05:45 +03:00

128 lines
3.5 KiB
Go

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