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>
128 lines
3.5 KiB
Go
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)})
|
|
}
|