Add LOGPile BMC diagnostic log analyzer

Features:
- Modular parser architecture for vendor-specific formats
- Inspur/Kaytus parser supporting asset.json, devicefrusdr.log,
  component.log, idl.log, and syslog files
- PCI Vendor/Device ID lookup for hardware identification
- Web interface with tabs: Events, Sensors, Config, Serials, Firmware
- Server specification summary with component grouping
- Export to CSV, JSON, TXT formats
- BMC alarm parsing from IDL logs (memory errors, PSU events, etc.)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-25 04:11:23 +03:00
parent fb800216f1
commit 512957545a
29 changed files with 4086 additions and 1 deletions

352
internal/parser/vendors/inspur/asset.go vendored Normal file
View File

@@ -0,0 +1,352 @@
package inspur
import (
"encoding/json"
"fmt"
"strings"
"git.mchus.pro/mchus/logpile/internal/models"
"git.mchus.pro/mchus/logpile/internal/parser/vendors/pciids"
)
// AssetJSON represents the structure of Inspur asset.json file
type AssetJSON struct {
VersionInfo []struct {
DeviceID int `json:"DeviceId"`
DeviceName string `json:"DeviceName"`
DeviceRevision string `json:"DeviceRevision"`
BuildTime string `json:"BuildTime"`
} `json:"VersionInfo"`
CpuInfo []struct {
ProcessorName string `json:"ProcessorName"`
ProcessorID string `json:"ProcessorId"`
MicroCodeVer string `json:"MicroCodeVer"`
CurrentSpeed int `json:"CurrentSpeed"`
Core int `json:"Core"`
ThreadCount int `json:"ThreadCount"`
L1Cache int `json:"L1Cache"`
L2Cache int `json:"L2Cache"`
L3Cache int `json:"L3Cache"`
CpuTdp int `json:"CpuTdp"`
PPIN string `json:"PPIN"`
TurboEnableMaxSpeed int `json:"TurboEnableMaxSpeed"`
TurboCloseMaxSpeed int `json:"TurboCloseMaxSpeed"`
UPIBandwidth string `json:"UPIBandwidth"`
} `json:"CpuInfo"`
MemInfo struct {
MemCommonInfo []struct {
Manufacturer string `json:"Manufacturer"`
MaxSpeed int `json:"MaxSpeed"`
CurrentSpeed int `json:"CurrentSpeed"`
MemoryType int `json:"MemoryType"`
Rank int `json:"Rank"`
DataWidth int `json:"DataWidth"`
ConfiguredVoltage int `json:"ConfiguredVoltage"`
PhysicalSize int `json:"PhysicalSize"`
} `json:"MemCommonInfo"`
DimmInfo []struct {
SerialNumber string `json:"SerialNumber"`
PartNumber string `json:"PartNumber"`
AssetTag string `json:"AssetTag"`
} `json:"DimmInfo"`
} `json:"MemInfo"`
HddInfo []struct {
SerialNumber string `json:"SerialNumber"`
Manufacturer string `json:"Manufacturer"`
ModelName string `json:"ModelName"`
FirmwareVersion string `json:"FirmwareVersion"`
Capacity int `json:"Capacity"`
Location int `json:"Location"`
DiskInterfaceType int `json:"DiskInterfaceType"`
MediaType int `json:"MediaType"`
LocationString string `json:"LocationString"`
BlockSizeBytes int `json:"BlockSizeBytes"`
CapableSpeedGbs string `json:"CapableSpeedGbs"`
NegotiatedSpeedGbs string `json:"NegotiatedSpeedGbs"`
PcieSlot int `json:"PcieSlot"`
} `json:"HddInfo"`
PcieInfo []struct {
VendorId int `json:"VendorId"`
DeviceId int `json:"DeviceId"`
BusNumber int `json:"BusNumber"`
DeviceNumber int `json:"DeviceNumber"`
FunctionNumber int `json:"FunctionNumber"`
MaxLinkWidth int `json:"MaxLinkWidth"`
MaxLinkSpeed int `json:"MaxLinkSpeed"`
NegotiatedLinkWidth int `json:"NegotiatedLinkWidth"`
CurrentLinkSpeed int `json:"CurrentLinkSpeed"`
ClassCode int `json:"ClassCode"`
SubClassCode int `json:"SubClassCode"`
PcieSlot int `json:"PcieSlot"`
LocString string `json:"LocString"`
PartNumber *string `json:"PartNumber"`
SerialNumber *string `json:"SerialNumber"`
Mac []string `json:"Mac"`
} `json:"PcieInfo"`
}
// ParseAssetJSON parses Inspur asset.json content
func ParseAssetJSON(content []byte) (*models.HardwareConfig, error) {
var asset AssetJSON
if err := json.Unmarshal(content, &asset); err != nil {
return nil, err
}
config := &models.HardwareConfig{}
// Parse version info
for _, v := range asset.VersionInfo {
config.Firmware = append(config.Firmware, models.FirmwareInfo{
DeviceName: v.DeviceName,
Version: v.DeviceRevision,
BuildTime: v.BuildTime,
})
}
// Parse CPU info
for i, cpu := range asset.CpuInfo {
config.CPUs = append(config.CPUs, models.CPU{
Socket: i,
Model: strings.TrimSpace(cpu.ProcessorName),
Cores: cpu.Core,
Threads: cpu.ThreadCount,
FrequencyMHz: cpu.CurrentSpeed,
MaxFreqMHz: cpu.TurboEnableMaxSpeed,
L1CacheKB: cpu.L1Cache,
L2CacheKB: cpu.L2Cache,
L3CacheKB: cpu.L3Cache,
TDP: cpu.CpuTdp,
PPIN: cpu.PPIN,
})
}
// Parse memory info
if len(asset.MemInfo.MemCommonInfo) > 0 {
common := asset.MemInfo.MemCommonInfo[0]
for i, dimm := range asset.MemInfo.DimmInfo {
config.Memory = append(config.Memory, models.MemoryDIMM{
Slot: i,
SizeMB: common.PhysicalSize * 1024,
Type: memoryTypeToString(common.MemoryType),
SpeedMHz: common.CurrentSpeed,
Manufacturer: common.Manufacturer,
SerialNumber: dimm.SerialNumber,
PartNumber: strings.TrimSpace(dimm.PartNumber),
})
}
}
// Parse storage info
for _, hdd := range asset.HddInfo {
storageType := "HDD"
if hdd.DiskInterfaceType == 5 {
storageType = "NVMe"
} else if hdd.MediaType == 1 {
storageType = "SSD"
}
// Resolve manufacturer: try vendor ID first, then model name extraction
modelName := strings.TrimSpace(hdd.ModelName)
manufacturer := resolveManufacturer(hdd.Manufacturer, modelName)
config.Storage = append(config.Storage, models.Storage{
Slot: hdd.LocationString,
Type: storageType,
Model: modelName,
SizeGB: hdd.Capacity,
SerialNumber: hdd.SerialNumber,
Manufacturer: manufacturer,
Firmware: hdd.FirmwareVersion,
Interface: diskInterfaceToString(hdd.DiskInterfaceType),
})
}
// Parse PCIe info
for _, pcie := range asset.PcieInfo {
vendor, deviceName := pciids.DeviceInfo(pcie.VendorId, pcie.DeviceId)
device := models.PCIeDevice{
Slot: pcie.LocString,
VendorID: pcie.VendorId,
DeviceID: pcie.DeviceId,
BDF: formatBDF(pcie.BusNumber, pcie.DeviceNumber, pcie.FunctionNumber),
LinkWidth: pcie.NegotiatedLinkWidth,
LinkSpeed: pcieLinkSpeedToString(pcie.CurrentLinkSpeed),
MaxLinkWidth: pcie.MaxLinkWidth,
MaxLinkSpeed: pcieLinkSpeedToString(pcie.MaxLinkSpeed),
DeviceClass: pcieClassToString(pcie.ClassCode, pcie.SubClassCode),
Manufacturer: vendor,
}
if pcie.PartNumber != nil {
device.PartNumber = strings.TrimSpace(*pcie.PartNumber)
}
if pcie.SerialNumber != nil {
device.SerialNumber = strings.TrimSpace(*pcie.SerialNumber)
}
if len(pcie.Mac) > 0 {
device.MACAddresses = pcie.Mac
}
// Use device name from PCI IDs database if available
if deviceName != "" {
device.DeviceClass = deviceName
}
config.PCIeDevices = append(config.PCIeDevices, device)
}
return config, nil
}
func memoryTypeToString(memType int) string {
switch memType {
case 26:
return "DDR4"
case 34:
return "DDR5"
default:
return "Unknown"
}
}
func diskInterfaceToString(ifType int) string {
switch ifType {
case 4:
return "SATA"
case 5:
return "NVMe"
case 6:
return "SAS"
default:
return "Unknown"
}
}
func pcieLinkSpeedToString(speed int) string {
switch speed {
case 1:
return "2.5 GT/s"
case 2:
return "5.0 GT/s"
case 3:
return "8.0 GT/s"
case 4:
return "16.0 GT/s"
case 5:
return "32.0 GT/s"
default:
return "Unknown"
}
}
func pcieClassToString(classCode, subClass int) string {
switch classCode {
case 1:
switch subClass {
case 0:
return "SCSI"
case 1:
return "IDE"
case 4:
return "RAID"
case 6:
return "SATA"
case 7:
return "SAS"
case 8:
return "NVMe"
default:
return "Storage"
}
case 2:
return "Network"
case 3:
switch subClass {
case 0:
return "VGA"
case 2:
return "3D Controller"
default:
return "Display"
}
case 4:
return "Multimedia"
case 6:
return "Bridge"
case 12:
return "Serial Bus"
default:
return "Other"
}
}
func formatBDF(bus, dev, fun int) string {
return fmt.Sprintf("%02x:%02x.%x", bus, dev, fun)
}
// resolveManufacturer resolves manufacturer name from various sources
func resolveManufacturer(rawManufacturer, modelName string) string {
raw := strings.TrimSpace(rawManufacturer)
// If it looks like a vendor ID (hex), try to resolve it
if raw != "" {
if name := pciids.VendorNameFromString(raw); name != "" {
return name
}
// If not a vendor ID but looks like a real name (has letters), use it
hasLetter := false
for _, c := range raw {
if (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') {
hasLetter = true
break
}
}
if hasLetter && len(raw) > 2 {
return raw
}
}
// Try to extract from model name
return extractStorageManufacturer(modelName)
}
// extractStorageManufacturer tries to extract manufacturer from model name
func extractStorageManufacturer(model string) string {
modelUpper := strings.ToUpper(model)
knownVendors := []struct {
prefix string
name string
}{
{"SAMSUNG", "Samsung"},
{"KIOXIA", "KIOXIA"},
{"TOSHIBA", "Toshiba"},
{"WDC", "Western Digital"},
{"WD", "Western Digital"},
{"SEAGATE", "Seagate"},
{"HGST", "HGST"},
{"INTEL", "Intel"},
{"MICRON", "Micron"},
{"KINGSTON", "Kingston"},
{"CRUCIAL", "Crucial"},
{"SK HYNIX", "SK Hynix"},
{"SKHYNIX", "SK Hynix"},
{"SANDISK", "SanDisk"},
{"LITEON", "Lite-On"},
{"PLEXTOR", "Plextor"},
{"ADATA", "ADATA"},
{"TRANSCEND", "Transcend"},
{"CORSAIR", "Corsair"},
{"SOLIDIGM", "Solidigm"},
}
for _, v := range knownVendors {
if strings.HasPrefix(modelUpper, v.prefix) {
return v.name
}
}
return ""
}

