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 + ")" }