Support TXT uploads and extend XigmaNAS event parsing

This commit is contained in:
2026-02-04 22:25:43 +03:00
parent ae588ae75a
commit 92134a6cc1
6 changed files with 296 additions and 5 deletions

View File

@@ -12,7 +12,7 @@ import (
)
// parserVersion - increment when parsing logic changes.
const parserVersion = "2.0.0"
const parserVersion = "2.1.0"
func init() {
parser.Register(&Parser{})
@@ -86,6 +86,7 @@ func (p *Parser) Parse(files []parser.ExtractedFile) (*models.AnalysisResult, er
parseUptime(content, result)
parseZFSState(content, result)
parseStorageAndSMART(content, result)
parseJournalLogSections(content, result)
return result, nil
}
@@ -337,6 +338,138 @@ func parseStorageAndSMART(content string, result *models.AnalysisResult) {
}
}
func parseJournalLogSections(content string, result *models.AnalysisResult) {
sections := []struct {
heading string
eventType string
source string
}{
{heading: "Last 275 System log entries:", eventType: "System Log", source: "system.log"},
{heading: "Last 275 SMARTD log entries:", eventType: "SMARTD Log", source: "smartd.log"},
{heading: "Last 275 Daemon log entries:", eventType: "Daemon Log", source: "daemon.log"},
}
for _, sec := range sections {
body := extractLogSection(content, sec.heading)
if body == "" {
continue
}
for _, line := range strings.Split(body, "\n") {
line = strings.TrimSpace(line)
if line == "" {
continue
}
msg := extractSyslogMessage(line)
if msg == "" {
msg = line
}
result.Events = append(result.Events, models.Event{
Timestamp: parseEventTimestamp(line),
Source: sec.source,
EventType: sec.eventType,
Severity: classifyEventSeverity(line),
Description: msg,
RawData: line,
})
}
}
}
func extractLogSection(content, heading string) string {
start := strings.Index(content, heading)
if start == -1 {
return ""
}
tail := content[start+len(heading):]
lines := strings.Split(tail, "\n")
i := 0
for i < len(lines) && strings.TrimSpace(lines[i]) == "" {
i++
}
if i < len(lines) && isDashLine(lines[i]) {
i++
}
out := make([]string, 0, 64)
for ; i < len(lines); i++ {
line := lines[i]
trimmed := strings.TrimSpace(line)
if strings.HasPrefix(trimmed, "Last 275 ") && strings.HasSuffix(trimmed, " log entries:") {
break
}
out = append(out, line)
}
return strings.TrimSpace(strings.Join(out, "\n"))
}
func isDashLine(s string) bool {
s = strings.TrimSpace(s)
if s == "" {
return false
}
for _, r := range s {
if r != '-' {
return false
}
}
return true
}
func parseEventTimestamp(line string) time.Time {
isoRe := regexp.MustCompile(`\b\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?[+-]\d{2}:\d{2}\b`)
if iso := isoRe.FindString(line); iso != "" {
if ts, err := time.Parse(time.RFC3339Nano, iso); err == nil {
return ts
}
}
prefixRe := regexp.MustCompile(`^[A-Z][a-z]{2}\s+\d{1,2}\s+\d{2}:\d{2}:\d{2}`)
if prefix := prefixRe.FindString(line); prefix != "" {
year := time.Now().Year()
if ts, err := time.Parse("Jan 2 15:04:05 2006", prefix+" "+strconv.Itoa(year)); err == nil {
return ts
}
}
return time.Now()
}
func classifyEventSeverity(line string) models.Severity {
lower := strings.ToLower(line)
switch {
case strings.Contains(lower, "panic"), strings.Contains(lower, "fatal"), strings.Contains(lower, "critical"):
return models.SeverityCritical
case strings.Contains(lower, "warning"),
strings.Contains(lower, "error"),
strings.Contains(lower, "failed"),
strings.Contains(lower, "failure"),
strings.Contains(lower, "login failure"),
strings.Contains(lower, "limiting open port"):
return models.SeverityWarning
default:
return models.SeverityInfo
}
}
func extractSyslogMessage(line string) string {
if idx := strings.Index(line, ": "); idx != -1 && idx+2 < len(line) {
return strings.TrimSpace(line[idx+2:])
}
// RFC5424-like segment in XigmaNAS dumps: "... <host> <proc> <pid> - - <message>"
fields := strings.Fields(line)
if len(fields) > 10 {
return strings.TrimSpace(strings.Join(fields[10:], " "))
}
return strings.TrimSpace(line)
}
func splitModelAndFirmware(raw string) (string, string) {
fields := strings.Fields(raw)
if len(fields) < 2 {

View File

@@ -91,4 +91,26 @@ func TestParserParseExample(t *testing.T) {
if len(result.Events) == 0 {
t.Fatal("expected events from uptime/zfs sections")
}
var hasSystemLog, hasSmartdLog, hasDaemonLog, hasLoginFailure bool
for _, ev := range result.Events {
if ev.EventType == "System Log" {
hasSystemLog = true
}
if ev.EventType == "SMARTD Log" {
hasSmartdLog = true
}
if ev.EventType == "Daemon Log" {
hasDaemonLog = true
}
if strings.Contains(strings.ToLower(ev.Description), "login failure") {
hasLoginFailure = true
}
}
if !hasSystemLog || !hasSmartdLog || !hasDaemonLog {
t.Fatalf("expected events from System/SMARTD/Daemon sections, got system=%v smartd=%v daemon=%v", hasSystemLog, hasSmartdLog, hasDaemonLog)
}
if !hasLoginFailure {
t.Fatal("expected to parse login failure event from system log section")
}
}