feat(parser): lenovo xcc warnings and redfish logs - v1.1

This commit is contained in:
2026-04-13 20:34:04 +03:00
parent 89b6701f43
commit c9969fc3da
8 changed files with 283 additions and 26 deletions
+56 -3
View File
@@ -50,11 +50,15 @@ func (c *RedfishConnector) collectRedfishLogEntries(ctx context.Context, client
}
for _, systemPath := range systemPaths {
collectFrom(joinPath(systemPath, "/LogServices"), isHardwareLogService)
for _, logServicesPath := range c.redfishLinkedCollectionPaths(ctx, client, req, baseURL, systemPath, "LogServices") {
collectFrom(logServicesPath, isHardwareLogService)
}
}
// Managers hold the IPMI SEL on AMI/MSI BMCs — include only the "SEL" service.
for _, managerPath := range managerPaths {
collectFrom(joinPath(managerPath, "/LogServices"), isManagerSELService)
for _, logServicesPath := range c.redfishLinkedCollectionPaths(ctx, client, req, baseURL, managerPath, "LogServices") {
collectFrom(logServicesPath, isManagerSELService)
}
}
if len(out) > 0 {
@@ -63,6 +67,42 @@ func (c *RedfishConnector) collectRedfishLogEntries(ctx context.Context, client
return out
}
func (c *RedfishConnector) redfishLinkedCollectionPaths(
ctx context.Context,
client *http.Client,
req Request,
baseURL, resourcePath, linkKey string,
) []string {
resourcePath = normalizeRedfishPath(resourcePath)
if resourcePath == "" || strings.TrimSpace(linkKey) == "" {
return nil
}
seen := make(map[string]struct{}, 2)
var out []string
add := func(path string) {
path = normalizeRedfishPath(path)
if path == "" {
return
}
if _, ok := seen[path]; ok {
return
}
seen[path] = struct{}{}
out = append(out, path)
}
add(joinPath(resourcePath, "/"+strings.TrimSpace(linkKey)))
resourceDoc, err := c.getJSON(ctx, client, req, baseURL, resourcePath)
if err == nil {
if linked := redfishLinkedPath(resourceDoc, linkKey); linked != "" {
add(linked)
}
}
return out
}
// fetchRedfishLogEntriesWithPaging fetches entries from a LogEntry collection,
// following nextLink pages. Stops early when entries older than cutoff are encountered
// (assumes BMC returns entries newest-first, which is typical).
@@ -182,7 +222,7 @@ func redfishLogServiceEntriesPath(svc map[string]interface{}) string {
// Audit, authentication, and session events are excluded.
func isHardwareLogEntry(entry map[string]interface{}) bool {
entryType := strings.TrimSpace(asString(entry["EntryType"]))
if strings.EqualFold(entryType, "Oem") {
if strings.EqualFold(entryType, "Oem") && !strings.EqualFold(strings.TrimSpace(asString(entry["OemRecordFormat"])), "Lenovo") {
return false
}
@@ -362,6 +402,9 @@ func parseIPMIDumpKV(message string) map[string]string {
// AMI/MSI BMCs often set Severity="OK" on all SEL records regardless of content,
// so we fall back to inferring severity from SensorType when the explicit field is unhelpful.
func redfishLogEntrySeverity(entry map[string]interface{}) models.Severity {
if redfishLogEntryLooksLikeWarning(entry) {
return models.SeverityWarning
}
// Newer Redfish uses MessageSeverity; older uses Severity.
raw := strings.ToLower(firstNonEmpty(
strings.TrimSpace(asString(entry["MessageSeverity"])),
@@ -380,6 +423,16 @@ func redfishLogEntrySeverity(entry map[string]interface{}) models.Severity {
}
}
func redfishLogEntryLooksLikeWarning(entry map[string]interface{}) bool {
joined := strings.ToLower(strings.TrimSpace(strings.Join([]string{
asString(entry["Message"]),
asString(entry["Name"]),
asString(entry["SensorType"]),
asString(entry["EntryCode"]),
}, " ")))
return strings.Contains(joined, "unqualified dimm")
}
// redfishSeverityFromSensorType infers event severity from the IPMI/Redfish SensorType string.
func redfishSeverityFromSensorType(sensorType string) models.Severity {
switch strings.ToLower(sensorType) {