2026-06-26 16:31:08 +02:00

417 lines
8.2 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// backend\log.go
package main
import (
"fmt"
"io"
"log"
"net/http"
"os"
"path/filepath"
"runtime/debug"
"strings"
"sync"
"time"
)
var (
appLogMu sync.Mutex
appLogFile *os.File
appConsoleOut *os.File
appStdStreamsRedirected bool
appDiagnosticsConfigured bool
)
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
configureAppDiagnosticsLocked()
_, _ = fmt.Fprintf(
appLogFile,
"\n\n--- Recorder Start %s ---\nlog=%s\n",
time.Now().Format(time.RFC3339),
appLogPath(),
)
_ = appLogFile.Sync()
}
func configureAppDiagnosticsLocked() {
if appLogFile == nil || appDiagnosticsConfigured {
return
}
log.SetOutput(appLogFile)
log.SetFlags(log.LstdFlags | log.Lmicroseconds | log.Lshortfile)
debug.SetTraceback("all")
appConsoleOut = os.Stdout
if err := redirectProcessStdStreamsToFile(appLogFile); err != nil {
_, _ = fmt.Fprintf(appLogFile, "⚠️ stdout/stderr konnte nicht auf recorder.log umgeleitet werden: %v\n", err)
} else {
appStdStreamsRedirected = true
}
appDiagnosticsConfigured = true
}
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()
if appStdStreamsRedirected {
if appConsoleOut != nil && appConsoleOut != appLogFile {
_, _ = appConsoleOut.WriteString(out)
}
} else {
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...))
}
func appLogMultiline(prefix string, text string) {
text = strings.TrimRight(text, "\r\n")
if text == "" {
return
}
for _, line := range strings.Split(text, "\n") {
appLogWriteLine(prefix + strings.TrimRight(line, "\r"))
}
}
func appLogPanic(scope string, recovered any) {
scope = strings.TrimSpace(scope)
if scope == "" {
scope = "unbekannt"
}
appLogln("❌ PANIC in "+scope+":", recovered)
appLogln("Stacktrace:")
appLogMultiline(" ", string(debug.Stack()))
}
func appRecoverPanic(scope string) bool {
if recovered := recover(); recovered != nil {
appLogPanic(scope, recovered)
return true
}
return false
}
func appGo(scope string, fn func()) {
go func() {
defer appRecoverPanic(scope)
fn()
}()
}
// 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 m == "not found" {
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)
}
}