View File

@@ -0,0 +1,147 @@
package inspur
import (
"encoding/json"
"fmt"
"regexp"
"strings"
"time"
"git.mchus.pro/mchus/logpile/internal/models"
)
// ParseComponentLog parses component.log file and extracts PSU and other info
func ParseComponentLog(content []byte, hw *models.HardwareConfig) {
if hw == nil {
return
}
text := string(content)
// Parse RESTful PSU info
parsePSUInfo(text, hw)
}
// ParseComponentLogEvents extracts events from component.log (memory errors, etc.)
func ParseComponentLogEvents(content []byte) []models.Event {
var events []models.Event
text := string(content)
// Parse RESTful Memory info for Warning/Error status
memEvents := parseMemoryEvents(text)
events = append(events, memEvents...)
return events
}
// PSUInfo represents the RESTful PSU info structure
type PSUInfo struct {
PowerSupplies []struct {
ID int `json:"id"`
Present int `json:"present"`
VendorID string `json:"vendor_id"`
Model string `json:"model"`
SerialNum string `json:"serial_num"`
FwVer string `json:"fw_ver"`
RatedPower int `json:"rated_power"`
Status string `json:"status"`
} `json:"power_supplies"`
}
func parsePSUInfo(text string, hw *models.HardwareConfig) {
// Find RESTful PSU info section
re := regexp.MustCompile(`RESTful PSU info:\s*(\{[\s\S]*?\})\s*(?:RESTful|BMC|$)`)
match := re.FindStringSubmatch(text)
if match == nil {
return
}
jsonStr := match[1]
// Clean up the JSON (it might have newlines)
jsonStr = strings.ReplaceAll(jsonStr, "\n", "")
var psuInfo PSUInfo
if err := json.Unmarshal([]byte(jsonStr), &psuInfo); err != nil {
return
}
// Clear existing PSU data and populate with RESTful data
hw.PowerSupply = nil
for _, psu := range psuInfo.PowerSupplies {
if psu.Present != 1 {
continue
}
hw.PowerSupply = append(hw.PowerSupply, models.PSU{
Slot: formatPSUSlot(psu.ID),
Model: psu.Model,
WattageW: psu.RatedPower,
SerialNumber: psu.SerialNum,
Status: psu.Status,
})
}
}
func formatPSUSlot(id int) string {
return fmt.Sprintf("PSU%d", id)
}
// MemoryInfo represents the RESTful Memory info structure
type MemoryInfo struct {
MemModules []struct {
MemModID int `json:"mem_mod_id"`
MemModSlot string `json:"mem_mod_slot"`
MemModSize int `json:"mem_mod_size"`
MemModVendor string `json:"mem_mod_vendor"`
MemModPartNum string `json:"mem_mod_part_num"`
MemModSerial string `json:"mem_mod_serial_num"`
Status string `json:"status"`
} `json:"mem_modules"`
}
func parseMemoryEvents(text string) []models.Event {
var events []models.Event
// Find RESTful Memory info section
re := regexp.MustCompile(`RESTful Memory info:\s*(\{[\s\S]*?\})\s*RESTful HDD`)
match := re.FindStringSubmatch(text)
if match == nil {
return events
}
jsonStr := match[1]
jsonStr = strings.ReplaceAll(jsonStr, "\n", "")
var memInfo MemoryInfo
if err := json.Unmarshal([]byte(jsonStr), &memInfo); err != nil {
return events
}
// Generate events for memory modules with Warning or Error status
for _, mem := range memInfo.MemModules {
if mem.Status == "Warning" || mem.Status == "Error" || mem.Status == "Critical" {
severity := models.SeverityWarning
if mem.Status == "Error" || mem.Status == "Critical" {
severity = models.SeverityCritical
}
description := fmt.Sprintf("Memory module %s: %s", mem.MemModSlot, mem.Status)
if mem.MemModSize == 0 {
description = fmt.Sprintf("Memory module %s not detected (capacity 0GB)", mem.MemModSlot)
}
events = append(events, models.Event{
ID: fmt.Sprintf("mem_%d", mem.MemModID),
Timestamp: time.Now(), // No timestamp in source, use current
Source: "Memory",
SensorType: "memory",
SensorName: mem.MemModSlot,
EventType: "Memory Status",
Severity: severity,
Description: description,
RawData: fmt.Sprintf("Slot: %s, Vendor: %s, P/N: %s, S/N: %s", mem.MemModSlot, mem.MemModVendor, mem.MemModPartNum, mem.MemModSerial),
})
}
}
return events
}

