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
+50 -6
View File
@@ -3,6 +3,7 @@ package inspur
import (
"bufio"
"regexp"
"strconv"
"strings"
"time"
@@ -11,16 +12,14 @@ import (
var (
// Syslog format: <priority> timestamp hostname process: message
syslogRegex = regexp.MustCompile(`^<(\d+)>\s*(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}[^\s]*)\s+(\S+)\s+(\S+):\s*(.*)$`)
syslogRegex = regexp.MustCompile(`^<(\d+)>\s*(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}[^\s]*)\s+(\S+)\s+(\S+):\s*(.*)$`)
bootRelativeMessageRegex = regexp.MustCompile(`^\[\s*\d+(?:\.\d+)?\]`)
)
// ParseSyslog parses syslog format logs
func ParseSyslog(content []byte, sourcePath string) []models.Event {
var events []models.Event
// Determine severity from file path
severity := determineSeverityFromPath(sourcePath)
scanner := bufio.NewScanner(strings.NewReader(string(content)))
lineNum := 0
@@ -35,6 +34,10 @@ func ParseSyslog(content []byte, sourcePath string) []models.Event {
if matches == nil {
continue
}
priority, err := strconv.Atoi(matches[1])
if err != nil {
continue
}
timestamp, err := time.Parse(time.RFC3339, matches[2])
if err != nil {
@@ -44,6 +47,14 @@ func ParseSyslog(content []byte, sourcePath string) []models.Event {
continue
}
}
message := strings.TrimSpace(matches[5])
// Some AMI BMC boots emit a fabricated 1970 wall-clock timestamp while
// the payload only carries seconds since boot. Without a trustworthy
// boot epoch this cannot become a wall-clock event, so omit it instead
// of exporting either 1970 or the archive collection time.
if timestamp.Year() <= 1971 && bootRelativeMessageRegex.MatchString(message) {
continue
}
event := models.Event{
ID: generateEventID(sourcePath, lineNum),
@@ -51,8 +62,8 @@ func ParseSyslog(content []byte, sourcePath string) []models.Event {
Source: "syslog",
SensorType: "syslog",
SensorName: matches[4],
Description: matches[5],
Severity: severity,
Description: message,
Severity: determineSyslogSeverity(priority, message, sourcePath),
RawData: line,
}
@@ -62,6 +73,39 @@ func ParseSyslog(content []byte, sourcePath string) []models.Event {
return events
}
func determineSyslogSeverity(priority int, message, sourcePath string) models.Severity {
// These AMI driver start-up/status strings are routed to alert.log and
// warning.log despite not describing a fault. The PRI value is therefore
// not trustworthy for this small, observed set of benign messages.
lowerMessage := strings.ToLower(message)
benignMessages := []string{
"helper module driver version",
"copyright (c)",
"color depth is 15 bpp or higher",
"new driver 0 directmode 1",
}
for _, benign := range benignMessages {
if strings.Contains(lowerMessage, benign) {
return models.SeverityInfo
}
}
// RFC 5424 severity is stored in the low three bits of PRI:
// 0..2 emergency/alert/critical, 3..4 error/warning, 5..7 notice/info/debug.
if priority >= 0 {
switch priority & 7 {
case 0, 1, 2:
return models.SeverityCritical
case 3, 4:
return models.SeverityWarning
default:
return models.SeverityInfo
}
}
return determineSeverityFromPath(sourcePath)
}
func determineSeverityFromPath(path string) models.Severity {
pathLower := strings.ToLower(path)