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>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
07c270cda2
commit
e7b8a8badc
+127
@@ -0,0 +1,127 @@
|
||||
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)})
|
||||
}
|
||||
Reference in New Issue
Block a user