Files
logpile/internal/parser/vendors/inspur/sel.go
T
Mikhail ChusavitinandClaude Sonnet 5 e7b8a8badc 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>
2026-08-27 17:05:45 +03:00

294 lines
7.9 KiB
Go

package inspur
import (
"encoding/csv"
"sort"
"strings"
"time"
"unicode"
"git.mchus.pro/mchus/logpile/internal/models"
"git.mchus.pro/mchus/logpile/internal/parser"
)
// ParseSELList parses selelist.csv file with SEL events
// Format: ID, Date (MM/DD/YYYY), Time (HH:MM:SS), Sensor, Event, Status
// Example: 1,04/18/2025,09:31:18,Event Logging Disabled SEL_Status,Log area reset/cleared,Asserted
func ParseSELList(content []byte) []models.Event {
return ParseSELListWithLocation(content, parser.DefaultArchiveLocation())
}
// 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)
lines := strings.Split(text, "\n")
// Skip header line(s) if present
startIdx := 0
for i, line := range lines {
if strings.Contains(strings.ToLower(line), "sel elist") {
startIdx = i + 1
break
}
}
// Parse CSV data
for i := startIdx; i < len(lines); i++ {
line := strings.TrimSpace(lines[i])
if line == "" {
continue
}
// Parse CSV line
r := csv.NewReader(strings.NewReader(line))
records, err := r.Read()
if err != nil || len(records) < 6 {
continue
}
eventID := strings.TrimSpace(records[0])
dateStr := strings.TrimSpace(records[1])
timeStr := strings.TrimSpace(records[2])
sensorStr := strings.TrimSpace(records[3])
eventDesc := strings.TrimSpace(records[4])
status := strings.TrimSpace(records[5])
// Parse timestamp: MM/DD/YYYY HH:MM:SS
timestamp := parseSELTimestampWithResolver(dateStr, timeStr, locationFor)
// Extract sensor type and name
sensorType, sensorName := parseSensorInfo(sensorStr)
// Determine severity
severity := determineSELSeverity(sensorStr, eventDesc, status)
// Build full description
description := buildSELDescription(eventDesc, status)
events = append(events, models.Event{
ID: eventID,
Timestamp: timestamp,
Source: "SEL",
SensorType: sensorType,
SensorName: sensorName,
EventType: eventDesc,
Severity: severity,
Description: description,
RawData: line,
})
}
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 {
return parseSELTimestampWithResolver(dateStr, timeStr, func(time.Time) *time.Location { return location })
}
// parseSensorInfo extracts sensor type and name from sensor string
// Example: "Event Logging Disabled SEL_Status" -> ("sel", "SEL_Status")
// Example: "Power Supply PSU0_Status" -> ("power_supply", "PSU0_Status")
func parseSensorInfo(sensorStr string) (sensorType, sensorName string) {
parts := strings.Fields(sensorStr)
if len(parts) == 0 {
return "unknown", sensorStr
}
// Last part is usually the sensor name
sensorName = parts[len(parts)-1]
// First parts form the sensor type
if len(parts) > 1 {
sensorType = strings.ToLower(strings.Join(parts[:len(parts)-1], "_"))
} else {
sensorType = "system"
}
return
}
// determineSELSeverity determines event severity based on sensor and event description
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{
"critical", "failure", "fault", "error",
"ac lost", "predictive failure", "redundancy lost",
"going high", "going low", "transition to critical",
}
for _, keyword := range criticalKeywords {
if strings.Contains(lowerSensor, keyword) ||
strings.Contains(lowerEvent, keyword) ||
strings.Contains(lowerStatus, keyword) {
return models.SeverityCritical
}
}
// Warning indicators
warningKeywords := []string{
"warning", "disabled", "non-recoverable",
"device removed", "device absent",
}
for _, keyword := range warningKeywords {
if strings.Contains(lowerSensor, keyword) ||
strings.Contains(lowerEvent, keyword) ||
strings.Contains(lowerStatus, keyword) {
return models.SeverityWarning
}
}
// Info indicators (normal operations)
infoKeywords := []string{
"presence detected", "device present", "asserted",
"initiated by", "state asserted", "s0/g0: working",
"power button pressed",
}
for _, keyword := range infoKeywords {
if strings.Contains(lowerEvent, keyword) ||
strings.Contains(lowerStatus, keyword) {
return models.SeverityInfo
}
}
// Default to info
return models.SeverityInfo
}
// 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
}
out := make([]models.Event, 0, len(events))
bySecond := make(map[int64][]int)
for _, e := range events {
if e.Timestamp.IsZero() {
out = append(out, e)
continue
}
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
}
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" {
return eventDesc
}
return eventDesc + " (" + status + ")"
}