package exporter import ( "encoding/csv" "encoding/json" "io" "strings" "git.mchus.pro/mchus/logpile/internal/models" ) // Exporter handles data export in various formats type Exporter struct { result *models.AnalysisResult } // New creates a new exporter func New(result *models.AnalysisResult) *Exporter { return &Exporter{result: result} } // ExportCSV exports serial numbers to CSV format func (e *Exporter) ExportCSV(w io.Writer) error { if _, err := w.Write([]byte{0xEF, 0xBB, 0xBF}); err != nil { return err } writer := csv.NewWriter(w) writer.Comma = ';' defer writer.Flush() // Header if err := writer.Write([]string{"Component", "Serial Number", "Manufacturer", "Location"}); err != nil { return err } if e.result == nil { return nil } // FRU data for _, fru := range e.result.FRU { if !hasUsableSerial(fru.SerialNumber) { continue } name := fru.ProductName if name == "" { name = fru.Description } if err := writer.Write([]string{ name, fru.SerialNumber, fru.Manufacturer, fru.PartNumber, }); err != nil { return err } } // Hardware data if e.result.Hardware != nil { // Board if hasUsableSerial(e.result.Hardware.BoardInfo.SerialNumber) { if err := writer.Write([]string{ e.result.Hardware.BoardInfo.ProductName, strings.TrimSpace(e.result.Hardware.BoardInfo.SerialNumber), e.result.Hardware.BoardInfo.Manufacturer, "Board", }); err != nil { return err } } seenCanonical := make(map[string]struct{}) for _, dev := range canonicalDevicesForExport(e.result.Hardware) { if !hasUsableSerial(dev.SerialNumber) { continue } serial := strings.TrimSpace(dev.SerialNumber) seenCanonical[serial] = struct{}{} component, manufacturer, location := csvFieldsFromCanonicalDevice(dev) if err := writer.Write([]string{component, serial, manufacturer, location}); err != nil { return err } } // Legacy network cards for _, nic := range e.result.Hardware.NetworkCards { if !hasUsableSerial(nic.SerialNumber) { continue } serial := strings.TrimSpace(nic.SerialNumber) if _, ok := seenCanonical[serial]; ok { continue } if err := writer.Write([]string{ nic.Model, serial, "", "Network", }); err != nil { return err } } } return nil } // ExportJSON exports all data to JSON format func (e *Exporter) ExportJSON(w io.Writer) error { encoder := json.NewEncoder(w) encoder.SetIndent("", " ") return encoder.Encode(e.result) } func hasUsableSerial(serial string) bool { s := strings.TrimSpace(serial) if s == "" { return false } switch strings.ToUpper(s) { case "N/A", "NA", "NONE", "NULL", "UNKNOWN", "-": return false default: return true } } func csvFieldsFromCanonicalDevice(dev models.HardwareDevice) (component, manufacturer, location string) { component = firstNonEmptyString( dev.Model, dev.PartNumber, dev.DeviceClass, dev.Kind, ) manufacturer = firstNonEmptyString(dev.Manufacturer, inferCSVVendor(dev)) location = firstNonEmptyString(dev.Location, dev.Slot, dev.BDF, dev.Kind) switch dev.Kind { case models.DeviceKindCPU: if component == "" { component = "CPU" } if location == "" { location = "CPU" } case models.DeviceKindMemory: component = firstNonEmptyString(dev.PartNumber, dev.Model, "Memory") case models.DeviceKindPCIe, models.DeviceKindGPU, models.DeviceKindNetwork: if location == "" { location = firstNonEmptyString(dev.Slot, dev.BDF, "PCIe") } case models.DeviceKindPSU: component = firstNonEmptyString(dev.Model, "Power Supply") } return component, manufacturer, location } func inferCSVVendor(dev models.HardwareDevice) string { switch dev.Kind { case models.DeviceKindCPU: return "" default: return "" } } func firstNonEmptyString(values ...string) string { for _, value := range values { if strings.TrimSpace(value) != "" { return strings.TrimSpace(value) } } return "" } // ExportLogsCSV writes all recognized events as a semicolon-delimited UTF-8 CSV readable in Excel. func ExportLogsCSV(w io.Writer, result *models.AnalysisResult) error { if _, err := w.Write([]byte{0xEF, 0xBB, 0xBF}); err != nil { return err } writer := csv.NewWriter(w) writer.Comma = ';' defer writer.Flush() if err := writer.Write([]string{"timestamp", "source", "severity", "sensor_type", "sensor_name", "event_type", "id", "description", "raw_data"}); err != nil { return err } if result == nil { return nil } for _, e := range result.Events { ts := "" if !e.Timestamp.IsZero() { ts = e.Timestamp.UTC().Format("2006-01-02T15:04:05Z") } if err := writer.Write([]string{ ts, e.Source, string(e.Severity), e.SensorType, e.SensorName, e.EventType, e.ID, e.Description, e.RawData, }); err != nil { return err } } return nil }