// backend\log.go package main import ( "fmt" "io" "net/http" "os" "path/filepath" "strings" "sync" "time" ) var ( appLogMu sync.Mutex appLogFile *os.File ) func appLogPath() string { exePath, _ := os.Executable() exeDir := "" if exePath != "" { exeDir = filepath.Dir(exePath) } if exeDir == "" { exeDir, _ = os.Getwd() } return filepath.Join(exeDir, "recorder.log") } func openAppLogFile() (*os.File, error) { path := appLogPath() if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil { return nil, err } return os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) } func initAppLog() { appLogMu.Lock() defer appLogMu.Unlock() if appLogFile != nil { return } f, err := openAppLogFile() if err != nil { // Wichtig: hier NICHT appLogln verwenden, weil appLogMu schon gelockt ist. fmt.Println("⚠️ recorder log konnte nicht geöffnet werden:", err) return } appLogFile = f _, _ = fmt.Fprintf( appLogFile, "\n\n--- Recorder Start %s ---\nlog=%s\n", time.Now().Format(time.RFC3339), appLogPath(), ) _ = appLogFile.Sync() } func closeAppLog() { appLogMu.Lock() defer appLogMu.Unlock() if appLogFile == nil { return } _, _ = fmt.Fprintf( appLogFile, "--- Recorder Stop %s ---\n", time.Now().Format(time.RFC3339), ) _ = appLogFile.Sync() _ = appLogFile.Close() appLogFile = nil } func appLogWriter() io.Writer { initAppLog() appLogMu.Lock() defer appLogMu.Unlock() if appLogFile != nil { return appLogFile } return os.Stderr } func appLogWriteLine(line string) { line = strings.TrimRight(line, "\r\n") if appLogLineSuppressed(line) { return } ts := time.Now().Format("2006-01-02 15:04:05") out := fmt.Sprintf("[%s] %s\n", ts, line) appLogMu.Lock() defer appLogMu.Unlock() fmt.Print(out) if appLogFile != nil { _, _ = appLogFile.WriteString(out) _ = appLogFile.Sync() } } func appLogln(args ...any) { appLogWriteLine(fmt.Sprintln(args...)) } func appLogf(format string, args ...any) { appLogWriteLine(fmt.Sprintf(format, args...)) } // appErrorLogSuppressed unterdrückt das automatische ❌-Logging für bekannte, // erwartbare und sehr häufige Fehler (z.B. abgelaufene HLS-Edge-URLs während // eines Stream-Reconnects). Der Fehler wird weiterhin zurückgegeben, damit die // Retry-/Reconnect-Logik unverändert funktioniert – nur die Log-Zeile entfällt. func appLogLineSuppressed(line string) bool { m := strings.ToLower(strings.TrimSpace(line)) if m == "" { return true } if strings.Contains(m, "get playlist: context canceled") { return true } if strings.Contains(m, "usernamelookup ohne user.id") { return true } if strings.Contains(m, "mfc status failed") && (strings.Contains(m, "user not found") || strings.Contains(m, "usernamelookup")) { return true } if strings.Contains(m, "mfc playlist candidate failed") && (strings.Contains(m, "http 403") || strings.Contains(m, "http 404")) { return true } if strings.Contains(m, "mfc lookup:") { return true } isHLS := strings.Contains(m, ".m3u8") || strings.Contains(m, "abruf der m3u8") || strings.Contains(m, "variant-playlist") if isHLS && strings.Contains(m, "http 403") { return true } if isHLS && (strings.Contains(m, "http 500") || strings.Contains(m, "http 502") || strings.Contains(m, "http 503") || strings.Contains(m, "http 504")) { return true } return false } func appLogFilterText(text string) string { lines := strings.Split(text, "\n") out := make([]string, 0, len(lines)) for _, line := range lines { if appLogLineSuppressed(line) { continue } out = append(out, line) } return strings.Join(out, "\n") } func appErrorLogSuppressed(msg string) bool { m := strings.ToLower(msg) return appLogLineSuppressed(msg) || strings.Contains(m, "leere hls url") } func appErrorf(format string, args ...any) error { err := fmt.Errorf(format, args...) if !appErrorLogSuppressed(err.Error()) { appLogln("❌", err) } return err } // Nur zentraler Ersatz für fmt.Sprintf. // Loggt absichtlich NICHT, weil Sprintf oft nur UI-/Command-Strings baut. func appSprintf(format string, args ...any) string { return fmt.Sprintf(format, args...) } // Zentraler Ersatz für fmt.Sprint. func appSprint(args ...any) string { return fmt.Sprint(args...) } func appLogHandler(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { http.Error(w, "Nur GET erlaubt", http.StatusMethodNotAllowed) return } path := appLogPath() maxBytes := int64(64 * 1024) switch strings.TrimSpace(r.URL.Query().Get("maxBytes")) { case "16384": maxBytes = 16 * 1024 case "32768": maxBytes = 32 * 1024 case "131072": maxBytes = 128 * 1024 case "262144": maxBytes = 256 * 1024 } info, err := os.Stat(path) if err != nil { if os.IsNotExist(err) { writeJSON(w, http.StatusOK, map[string]any{ "ok": false, "path": path, "exists": false, "log": "", "error": "Logdatei existiert noch nicht.", }) return } writeJSON(w, http.StatusOK, map[string]any{ "ok": false, "path": path, "exists": false, "log": "", "error": err.Error(), }) return } if info.IsDir() { writeJSON(w, http.StatusOK, map[string]any{ "ok": false, "path": path, "exists": true, "log": "", "error": "Logpfad ist ein Ordner.", }) return } f, err := os.Open(path) if err != nil { writeJSON(w, http.StatusOK, map[string]any{ "ok": false, "path": path, "exists": true, "log": "", "error": err.Error(), }) return } defer f.Close() size := info.Size() offset := int64(0) if size > maxBytes { offset = size - maxBytes } buf := make([]byte, size-offset) if len(buf) > 0 { if _, err := f.ReadAt(buf, offset); err != nil && err != io.EOF { writeJSON(w, http.StatusOK, map[string]any{ "ok": false, "path": path, "exists": true, "log": "", "error": err.Error(), }) return } } logText := appLogFilterText(string(buf)) if offset > 0 { logText = "… Log gekürzt, zeige die letzten " + fmt.Sprint(maxBytes/1024) + " KB …\n\n" + logText } writeJSON(w, http.StatusOK, map[string]any{ "ok": true, "path": path, "exists": true, "sizeBytes": size, "log": logText, }) } func clearAppLog() { appLogMu.Lock() defer appLogMu.Unlock() if appLogFile != nil { _ = appLogFile.Close() appLogFile = nil } path := appLogPath() if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil { fmt.Println("⚠️ recorder log dir konnte nicht erstellt werden:", err) return } if err := os.WriteFile(path, []byte{}, 0644); err != nil { fmt.Println("⚠️ recorder log konnte nicht geleert werden:", err) } }