Files
svc-proxy/internal/bridge/stats.go
T
claude-timemachine f823c05aa3
CI / validate (push) Successful in 24s
CI / docker (push) Failing after 1m49s
initial: svc-proxy — UDP valve for Simple Voice Chat
Standalone Go service that routes SVC client traffic to per-server
backend voice endpoints, configured via pg LISTEN/NOTIFY (same channel
mc-router subscribes to). Each pg `servers` row with both
`voice_address` and `voice_proxy_port` set spawns a Valve: a public
UDP listener that maintains per-client ephemeral bridges to the
backend's SVC port.

Pieces:
  cmd/svc-proxy/main.go     entry; wires config, log fan-out,
                            bridge.Manager, pgsync, httpsrv
  internal/config/          DATABASE_URL + BIND_HOST +
                            BRIDGE_IDLE_TTL (default 1m) +
                            HTTP_ADDR (default :8081)
  internal/pgsync/          LISTEN automc_routes_changed; diff
                            desired/actual routes; emit Apply()
  internal/bridge/          Valve per public port; per-client
                            bridge with atomic up/down byte counters;
                            idle eviction every 15s against TTL
  internal/httpsrv/         operator UI — embedded single-page HTML
                            with active-connections table polled
                            every 1s + SSE log stream
                            (last 500 lines backlog on connect)

Reverse-proxied behind server-manager at /infra/svc-proxy/* — bind
internal-only addresses for production; auth is the dashboard's
Basic gate.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-10 18:01:04 +02:00

66 lines
2.1 KiB
Go

package bridge
import (
"sync/atomic"
"time"
)
// counters is the per-bridge byte tally. Updated from the two hot paths
// (readLoop client→backend, readBackend backend→client) — atomic to avoid
// locking the bridge for every datagram.
type counters struct {
bytesUp atomic.Uint64 // client → backend
bytesDown atomic.Uint64 // backend → client
}
// ConnSnapshot is one row of the active-connections table the UI renders.
// All times are wall-clock; sizes are total bytes since the bridge opened.
type ConnSnapshot struct {
Server string `json:"server"` // pg row name (e.g. "gtnh")
Port int `json:"port"` // public UDP port (the valve)
Backend string `json:"backend"` // backend addr
Client string `json:"client"` // source IP:port
BytesUp uint64 `json:"bytes_up"` // client → backend
BytesDown uint64 `json:"bytes_down"` // backend → client
OpenedAt time.Time `json:"opened_at"` // bridge creation
LastSeen time.Time `json:"last_seen"` // most-recent datagram either direction
IdleSeconds float64 `json:"idle_seconds"` // derived; UI sorts by this
}
// Snapshot returns one row per active client bridge across all valves.
// Cheap-ish: takes the Manager lock + each Valve lock briefly, no per-bridge
// lock (counters are atomic; LastSeen is read under the bridge lock).
func (m *Manager) Snapshot() []ConnSnapshot {
m.mu.Lock()
valves := make([]*Valve, 0, len(m.valves))
for _, v := range m.valves {
valves = append(valves, v)
}
m.mu.Unlock()
now := time.Now()
var out []ConnSnapshot
for _, v := range valves {
v.mu.Lock()
for _, b := range v.bridges {
b.mu.Lock()
lastSeen := b.lastSeen
opened := b.openedAt
b.mu.Unlock()
out = append(out, ConnSnapshot{
Server: v.route.Name,
Port: v.route.Port,
Backend: v.route.Address,
Client: b.client.String(),
BytesUp: b.counters.bytesUp.Load(),
BytesDown: b.counters.bytesDown.Load(),
OpenedAt: opened,
LastSeen: lastSeen,
IdleSeconds: now.Sub(lastSeen).Seconds(),
})
}
v.mu.Unlock()
}
return out
}