97
internal/parser/vendors/inspur/fru.go vendored Normal file
View File

@@ -0,0 +1,97 @@
package inspur
import (
"bufio"
"regexp"
"strings"
"git.mchus.pro/mchus/logpile/internal/models"
)
var (
fruDeviceRegex = regexp.MustCompile(`^FRU Device Description\s*:\s*(.+)$`)
fruFieldRegex = regexp.MustCompile(`^\s+(.+?)\s*:\s*(.*)$`)
)
// ParseFRU parses BMC FRU (Field Replaceable Unit) output
func ParseFRU(content []byte) []models.FRUInfo {
var fruList []models.FRUInfo
var current *models.FRUInfo
scanner := bufio.NewScanner(strings.NewReader(string(content)))
for scanner.Scan() {
line := scanner.Text()
// Check for new FRU device
if matches := fruDeviceRegex.FindStringSubmatch(line); matches != nil {
if current != nil && current.Description != "" {
fruList = append(fruList, *current)
}
current = &models.FRUInfo{
Description: strings.TrimSpace(matches[1]),
}
continue
}
// Skip if no current FRU device
if current == nil {
continue
}
// Skip "Device not present" entries
if strings.Contains(line, "Device not present") {
current = nil
continue
}
// Parse FRU fields
if matches := fruFieldRegex.FindStringSubmatch(line); matches != nil {
fieldName := strings.TrimSpace(matches[1])
fieldValue := strings.TrimSpace(matches[2])
switch fieldName {
case "Chassis Type":
current.ChassisType = fieldValue
case "Chassis Part Number":
if fieldValue != "0" {
current.PartNumber = fieldValue
}
case "Chassis Serial":
if fieldValue != "0" {
current.SerialNumber = fieldValue
}
case "Board Mfg Date":
current.MfgDate = fieldValue
case "Board Mfg", "Product Manufacturer":
if fieldValue != "NULL" {
current.Manufacturer = fieldValue
}
case "Board Product", "Product Name":
if fieldValue != "NULL" {
current.ProductName = fieldValue
}
case "Board Serial", "Product Serial":
current.SerialNumber = fieldValue
case "Board Part Number", "Product Part Number":
if fieldValue != "0" {
current.PartNumber = fieldValue
}
case "Product Version":
if fieldValue != "0" {
current.Version = fieldValue
}
case "Product Asset Tag":
if fieldValue != "NULL" {
current.AssetTag = fieldValue
}
}
}
}
// Don't forget the last one
if current != nil && current.Description != "" {
fruList = append(fruList, *current)
}
return fruList
}

