platform: modularize api/gui, add docs-tests-web foundation, and refresh root config
This commit is contained in:
@@ -1,261 +1,10 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// trace: чтение + запись
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
//
|
||||
// EN: Trace log endpoints and helpers for GUI/operator visibility.
|
||||
// EN: Includes plain tail, filtered JSON views, append API, and bounded tail reader.
|
||||
// RU: Эндпоинты и хелперы trace-логов для GUI/оператора.
|
||||
// RU: Включает plain tail, фильтрованные JSON-режимы, append API и безопасный tail-reader.
|
||||
|
||||
func handleTraceTailPlain(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
lines := tailFile(traceLogPath, defaultTraceTailMax)
|
||||
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||
_, _ = io.WriteString(w, strings.Join(lines, "\n"))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// trace-json
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
// GET /api/v1/trace-json?mode=full|gui|events|smartdns
|
||||
func handleTraceJSON(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
mode := strings.ToLower(strings.TrimSpace(r.URL.Query().Get("mode")))
|
||||
if mode == "" {
|
||||
mode = "full"
|
||||
}
|
||||
if mode == "events" {
|
||||
mode = "gui"
|
||||
}
|
||||
|
||||
var lines []string
|
||||
|
||||
switch mode {
|
||||
case "smartdns":
|
||||
// чисто SmartDNS-лог
|
||||
lines = tailFile(smartdnsLogPath, defaultTraceTailMax)
|
||||
|
||||
case "gui":
|
||||
// Events: только человеко-читабельные события/ошибки/команды.
|
||||
full := tailFile(traceLogPath, defaultTraceTailMax)
|
||||
allow := []string{
|
||||
"[gui]", "[info]", "[login]", "[vpn]", "[event]", "[error]",
|
||||
}
|
||||
for _, l := range full {
|
||||
ll := strings.ToLower(l)
|
||||
|
||||
// берём только наши "человеческие" префиксы
|
||||
ok := false
|
||||
for _, a := range allow {
|
||||
if strings.Contains(ll, strings.ToLower(a)) {
|
||||
ok = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !ok {
|
||||
// если префикса нет, но это похоже на ошибку — тоже включаем
|
||||
if strings.Contains(ll, "error") || strings.Contains(ll, "failed") || strings.Contains(ll, "timeout") {
|
||||
ok = true
|
||||
}
|
||||
}
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
// режем шум от резолвера/маршрутов/массовых вставок
|
||||
if strings.Contains(ll, "smartdns") ||
|
||||
strings.Contains(ll, "resolver") ||
|
||||
strings.Contains(ll, "dnstt") ||
|
||||
strings.Contains(ll, "routes") ||
|
||||
strings.Contains(ll, "nft add element") ||
|
||||
strings.Contains(ll, "cache hit:") {
|
||||
continue
|
||||
}
|
||||
|
||||
lines = append(lines, l)
|
||||
}
|
||||
|
||||
default: // full
|
||||
// полный хвост trace.log без фильтрации
|
||||
lines = tailFile(traceLogPath, defaultTraceTailMax)
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, map[string]any{
|
||||
"lines": lines,
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// 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)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// tail helper
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
const defaultTailMaxBytes = 512 * 1024
|
||||
|
||||
func tailFile(path string, maxLines int) []string {
|
||||
if maxLines <= 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// читаем лимит из env, если задан
|
||||
maxBytes := defaultTailMaxBytes
|
||||
if env := os.Getenv("SVPN_TAIL_MAX_BYTES"); env != "" {
|
||||
if n, err := strconv.Atoi(env); err == nil && n > 0 {
|
||||
maxBytes = n
|
||||
}
|
||||
}
|
||||
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
// файла нет или нет прав — просто ничего не отдаём
|
||||
return nil
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
fi, err := f.Stat()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
size := fi.Size()
|
||||
if size <= 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// с какого смещения читаем хвост
|
||||
start := int64(0)
|
||||
if size > int64(maxBytes) {
|
||||
start = size - int64(maxBytes)
|
||||
}
|
||||
|
||||
// двигаем указатель в файле
|
||||
if _, err := f.Seek(start, io.SeekStart); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// читаем хвост
|
||||
data, err := io.ReadAll(f)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// режем по строкам
|
||||
lines := strings.Split(string(data), "\n")
|
||||
|
||||
// если мы начали читать с середины файла (start > 0),
|
||||
// первая строка почти наверняка обрезана — выбрасываем её.
|
||||
if start > 0 && len(lines) > 0 {
|
||||
lines = lines[1:]
|
||||
}
|
||||
|
||||
// убираем финальную пустую строку, если есть
|
||||
if n := len(lines); n > 0 && lines[n-1] == "" {
|
||||
lines = lines[:n-1]
|
||||
}
|
||||
|
||||
// берём только последние maxLines
|
||||
if len(lines) > maxLines {
|
||||
lines = lines[len(lines)-maxLines:]
|
||||
}
|
||||
|
||||
return lines
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user