Files
elmprodvpn/selective-vpn-api/app/trace_handlers_write.go

174 lines
4.5 KiB
Go

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
}
}
}