123
internal/parser/vendors/inspur/idl.go vendored Normal file
View File

@@ -0,0 +1,123 @@
package inspur
import (
"regexp"
"strings"
"time"
"git.mchus.pro/mchus/logpile/internal/models"
)
// ParseIDLLog parses the IDL (Inspur Diagnostic Log) file for BMC alarms
// Format: |timestamp|component|type|severity|eventID|description|
func ParseIDLLog(content []byte) []models.Event {
var events []models.Event
// Pattern to match CommerDiagnose log entries
// Example: |2025-12-02T17:54:27+08:00|MEMORY|Assert|Warning|0C180401|CPU1_C4D0 Memory Device Disabled...|
re := regexp.MustCompile(`\|(\d{4}-\d{2}-\d{2}T[\d:]+[+-]\d{2}:\d{2})\|([^|]+)\|([^|]+)\|([^|]+)\|([^|]+)\|([^|]+)\|`)
lines := strings.Split(string(content), "\n")
seenEvents := make(map[string]bool) // Deduplicate events
for _, line := range lines {
if !strings.Contains(line, "CommerDiagnose") {
continue
}
matches := re.FindStringSubmatch(line)
if matches == nil {
continue
}
timestamp := matches[1]
component := matches[2]
eventType := matches[3]
severityStr := matches[4]
eventID := matches[5]
description := matches[6]
// Parse timestamp
ts, err := time.Parse("2006-01-02T15:04:05-07:00", timestamp)
if err != nil {
ts = time.Now()
}
// Map severity
severity := mapIDLSeverity(severityStr)
// Clean up description
description = cleanDescription(description)
// Create unique key for deduplication
eventKey := eventID + "|" + description
if seenEvents[eventKey] {
continue
}
seenEvents[eventKey] = true
// Extract sensor name from description if available
sensorName := extractSensorName(description, component)
events = append(events, models.Event{
ID: eventID,
Timestamp: ts,
Source: component,
SensorType: strings.ToLower(component),
SensorName: sensorName,
EventType: eventType,
Severity: severity,
Description: description,
})
}
return events
}
func mapIDLSeverity(s string) models.Severity {
switch strings.ToLower(s) {
case "critical", "error":
return models.SeverityCritical
case "warning":
return models.SeverityWarning
default:
return models.SeverityInfo
}
}
func cleanDescription(desc string) string {
// Remove trailing " - Assert" or similar
desc = strings.TrimSuffix(desc, " - Assert")
desc = strings.TrimSuffix(desc, " - Deassert")
desc = strings.TrimSpace(desc)
return desc
}
func extractSensorName(desc, component string) string {
// Try to extract sensor/device name from description
// For memory: CPU1_C4D0, CPU1_C4D1, etc.
if component == "MEMORY" {
re := regexp.MustCompile(`(CPU\d+_C\d+D\d+)`)
if matches := re.FindStringSubmatch(desc); matches != nil {
return matches[1]
}
}
// For PSU: PSU0, PSU1, etc.
if component == "PSU" || component == "POWER" {
re := regexp.MustCompile(`(PSU\d+)`)
if matches := re.FindStringSubmatch(desc); matches != nil {
return matches[1]
}
}
// For temperature sensors
if component == "TEMPERATURE" || component == "THERMAL" {
re := regexp.MustCompile(`(\w+_Temp|\w+_DTS)`)
if matches := re.FindStringSubmatch(desc); matches != nil {
return matches[1]
}
}
return component
}

