package inspur import ( "bufio" "regexp" "strconv" "strings" "time" "git.mchus.pro/mchus/logpile/internal/models" ) var ( // Syslog format: 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*(.*)$`) bootRelativeMessageRegex = regexp.MustCompile(`^\[\s*\d+(?:\.\d+)?\]`) ) // ParseSyslog parses syslog format logs func ParseSyslog(content []byte, sourcePath string) []models.Event { var events []models.Event 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 } priority, err := strconv.Atoi(matches[1]) if err != 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 } } message := strings.TrimSpace(matches[5]) // Some AMI BMC boots emit a fabricated 1970 wall-clock timestamp while // the payload only carries seconds since boot. Without a trustworthy // boot epoch this cannot become a wall-clock event, so omit it instead // of exporting either 1970 or the archive collection time. if timestamp.Year() <= 1971 && bootRelativeMessageRegex.MatchString(message) { continue } event := models.Event{ ID: generateEventID(sourcePath, lineNum), Timestamp: timestamp, Source: "syslog", SensorType: "syslog", SensorName: matches[4], Description: message, Severity: determineSyslogSeverity(priority, message, sourcePath), RawData: line, } events = append(events, event) } return events } func determineSyslogSeverity(priority int, message, sourcePath string) models.Severity { // These AMI driver start-up/status strings are routed to alert.log and // warning.log despite not describing a fault. The PRI value is therefore // not trustworthy for this small, observed set of benign messages. lowerMessage := strings.ToLower(message) benignMessages := []string{ "helper module driver version", "copyright (c)", "color depth is 15 bpp or higher", "new driver 0 directmode 1", } for _, benign := range benignMessages { if strings.Contains(lowerMessage, benign) { return models.SeverityInfo } } // RFC 5424 severity is stored in the low three bits of PRI: // 0..2 emergency/alert/critical, 3..4 error/warning, 5..7 notice/info/debug. if priority >= 0 { switch priority & 7 { case 0, 1, 2: return models.SeverityCritical case 3, 4: return models.SeverityWarning default: return models.SeverityInfo } } return determineSeverityFromPath(sourcePath) } 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:]) }