package collector import ( "bee/audit/internal/schema" "os" "path/filepath" "sort" "strconv" "strings" ) var edacBaseDir = "/sys/devices/system/edac/mc" type edacDIMMStats struct { Label string CECount *int64 UECount *int64 } func enrichMemoryWithTelemetry(dimms []schema.HardwareMemory, doc sensorsDoc) []schema.HardwareMemory { if len(dimms) == 0 { return dimms } tempByLabel := memoryTempsFromSensors(doc) stats := readEDACStats() for i := range dimms { labelKeys := dimmMatchKeys(dimms[i].Slot, dimms[i].Location) for _, key := range labelKeys { if temp, ok := tempByLabel[key]; ok { dimms[i].TemperatureC = &temp break } } for _, key := range labelKeys { if stat, ok := stats[key]; ok { if stat.CECount != nil { dimms[i].CorrectableECCErrorCount = stat.CECount } if stat.UECount != nil { dimms[i].UncorrectableECCErrorCount = stat.UECount } if stat.UECount != nil && *stat.UECount > 0 { dimms[i].DataLossDetected = boolPtr(true) status := statusCritical dimms[i].Status = &status if dimms[i].ErrorDescription == nil { dimms[i].ErrorDescription = stringPtr("EDAC reports uncorrectable ECC errors") } } else if stat.CECount != nil && *stat.CECount > 0 && (dimms[i].Status == nil || *dimms[i].Status == statusOK) { status := statusWarning dimms[i].Status = &status if dimms[i].ErrorDescription == nil { dimms[i].ErrorDescription = stringPtr("EDAC reports correctable ECC errors") } } break } } } return dimms } func memoryTempsFromSensors(doc sensorsDoc) map[string]float64 { out := map[string]float64{} if len(doc) == 0 { return out } for chip, features := range doc { for featureName, raw := range features { feature, ok := raw.(map[string]any) if !ok || classifySensorFeature(feature) != "temp" { continue } if !isLikelyMemoryTemp(chip, featureName) { continue } temp, ok := firstFeatureFloat(feature, "_input") if !ok { continue } key := canonicalLabel(featureName) if key == "" { continue } if _, exists := out[key]; !exists { out[key] = temp } } } return out } func readEDACStats() map[string]edacDIMMStats { out := map[string]edacDIMMStats{} mcDirs, err := filepath.Glob(filepath.Join(edacBaseDir, "mc*")) if err != nil { return out } sort.Strings(mcDirs) for _, mcDir := range mcDirs { dimmDirs, err := filepath.Glob(filepath.Join(mcDir, "dimm*")) if err != nil { continue } sort.Strings(dimmDirs) for _, dimmDir := range dimmDirs { stat, ok := readEDACDIMMStats(dimmDir) if !ok { continue } key := canonicalLabel(stat.Label) if key == "" { continue } out[key] = stat } } return out } func readEDACDIMMStats(dimmDir string) (edacDIMMStats, bool) { labelBytes, err := os.ReadFile(filepath.Join(dimmDir, "dimm_label")) if err != nil { labelBytes, err = os.ReadFile(filepath.Join(dimmDir, "label")) if err != nil { return edacDIMMStats{}, false } } label := strings.TrimSpace(string(labelBytes)) if label == "" { return edacDIMMStats{}, false } stat := edacDIMMStats{Label: label} if value, ok := readEDACCount(dimmDir, []string{"dimm_ce_count", "ce_count"}); ok { stat.CECount = &value } if value, ok := readEDACCount(dimmDir, []string{"dimm_ue_count", "ue_count"}); ok { stat.UECount = &value } return stat, true } func readEDACCount(dir string, names []string) (int64, bool) { for _, name := range names { raw, err := os.ReadFile(filepath.Join(dir, name)) if err != nil { continue } value, err := strconv.ParseInt(strings.TrimSpace(string(raw)), 10, 64) if err == nil && value >= 0 { return value, true } } return 0, false } func dimmMatchKeys(slot, location *string) []string { var out []string add := func(value *string) { key := canonicalLabel(derefString(value)) if key == "" { return } for _, existing := range out { if existing == key { return } } out = append(out, key) } add(slot) add(location) return out } func canonicalLabel(value string) string { value = strings.ToUpper(strings.TrimSpace(value)) if value == "" { return "" } var b strings.Builder for _, r := range value { if (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') { b.WriteRune(r) } } return b.String() } func isLikelyMemoryTemp(chip, feature string) bool { value := strings.ToLower(chip + " " + feature) return strings.Contains(value, "dimm") || strings.Contains(value, "sodimm") } func boolPtr(value bool) *bool { return &value }