package service import ( "bufio" "encoding/json" "fmt" "hy2xs-admin/model/vo" "hy2xs-admin/util" "strconv" "strings" "time" ) type journalctlLogLine struct { Message string `json:"MESSAGE"` Priority string `json:"PRIORITY"` RealtimeTimestamp string `json:"__REALTIME_TIMESTAMP"` } func normalizeJournalLineLimit(value int, fallback int, max int) int { if value <= 0 { return fallback } if value > max { return max } return value } func ReadHysteriaJournalLogs(numLine int) ([]vo.LogHysteria2Vo, int, error) { lines := normalizeJournalLineLimit(numLine, 100, 300) output, err := util.Exec(fmt.Sprintf("journalctl --no-pager -u hysteria-server.service -n %d -o json", lines)) if err != nil { return nil, 0, err } result := make([]vo.LogHysteria2Vo, 0, lines) scanner := bufio.NewScanner(strings.NewReader(output)) for scanner.Scan() { line := strings.TrimSpace(scanner.Text()) if line == "" { continue } item := journalctlLogLine{} if err = json.Unmarshal([]byte(line), &item); err != nil { continue } result = append(result, parseHysteriaJournalRecord(item)) } if scanErr := scanner.Err(); scanErr != nil { return nil, 0, scanErr } return result, len(result), nil } // ExportHysteriaJournalLogs отдаёт журнал Hysteria оператору. // // Файл покидает сервер, поэтому проходит тот же санитайз, что и остальные // выгружаемые артефакты: HY2_AUTH_URL несёт access_token, и upstream волен // упомянуть его в сообщении об ошибке обращения к auth-backend. func ExportHysteriaJournalLogs(numLine int) (string, error) { lines := normalizeJournalLineLimit(numLine, 5000, 10000) output, err := util.Exec(fmt.Sprintf("journalctl --no-pager -u hysteria-server.service -n %d -o short-iso", lines)) if err != nil { return "", err } return SanitizeLogText(output), nil } func parseHysteriaJournalRecord(item journalctlLogLine) vo.LogHysteria2Vo { fallbackTime := convertJournalTimestamp(item.RealtimeTimestamp) fallbackLevel := mapJournalPriorityToLevel(item.Priority) if strings.TrimSpace(item.Message) == "" { return vo.LogHysteria2Vo{ Level: fallbackLevel, Msg: "", Time: fallbackTime, } } parsed := vo.LogHysteria2Vo{} if err := json.Unmarshal([]byte(item.Message), &parsed); err == nil { if parsed.Level == "" { parsed.Level = fallbackLevel } if parsed.Time == "" { parsed.Time = fallbackTime } if parsed.Msg == "" { parsed.Msg = item.Message } parsed.Msg = SanitizeLogText(parsed.Msg) return parsed } return vo.LogHysteria2Vo{ Level: fallbackLevel, Msg: SanitizeLogText(item.Message), Time: fallbackTime, } } func convertJournalTimestamp(raw string) string { if raw == "" { return "" } us, err := strconv.ParseInt(raw, 10, 64) if err != nil { return "" } t := time.UnixMicro(us) return t.Format("2006-01-02 15:04:05") } func mapJournalPriorityToLevel(priority string) string { switch strings.TrimSpace(priority) { case "0", "1", "2", "3": return "error" case "4": return "warn" default: return "info" } }