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
+108
-26
@@ -2,8 +2,10 @@ package inspur
|
||||
|
||||
import (
|
||||
"encoding/csv"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode"
|
||||
|
||||
"git.mchus.pro/mchus/logpile/internal/models"
|
||||
"git.mchus.pro/mchus/logpile/internal/parser"
|
||||
@@ -19,6 +21,14 @@ func ParseSELList(content []byte) []models.Event {
|
||||
// ParseSELListWithLocation parses selelist.csv using provided source timezone
|
||||
// for timestamps that don't contain an explicit offset.
|
||||
func ParseSELListWithLocation(content []byte, location *time.Location) []models.Event {
|
||||
return parseSELList(content, func(local time.Time) *time.Location { return location })
|
||||
}
|
||||
|
||||
func parseSELListWithResolver(content []byte, resolver *inspurTimezoneResolver) []models.Event {
|
||||
return parseSELList(content, resolver.locationFor)
|
||||
}
|
||||
|
||||
func parseSELList(content []byte, locationFor func(time.Time) *time.Location) []models.Event {
|
||||
var events []models.Event
|
||||
|
||||
text := string(content)
|
||||
@@ -55,7 +65,7 @@ func ParseSELListWithLocation(content []byte, location *time.Location) []models.
|
||||
status := strings.TrimSpace(records[5])
|
||||
|
||||
// Parse timestamp: MM/DD/YYYY HH:MM:SS
|
||||
timestamp := parseSELTimestamp(dateStr, timeStr, location)
|
||||
timestamp := parseSELTimestampWithResolver(dateStr, timeStr, locationFor)
|
||||
|
||||
// Extract sensor type and name
|
||||
sensorType, sensorName := parseSensorInfo(sensorStr)
|
||||
@@ -82,23 +92,24 @@ func ParseSELListWithLocation(content []byte, location *time.Location) []models.
|
||||
return events
|
||||
}
|
||||
|
||||
func parseSELTimestampWithResolver(dateStr, timeStr string, locationFor func(time.Time) *time.Location) time.Time {
|
||||
timestampStr := dateStr + " " + timeStr
|
||||
local, err := time.ParseInLocation("01/02/2006 15:04:05", timestampStr, time.UTC)
|
||||
if err != nil {
|
||||
return time.Time{}
|
||||
}
|
||||
location := parser.DefaultArchiveLocation()
|
||||
if locationFor != nil {
|
||||
if resolved := locationFor(local); resolved != nil {
|
||||
location = resolved
|
||||
}
|
||||
}
|
||||
return time.Date(local.Year(), local.Month(), local.Day(), local.Hour(), local.Minute(), local.Second(), 0, location)
|
||||
}
|
||||
|
||||
// parseSELTimestamp parses MM/DD/YYYY and HH:MM:SS into time.Time
|
||||
func parseSELTimestamp(dateStr, timeStr string, location *time.Location) time.Time {
|
||||
// Combine date and time: MM/DD/YYYY HH:MM:SS
|
||||
timestampStr := dateStr + " " + timeStr
|
||||
|
||||
if location == nil {
|
||||
location = parser.DefaultArchiveLocation()
|
||||
}
|
||||
|
||||
// Try parsing with MM/DD/YYYY format
|
||||
t, err := time.ParseInLocation("01/02/2006 15:04:05", timestampStr, location)
|
||||
if err != nil {
|
||||
// Fallback to current time
|
||||
return time.Now()
|
||||
}
|
||||
|
||||
return t
|
||||
return parseSELTimestampWithResolver(dateStr, timeStr, func(time.Time) *time.Location { return location })
|
||||
}
|
||||
|
||||
// parseSensorInfo extracts sensor type and name from sensor string
|
||||
@@ -128,6 +139,9 @@ func determineSELSeverity(sensorStr, eventDesc, status string) models.Severity {
|
||||
lowerSensor := strings.ToLower(sensorStr)
|
||||
lowerEvent := strings.ToLower(eventDesc)
|
||||
lowerStatus := strings.ToLower(status)
|
||||
if strings.Contains(lowerStatus, "deassert") {
|
||||
return models.SeverityInfo
|
||||
}
|
||||
|
||||
// Critical indicators
|
||||
criticalKeywords := []string{
|
||||
@@ -176,32 +190,100 @@ func determineSELSeverity(sensorStr, eventDesc, status string) models.Severity {
|
||||
return models.SeverityInfo
|
||||
}
|
||||
|
||||
// dedupSELEvents collapses events reported more than once with the same
|
||||
// (timestamp, event_type, description) triple. Unlike ParseIDLLog's
|
||||
// dedup (which must keep recurring alarms with distinct timestamps), this
|
||||
// only removes true duplicates: the same SEL entry surfacing through more
|
||||
// than one source file for the exact same moment.
|
||||
// dedupSELEvents removes exact SEL repeats and equivalent IDL/SEL copies at
|
||||
// the same normalized instant. IDL is preferred because it carries an
|
||||
// explicit timezone offset and usually a richer component-prefixed message.
|
||||
func dedupSELEvents(events []models.Event) []models.Event {
|
||||
if len(events) == 0 {
|
||||
return events
|
||||
}
|
||||
seen := make(map[string]struct{}, len(events))
|
||||
out := make([]models.Event, 0, len(events))
|
||||
bySecond := make(map[int64][]int)
|
||||
for _, e := range events {
|
||||
if e.Source != "SEL" {
|
||||
if e.Timestamp.IsZero() {
|
||||
out = append(out, e)
|
||||
continue
|
||||
}
|
||||
key := e.Timestamp.String() + "|" + e.EventType + "|" + e.Description
|
||||
if _, ok := seen[key]; ok {
|
||||
|
||||
second := e.Timestamp.Unix()
|
||||
duplicate := false
|
||||
for _, idx := range bySecond[second] {
|
||||
previous := out[idx]
|
||||
if !equivalentInspurEvent(previous, e) {
|
||||
continue
|
||||
}
|
||||
if previous.Source == "SEL" && e.Source != "SEL" {
|
||||
out[idx] = e
|
||||
}
|
||||
duplicate = true
|
||||
break
|
||||
}
|
||||
if duplicate {
|
||||
continue
|
||||
}
|
||||
seen[key] = struct{}{}
|
||||
bySecond[second] = append(bySecond[second], len(out))
|
||||
out = append(out, e)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func equivalentInspurEvent(a, b models.Event) bool {
|
||||
if a.Source != "SEL" && b.Source != "SEL" {
|
||||
return false
|
||||
}
|
||||
if a.Severity != b.Severity || eventDirection(a) != eventDirection(b) {
|
||||
return false
|
||||
}
|
||||
left := normalizeEventDescription(a.Description)
|
||||
right := normalizeEventDescription(b.Description)
|
||||
if left == "" || right == "" {
|
||||
return false
|
||||
}
|
||||
if left == right {
|
||||
return true
|
||||
}
|
||||
shorter, longer := left, right
|
||||
if len(shorter) > len(longer) {
|
||||
shorter, longer = longer, shorter
|
||||
}
|
||||
return len(shorter) >= 12 && strings.Contains(longer, shorter)
|
||||
}
|
||||
|
||||
func eventDirection(e models.Event) string {
|
||||
value := strings.ToLower(e.EventType + " " + e.RawData)
|
||||
if strings.Contains(value, "deassert") {
|
||||
return "deassert"
|
||||
}
|
||||
if strings.Contains(value, "assert") {
|
||||
return "assert"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func normalizeEventDescription(value string) string {
|
||||
value = strings.ToLower(value)
|
||||
value = strings.Map(func(r rune) rune {
|
||||
if unicode.IsLetter(r) || unicode.IsDigit(r) {
|
||||
return r
|
||||
}
|
||||
return ' '
|
||||
}, value)
|
||||
return strings.Join(strings.Fields(value), " ")
|
||||
}
|
||||
|
||||
func sortInspurEvents(events []models.Event) {
|
||||
sort.SliceStable(events, func(i, j int) bool {
|
||||
left, right := events[i].Timestamp, events[j].Timestamp
|
||||
if left.IsZero() {
|
||||
return false
|
||||
}
|
||||
if right.IsZero() {
|
||||
return true
|
||||
}
|
||||
return left.Before(right)
|
||||
})
|
||||
}
|
||||
|
||||
// buildSELDescription builds human-readable description
|
||||
func buildSELDescription(eventDesc, status string) string {
|
||||
if status == "Asserted" || status == "Deasserted" {
|
||||
|
||||
Reference in New Issue
Block a user