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:
Mikhail Chusavitin
2026-08-27 17:05:45 +03:00
co-authored by Claude Sonnet 5
parent 07c270cda2
commit e7b8a8badc
13 changed files with 671 additions and 48 deletions
+19 -6
View File
@@ -16,7 +16,7 @@ import (
// parserVersion - version of this parser module
// IMPORTANT: Increment this version when making changes to parser logic!
const parserVersion = "2.3"
const parserVersion = "2.5"
func init() {
parser.Register(&Parser{})
@@ -96,6 +96,7 @@ func containsInspurMarkers(content []byte) bool {
// Parse parses Inspur/Kaytus archive
func (p *Parser) Parse(files []parser.ExtractedFile) (*models.AnalysisResult, error) {
selLocation := inferInspurArchiveLocation(files)
selTimezone := inferInspurTimezoneResolver(files, selLocation)
result := &models.AnalysisResult{
Events: make([]models.Event, 0),
@@ -239,15 +240,18 @@ func (p *Parser) Parse(files []parser.ExtractedFile) (*models.AnalysisResult, er
// Parse SEL list (selelist.csv), falling back to log/sel.csv on the
// dump_<serial>_<timestamp>/ layout that has no selelist.csv.
if f := parser.FindFileByName(files, "selelist.csv"); f != nil {
selEvents := ParseSELListWithLocation(f.Content, selLocation)
selEvents := parseSELListWithResolver(f.Content, selTimezone)
result.Events = append(result.Events, selEvents...)
} else if f := parser.FindFileByName(files, "sel.csv"); f != nil {
selEvents := ParseSELListWithLocation(f.Content, selLocation)
selEvents := parseSELListWithResolver(f.Content, selTimezone)
result.Events = append(result.Events, selEvents...)
}
// Parse syslog files
syslogFiles := parser.FindFileByPattern(files, "syslog/alert", "syslog/warning", "syslog/notice", "syslog/info")
syslogFiles := parser.FindFileByPattern(files,
"syslog/emerg", "syslog/alert", "syslog/crit", "syslog/error",
"syslog/warning", "syslog/notice", "syslog/info",
)
for _, f := range syslogFiles {
events := ParseSyslog(f.Content, f.Path)
result.Events = append(result.Events, events...)
@@ -257,9 +261,11 @@ func (p *Parser) Parse(files []parser.ExtractedFile) (*models.AnalysisResult, er
// controller, switch VR/power, switch CPLD and slot presence failures.
result.Events = append(result.Events, ParseCommerCompEvents(files, selLocation)...)
// Same SEL event can be reported twice by the BMC (e.g. once via sel.csv,
// once via idl.log); collapse exact duplicates so they don't double-count.
// Same event can be reported by both IDL and SEL. After timezone
// normalization, retain the richer offset-bearing IDL event and remove the
// equivalent SEL copy, then provide a deterministic chronological stream.
result.Events = dedupSELEvents(result.Events)
sortInspurEvents(result.Events)
// Fallback for archives where board serial is missing in parsed FRU/asset data:
// recover it from log content, never from archive filename.
@@ -284,6 +290,13 @@ func (p *Parser) Parse(files []parser.ExtractedFile) (*models.AnalysisResult, er
// Mark problematic GPUs from IDL errors like "BIOS miss F_GPU6".
if result.Hardware != nil {
applyGPUStatusFromEvents(result.Hardware, result.Events)
var currentDeviceEvents []models.Event
currentDeviceSnapshotAvailable := false
if f := parser.FindFileByName(files, "dev_status.log"); f != nil {
currentDeviceSnapshotAvailable = true
currentDeviceEvents = ParseIDLLog(f.Content)
}
applyStorageStatusFromEvents(result.Hardware, result.Events, currentDeviceEvents, currentDeviceSnapshotAvailable)
enrichStorageFromSerialFallbackFiles(files, result.Hardware)
// Enrich storage serial numbers from smartd output in SOLHostCapture.log.
// Fills in serial, model, firmware for backplane slots that the BMC HDD API left empty.