143
internal/parser/vendors/inspur/parser.go vendored Normal file
View File

@@ -0,0 +1,143 @@
// Package inspur provides parser for Inspur/Kaytus BMC diagnostic archives
// Tested with: Kaytus KR4268X2 (onekeylog format)
package inspur
import (
"strings"
"git.mchus.pro/mchus/logpile/internal/models"
"git.mchus.pro/mchus/logpile/internal/parser"
)
func init() {
parser.Register(&Parser{})
}
// Parser implements VendorParser for Inspur/Kaytus servers
type Parser struct{}
// Name returns human-readable parser name
func (p *Parser) Name() string {
return "Inspur/Kaytus BMC Parser"
}
// Vendor returns vendor identifier
func (p *Parser) Vendor() string {
return "inspur"
}
// Detect checks if archive matches Inspur/Kaytus format
// Returns confidence 0-100
func (p *Parser) Detect(files []parser.ExtractedFile) int {
confidence := 0
for _, f := range files {
path := strings.ToLower(f.Path)
// Strong indicators for Inspur/Kaytus onekeylog format
if strings.Contains(path, "onekeylog/") {
confidence += 30
}
if strings.Contains(path, "devicefrusdr.log") {
confidence += 25
}
if strings.Contains(path, "component/component.log") {
confidence += 15
}
// Check for asset.json with Inspur-specific structure
if strings.HasSuffix(path, "asset.json") {
if containsInspurMarkers(f.Content) {
confidence += 20
}
}
// Cap at 100
if confidence >= 100 {
return 100
}
}
return confidence
}
// containsInspurMarkers checks if content has Inspur-specific markers
func containsInspurMarkers(content []byte) bool {
s := string(content)
// Check for typical Inspur asset.json structure
return strings.Contains(s, "VersionInfo") &&
strings.Contains(s, "CpuInfo") &&
strings.Contains(s, "MemInfo")
}
// Parse parses Inspur/Kaytus archive
func (p *Parser) Parse(files []parser.ExtractedFile) (*models.AnalysisResult, error) {
result := &models.AnalysisResult{
Events: make([]models.Event, 0),
FRU: make([]models.FRUInfo, 0),
Sensors: make([]models.SensorReading, 0),
}
// Parse devicefrusdr.log (contains SDR and FRU data)
if f := parser.FindFileByName(files, "devicefrusdr.log"); f != nil {
p.parseDeviceFruSDR(f.Content, result)
}
// Parse asset.json
if f := parser.FindFileByName(files, "asset.json"); f != nil {
if hw, err := ParseAssetJSON(f.Content); err == nil {
result.Hardware = hw
}
}
// Parse component.log for additional data (PSU, etc.)
if f := parser.FindFileByName(files, "component.log"); f != nil {
if result.Hardware == nil {
result.Hardware = &models.HardwareConfig{}
}
ParseComponentLog(f.Content, result.Hardware)
// Extract events from component.log (memory errors, etc.)
componentEvents := ParseComponentLogEvents(f.Content)
result.Events = append(result.Events, componentEvents...)
}
// Parse IDL log (BMC alarms/diagnose events)
if f := parser.FindFileByName(files, "idl.log"); f != nil {
idlEvents := ParseIDLLog(f.Content)
result.Events = append(result.Events, idlEvents...)
}
// Parse syslog files
syslogFiles := parser.FindFileByPattern(files, "syslog/alert", "syslog/warning", "syslog/notice", "syslog/info")
for _, f := range syslogFiles {
events := ParseSyslog(f.Content, f.Path)
result.Events = append(result.Events, events...)
}
return result, nil
}
func (p *Parser) parseDeviceFruSDR(content []byte, result *models.AnalysisResult) {
lines := string(content)
// Find SDR section
sdrStart := strings.Index(lines, "BMC sdr Info:")
fruStart := strings.Index(lines, "BMC fru Info:")
if sdrStart != -1 {
var sdrContent string
if fruStart != -1 && fruStart > sdrStart {
sdrContent = lines[sdrStart:fruStart]
} else {
sdrContent = lines[sdrStart:]
}
result.Sensors = ParseSDR([]byte(sdrContent))
}
// Find FRU section
if fruStart != -1 {
fruContent := lines[fruStart:]
result.FRU = ParseFRU([]byte(fruContent))
}
}

