package app import ( "encoding/json" "fmt" "io" "net/http" "os" "strings" "sync" "time" ) // --------------------------------------------------------------------- // trace append // --------------------------------------------------------------------- // POST /api/v1/trace/append { "kind": "gui|smartdns|info", "line": "..." } func handleTraceAppend(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } var body struct { Kind string `json:"kind"` Line string `json:"line"` } if r.Body != nil { defer r.Body.Close() _ = json.NewDecoder(io.LimitReader(r.Body, 1<<20)).Decode(&body) } kind := strings.ToLower(strings.TrimSpace(body.Kind)) line := strings.TrimRight(body.Line, "\r\n") if line == "" { writeJSON(w, http.StatusOK, map[string]any{"ok": true}) return } _ = os.MkdirAll(stateDir, 0o755) switch kind { case "smartdns": appendTraceLineTo(smartdnsLogPath, "smartdns", line) case "gui": appendTraceLineTo(traceLogPath, "gui", line) default: appendTraceLineTo(traceLogPath, "info", line) } events.push("trace_append", map[string]any{ "kind": kind, }) writeJSON(w, http.StatusOK, map[string]any{"ok": true}) } // --------------------------------------------------------------------- // trace write helpers // --------------------------------------------------------------------- func appendTraceLineTo(path, prefix, line string) { line = strings.TrimRight(line, "\r\n") if line == "" { return } ts := time.Now().UTC().Format(time.RFC3339) _ = os.MkdirAll(stateDir, 0o755) // простейший "ручной логротейт" const maxSize = 10 * 1024 * 1024 // 10 МБ if fi, err := os.Stat(path); err == nil && fi.Size() > maxSize { // можно просто truncate _ = os.Truncate(path, 0) // или переименовать в *.1 и начать новый // _ = os.Rename(path, path+".1") } f, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644) if err != nil { return } defer f.Close() _, _ = fmt.Fprintf(f, "[%s] %s %s\n", prefix, ts, line) } // --------------------------------------------------------------------- // EN: `appendTraceLine` appends or adds trace line to an existing state. // RU: `appendTraceLine` - добавляет trace line в текущее состояние. // --------------------------------------------------------------------- func appendTraceLine(prefix, line string) { appendTraceLineTo(traceLogPath, prefix, line) } const ( traceRateLimitDefaultWindow = 20 * time.Second traceRateLimitMaxEntries = 1024 ) type traceRateLimitState struct { lastAt time.Time suppressed int } var ( traceRateLimitMu sync.Mutex traceRateLimitCache = map[string]traceRateLimitState{} ) // appendTraceLineRateLimited keeps first line in the window and suppresses exact duplicates. // When window is over, it writes one compact summary for suppressed duplicates. func appendTraceLineRateLimited(prefix, line string, window time.Duration) { line = strings.TrimSpace(strings.TrimRight(line, "\r\n")) if line == "" { return } if window <= 0 { window = traceRateLimitDefaultWindow } now := time.Now().UTC() key := strings.TrimSpace(prefix) + "\n" + line var summary string writeCurrent := true traceRateLimitMu.Lock() st, ok := traceRateLimitCache[key] if ok && !st.lastAt.IsZero() && now.Sub(st.lastAt) < window { st.suppressed++ traceRateLimitCache[key] = st writeCurrent = false } else { if ok && st.suppressed > 0 { summary = fmt.Sprintf("trace dedup: suppressed=%d within=%s line=%q", st.suppressed, window.String(), line) } st.lastAt = now st.suppressed = 0 traceRateLimitCache[key] = st } traceRateLimitShrinkLocked(now) traceRateLimitMu.Unlock() if summary != "" { appendTraceLineTo(traceLogPath, prefix, summary) } if writeCurrent { appendTraceLineTo(traceLogPath, prefix, line) } } func traceRateLimitShrinkLocked(now time.Time) { if len(traceRateLimitCache) <= traceRateLimitMaxEntries { return } cutoff := now.Add(-10 * time.Minute) for key, st := range traceRateLimitCache { if st.lastAt.Before(cutoff) && st.suppressed == 0 { delete(traceRateLimitCache, key) } } if len(traceRateLimitCache) <= traceRateLimitMaxEntries { return } overflow := len(traceRateLimitCache) - traceRateLimitMaxEntries for key := range traceRateLimitCache { delete(traceRateLimitCache, key) overflow-- if overflow <= 0 { break } } }