sync file-type support across upload/convert and fix collected_at timezone handling
This commit is contained in:
42
internal/parser/vendors/inspur/parser.go
vendored
42
internal/parser/vendors/inspur/parser.go
vendored
@@ -8,6 +8,7 @@ package inspur
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.mchus.pro/mchus/logpile/internal/models"
|
||||
"git.mchus.pro/mchus/logpile/internal/parser"
|
||||
@@ -86,6 +87,8 @@ func containsInspurMarkers(content []byte) bool {
|
||||
|
||||
// Parse parses Inspur/Kaytus archive
|
||||
func (p *Parser) Parse(files []parser.ExtractedFile) (*models.AnalysisResult, error) {
|
||||
selLocation := inferInspurArchiveLocation(files)
|
||||
|
||||
result := &models.AnalysisResult{
|
||||
Events: make([]models.Event, 0),
|
||||
FRU: make([]models.FRUInfo, 0),
|
||||
@@ -145,7 +148,7 @@ func (p *Parser) Parse(files []parser.ExtractedFile) (*models.AnalysisResult, er
|
||||
|
||||
// Parse SEL list (selelist.csv)
|
||||
if f := parser.FindFileByName(files, "selelist.csv"); f != nil {
|
||||
selEvents := ParseSELList(f.Content)
|
||||
selEvents := ParseSELListWithLocation(f.Content, selLocation)
|
||||
result.Events = append(result.Events, selEvents...)
|
||||
}
|
||||
|
||||
@@ -184,6 +187,43 @@ func (p *Parser) Parse(files []parser.ExtractedFile) (*models.AnalysisResult, er
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func inferInspurArchiveLocation(files []parser.ExtractedFile) *time.Location {
|
||||
fallback := parser.DefaultArchiveLocation()
|
||||
f := parser.FindFileByName(files, "timezone.conf")
|
||||
if f == nil {
|
||||
return fallback
|
||||
}
|
||||
locName := parseTimezoneConfigLocation(f.Content)
|
||||
if strings.TrimSpace(locName) == "" {
|
||||
return fallback
|
||||
}
|
||||
loc, err := time.LoadLocation(locName)
|
||||
if err != nil {
|
||||
return fallback
|
||||
}
|
||||
return loc
|
||||
}
|
||||
|
||||
func parseTimezoneConfigLocation(content []byte) string {
|
||||
lines := strings.Split(string(content), "\n")
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" || strings.HasPrefix(line, "[") || strings.HasPrefix(line, "#") || strings.HasPrefix(line, ";") {
|
||||
continue
|
||||
}
|
||||
parts := strings.SplitN(line, "=", 2)
|
||||
if len(parts) != 2 {
|
||||
continue
|
||||
}
|
||||
key := strings.ToLower(strings.TrimSpace(parts[0]))
|
||||
val := strings.TrimSpace(parts[1])
|
||||
if key == "timezone" && val != "" {
|
||||
return val
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (p *Parser) parseDeviceFruSDR(content []byte, result *models.AnalysisResult) {
|
||||
lines := string(content)
|
||||
|
||||
|
||||
16
internal/parser/vendors/inspur/sel.go
vendored
16
internal/parser/vendors/inspur/sel.go
vendored
@@ -13,6 +13,12 @@ import (
|
||||
// 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 {
|
||||
var events []models.Event
|
||||
|
||||
text := string(content)
|
||||
@@ -49,7 +55,7 @@ func ParseSELList(content []byte) []models.Event {
|
||||
status := strings.TrimSpace(records[5])
|
||||
|
||||
// Parse timestamp: MM/DD/YYYY HH:MM:SS
|
||||
timestamp := parseSELTimestamp(dateStr, timeStr)
|
||||
timestamp := parseSELTimestamp(dateStr, timeStr, location)
|
||||
|
||||
// Extract sensor type and name
|
||||
sensorType, sensorName := parseSensorInfo(sensorStr)
|
||||
@@ -77,12 +83,16 @@ func ParseSELList(content []byte) []models.Event {
|
||||
}
|
||||
|
||||
// parseSELTimestamp parses MM/DD/YYYY and HH:MM:SS into time.Time
|
||||
func parseSELTimestamp(dateStr, timeStr string) time.Time {
|
||||
func parseSELTimestamp(dateStr, timeStr string, location *time.Location) time.Time {
|
||||
// Combine date and time: MM/DD/YYYY HH:MM:SS
|
||||
timestampStr := dateStr + " " + timeStr
|
||||
|
||||
if location == nil {
|
||||
location = parser.DefaultArchiveLocation()
|
||||
}
|
||||
|
||||
// Try parsing with MM/DD/YYYY format
|
||||
t, err := parser.ParseInDefaultArchiveLocation("01/02/2006 15:04:05", timestampStr)
|
||||
t, err := time.ParseInLocation("01/02/2006 15:04:05", timestampStr, location)
|
||||
if err != nil {
|
||||
// Fallback to current time
|
||||
return time.Now()
|
||||
|
||||
33
internal/parser/vendors/inspur/sel_test.go
vendored
Normal file
33
internal/parser/vendors/inspur/sel_test.go
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
package inspur
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestParseSELListWithLocation_UsesProvidedTimezone(t *testing.T) {
|
||||
content := []byte("sel elist:\n1,02/28/2026,04:18:18,Sensor X,Event,Asserted\n")
|
||||
shanghai, err := time.LoadLocation("Asia/Shanghai")
|
||||
if err != nil {
|
||||
t.Fatalf("load location: %v", err)
|
||||
}
|
||||
|
||||
events := ParseSELListWithLocation(content, shanghai)
|
||||
if len(events) != 1 {
|
||||
t.Fatalf("expected 1 event, got %d", len(events))
|
||||
}
|
||||
|
||||
// 04:18:18 +08:00 == 20:18:18Z (previous day)
|
||||
want := time.Date(2026, 2, 27, 20, 18, 18, 0, time.UTC)
|
||||
if !events[0].Timestamp.UTC().Equal(want) {
|
||||
t.Fatalf("unexpected timestamp: got %s want %s", events[0].Timestamp.UTC(), want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseTimezoneConfigLocation(t *testing.T) {
|
||||
content := []byte("[TimeZoneConfig]\ntimezone=Asia/Shanghai\n")
|
||||
got := parseTimezoneConfigLocation(content)
|
||||
if got != "Asia/Shanghai" {
|
||||
t.Fatalf("unexpected timezone: %q", got)
|
||||
}
|
||||
}
|
||||
@@ -3,14 +3,33 @@
|
||||
package nvidia_bug_report
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.mchus.pro/mchus/logpile/internal/models"
|
||||
"git.mchus.pro/mchus/logpile/internal/parser"
|
||||
)
|
||||
|
||||
// parserVersion - version of this parser module
|
||||
const parserVersion = "1.0.0"
|
||||
const parserVersion = "1.1.0"
|
||||
|
||||
var bugReportDateLineRegex = regexp.MustCompile(`(?m)^Date:\s+(.+?)\s*$`)
|
||||
var dateWithTZAbbrevRegex = regexp.MustCompile(`^([A-Za-z]{3}\s+[A-Za-z]{3}\s+\d{1,2}\s+\d{2}:\d{2}:\d{2})\s+([A-Za-z]{2,5})\s+(\d{4})$`)
|
||||
|
||||
var timezoneAbbrevToOffset = map[string]string{
|
||||
"UTC": "+00:00",
|
||||
"GMT": "+00:00",
|
||||
"EST": "-05:00",
|
||||
"EDT": "-04:00",
|
||||
"CST": "-06:00",
|
||||
"CDT": "-05:00",
|
||||
"MST": "-07:00",
|
||||
"MDT": "-06:00",
|
||||
"PST": "-08:00",
|
||||
"PDT": "-07:00",
|
||||
}
|
||||
|
||||
func init() {
|
||||
parser.Register(&Parser{})
|
||||
@@ -81,6 +100,10 @@ func (p *Parser) Parse(files []parser.ExtractedFile) (*models.AnalysisResult, er
|
||||
}
|
||||
|
||||
content := string(files[0].Content)
|
||||
if collectedAt, tzOffset, ok := parseBugReportCollectedAt(content); ok {
|
||||
result.CollectedAt = collectedAt.UTC()
|
||||
result.SourceTimezone = tzOffset
|
||||
}
|
||||
|
||||
// Parse system information
|
||||
parseSystemInfo(content, result)
|
||||
@@ -105,3 +128,49 @@ func (p *Parser) Parse(files []parser.ExtractedFile) (*models.AnalysisResult, er
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func parseBugReportCollectedAt(content string) (time.Time, string, bool) {
|
||||
matches := bugReportDateLineRegex.FindStringSubmatch(content)
|
||||
if len(matches) != 2 {
|
||||
return time.Time{}, "", false
|
||||
}
|
||||
raw := strings.TrimSpace(matches[1])
|
||||
if raw == "" {
|
||||
return time.Time{}, "", false
|
||||
}
|
||||
|
||||
if m := dateWithTZAbbrevRegex.FindStringSubmatch(raw); len(m) == 4 {
|
||||
if offset, ok := timezoneAbbrevToOffset[strings.ToUpper(strings.TrimSpace(m[2]))]; ok {
|
||||
layout := "Mon Jan 2 15:04:05 -07:00 2006"
|
||||
normalized := strings.TrimSpace(m[1]) + " " + offset + " " + strings.TrimSpace(m[3])
|
||||
if ts, err := time.Parse(layout, normalized); err == nil {
|
||||
return ts, offset, true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
layouts := []string{
|
||||
"Mon Jan 2 15:04:05 MST 2006",
|
||||
"Mon Jan 2 15:04:05 2006",
|
||||
}
|
||||
for _, layout := range layouts {
|
||||
ts, err := time.Parse(layout, raw)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
return ts, formatOffset(ts), true
|
||||
}
|
||||
return time.Time{}, "", false
|
||||
}
|
||||
|
||||
func formatOffset(t time.Time) string {
|
||||
_, sec := t.Zone()
|
||||
sign := '+'
|
||||
if sec < 0 {
|
||||
sign = '-'
|
||||
sec = -sec
|
||||
}
|
||||
h := sec / 3600
|
||||
m := (sec % 3600) / 60
|
||||
return fmt.Sprintf("%c%02d:%02d", sign, h, m)
|
||||
}
|
||||
|
||||
54
internal/parser/vendors/nvidia_bug_report/parser_test.go
vendored
Normal file
54
internal/parser/vendors/nvidia_bug_report/parser_test.go
vendored
Normal file
@@ -0,0 +1,54 @@
|
||||
package nvidia_bug_report
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.mchus.pro/mchus/logpile/internal/parser"
|
||||
)
|
||||
|
||||
func TestParseBugReportCollectedAt(t *testing.T) {
|
||||
content := `
|
||||
Start of NVIDIA bug report log file
|
||||
Date: Fri Dec 12 10:14:49 EST 2025
|
||||
`
|
||||
|
||||
got, tz, ok := parseBugReportCollectedAt(content)
|
||||
if !ok {
|
||||
t.Fatalf("expected collected_at to be parsed")
|
||||
}
|
||||
if tz != "-05:00" {
|
||||
t.Fatalf("expected tz offset -05:00, got %q", tz)
|
||||
}
|
||||
wantUTC := time.Date(2025, 12, 12, 15, 14, 49, 0, time.UTC)
|
||||
if !got.UTC().Equal(wantUTC) {
|
||||
t.Fatalf("expected %s, got %s", wantUTC, got.UTC())
|
||||
}
|
||||
}
|
||||
|
||||
func TestNvidiaBugReportParser_SetsCollectedAtAndTimezone(t *testing.T) {
|
||||
p := &Parser{}
|
||||
files := []parser.ExtractedFile{
|
||||
{
|
||||
Path: "nvidia-bug-report-1653925023938.log",
|
||||
Content: []byte(`
|
||||
Start of NVIDIA bug report log file
|
||||
nvidia-bug-report.sh Version: 34275561
|
||||
Date: Fri Dec 12 10:14:49 EST 2025
|
||||
`),
|
||||
},
|
||||
}
|
||||
|
||||
result, err := p.Parse(files)
|
||||
if err != nil {
|
||||
t.Fatalf("parse failed: %v", err)
|
||||
}
|
||||
|
||||
if result.SourceTimezone != "-05:00" {
|
||||
t.Fatalf("expected source timezone -05:00, got %q", result.SourceTimezone)
|
||||
}
|
||||
wantUTC := time.Date(2025, 12, 12, 15, 14, 49, 0, time.UTC)
|
||||
if !result.CollectedAt.Equal(wantUTC) {
|
||||
t.Fatalf("expected collected_at %s, got %s", wantUTC, result.CollectedAt)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user