89
internal/parser/vendors/inspur/sdr.go vendored Normal file
View File

@@ -0,0 +1,89 @@
package inspur
import (
"bufio"
"regexp"
"strconv"
"strings"
"git.mchus.pro/mchus/logpile/internal/models"
)
// SDR sensor reading patterns
var (
sdrLineRegex = regexp.MustCompile(`^(\S+)\s+\|\s+(.+?)\s+\|\s+(\w+)$`)
valueRegex = regexp.MustCompile(`^([\d.]+)\s+(.+)$`)
)
// ParseSDR parses BMC SDR (Sensor Data Record) output
func ParseSDR(content []byte) []models.SensorReading {
var readings []models.SensorReading
scanner := bufio.NewScanner(strings.NewReader(string(content)))
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" || strings.HasPrefix(line, "BMC sdr Info:") {
continue
}
matches := sdrLineRegex.FindStringSubmatch(line)
if matches == nil {
continue
}
name := strings.TrimSpace(matches[1])
valueStr := strings.TrimSpace(matches[2])
status := strings.TrimSpace(matches[3])
reading := models.SensorReading{
Name: name,
Status: status,
}
// Parse value and unit
if valueStr != "disabled" && valueStr != "no reading" && !strings.HasPrefix(valueStr, "0x") {
if vm := valueRegex.FindStringSubmatch(valueStr); vm != nil {
if v, err := strconv.ParseFloat(vm[1], 64); err == nil {
reading.Value = v
reading.Unit = strings.TrimSpace(vm[2])
}
}
} else if strings.HasPrefix(valueStr, "0x") {
reading.RawValue = valueStr
}
// Determine sensor type
reading.Type = determineSensorType(name)
readings = append(readings, reading)
}
return readings
}
func determineSensorType(name string) string {
nameLower := strings.ToLower(name)
switch {
case strings.Contains(nameLower, "temp"):
return "temperature"
case strings.Contains(nameLower, "fan") && strings.Contains(nameLower, "speed"):
return "fan_speed"
case strings.Contains(nameLower, "fan") && strings.Contains(nameLower, "status"):
return "fan_status"
case strings.HasSuffix(nameLower, "_vin") || strings.HasSuffix(nameLower, "_vout") ||
strings.HasSuffix(nameLower, "_v") || strings.Contains(nameLower, "volt"):
return "voltage"
case strings.Contains(nameLower, "power") || strings.HasSuffix(nameLower, "_pin") ||
strings.HasSuffix(nameLower, "_pout") || strings.HasSuffix(nameLower, "_pwr"):
return "power"
case strings.Contains(nameLower, "psu") && strings.Contains(nameLower, "status"):
return "psu_status"
case strings.Contains(nameLower, "cpu") && strings.Contains(nameLower, "status"):
return "cpu_status"
case strings.Contains(nameLower, "hdd") || strings.Contains(nameLower, "nvme"):
return "storage_status"
default:
return "other"
}
}

