Files
svc-proxy/internal/bridge/stats.go
T
claude-timemachine 41a7e39754
CI / validate (push) Successful in 7s
CI / docker (push) Failing after 5s
rename: bridges/valves → tunnels (one term across types + API + UI)
API shape:
  GET /api/connections → GET /api/tunnels
  body: {"connections": […]} → {"tunnels": […]}

Type rename (package stays "bridge" — internal):
  Valve         → Listener
  clientBridge  → tunnel
  ConnSnapshot  → TunnelSnapshot

Log messages mirror the new vocab ("listener open/close", "tunnel
open/idle evict/forward failed"). UI header is now "Active tunnels"
and the empty state reads "no active tunnels".

server-manager's dashboard polls /infra/svc-proxy/api/tunnels and
shows "N tunnels" on the svc-proxy infra card.

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

67 lines
2.1 KiB
Go

package bridge
import (
"sync/atomic"
"time"
)
// counters is the per-tunnel byte tally. Updated from the two hot paths
// (readLoop client→backend, readBackend backend→client) — atomic to avoid
// locking the tunnel for every datagram.
type counters struct {
bytesUp atomic.Uint64 // client → backend
bytesDown atomic.Uint64 // backend → client
}
// TunnelSnapshot is one row of the tunnels table the UI renders. All times
// are wall-clock; sizes are total bytes since the tunnel opened.
type TunnelSnapshot struct {
Server string `json:"server"` // pg row name (e.g. "gtnh")
Port int `json:"port"` // public UDP port
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"` // tunnel 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 per-client tunnel across all
// listeners. Cheap-ish: takes the Manager lock + each Listener lock briefly,
// no per-tunnel lock (counters are atomic; LastSeen is read under the
// tunnel lock).
func (m *Manager) Snapshot() []TunnelSnapshot {
m.mu.Lock()
listeners := make([]*Listener, 0, len(m.listeners))
for _, l := range m.listeners {
listeners = append(listeners, l)
}
m.mu.Unlock()
now := time.Now()
var out []TunnelSnapshot
for _, l := range listeners {
l.mu.Lock()
for _, t := range l.tunnels {
t.mu.Lock()
lastSeen := t.lastSeen
opened := t.openedAt
t.mu.Unlock()
out = append(out, TunnelSnapshot{
Server: l.route.Name,
Port: l.route.Port,
Backend: l.route.Address,
Client: t.client.String(),
BytesUp: t.counters.bytesUp.Load(),
BytesDown: t.counters.bytesDown.Load(),
OpenedAt: opened,
LastSeen: lastSeen,
IdleSeconds: now.Sub(lastSeen).Seconds(),
})
}
l.mu.Unlock()
}
return out
}