View File

@@ -0,0 +1,97 @@
package inspur
import (
"bufio"
"regexp"
"strings"
"time"
"git.mchus.pro/mchus/logpile/internal/models"
)
var (
// Syslog format: <priority> timestamp hostname process: message
syslogRegex = regexp.MustCompile(`^<(\d+)>\s*(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}[^\s]*)\s+(\S+)\s+(\S+):\s*(.*)$`)
)
// ParseSyslog parses syslog format logs
func ParseSyslog(content []byte, sourcePath string) []models.Event {
var events []models.Event
// Determine severity from file path
severity := determineSeverityFromPath(sourcePath)
scanner := bufio.NewScanner(strings.NewReader(string(content)))
lineNum := 0
for scanner.Scan() {
lineNum++
line := strings.TrimSpace(scanner.Text())
if line == "" {
continue
}
matches := syslogRegex.FindStringSubmatch(line)
if matches == nil {
continue
}
timestamp, err := time.Parse(time.RFC3339, matches[2])
if err != nil {
// Try alternative format
timestamp, err = time.Parse("2006-01-02T15:04:05.000000-07:00", matches[2])
if err != nil {
continue
}
}
event := models.Event{
ID: generateEventID(sourcePath, lineNum),
Timestamp: timestamp,
Source: matches[4],
SensorType: "syslog",
SensorName: matches[3],
Description: matches[5],
Severity: severity,
RawData: line,
}
events = append(events, event)
}
return events
}
func determineSeverityFromPath(path string) models.Severity {
pathLower := strings.ToLower(path)
switch {
case strings.Contains(pathLower, "emerg") || strings.Contains(pathLower, "alert") ||
strings.Contains(pathLower, "crit"):
return models.SeverityCritical
case strings.Contains(pathLower, "warn") || strings.Contains(pathLower, "error"):
return models.SeverityWarning
default:
return models.SeverityInfo
}
}
func generateEventID(source string, lineNum int) string {
parts := strings.Split(source, "/")
filename := parts[len(parts)-1]
return strings.TrimSuffix(filename, ".log") + "_" + itoa(lineNum)
}
func itoa(i int) string {
if i == 0 {
return "0"
}
var b [20]byte
pos := len(b)
for i > 0 {
pos--
b[pos] = byte('0' + i%10)
i /= 10
}
return string(b[pos:])
}