| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179 |
- package mtglib
-
- import (
- "context"
- "encoding/json"
- "net"
- "net/http"
- "sync"
- "sync/atomic"
- "time"
- )
-
- type secretStats struct {
- connections atomic.Int64
- bytesIn atomic.Int64
- bytesOut atomic.Int64
- lastSeen atomic.Value // stores time.Time
- }
-
- // ProxyStats tracks per-secret connection stats with atomic counters.
- // Thread-safe for concurrent access from proxy goroutines.
- type ProxyStats struct {
- mu sync.RWMutex
- users map[string]*secretStats
- startedAt time.Time
- }
-
- // NewProxyStats creates a new ProxyStats instance.
- func NewProxyStats() *ProxyStats {
- return &ProxyStats{
- users: make(map[string]*secretStats),
- startedAt: time.Now(),
- }
- }
-
- func (s *ProxyStats) getOrCreate(name string) *secretStats {
- s.mu.RLock()
- st, ok := s.users[name]
- s.mu.RUnlock()
-
- if ok {
- return st
- }
-
- s.mu.Lock()
- defer s.mu.Unlock()
-
- if st, ok = s.users[name]; ok {
- return st
- }
-
- st = &secretStats{}
- st.lastSeen.Store(time.Time{})
- s.users[name] = st
-
- return st
- }
-
- // PreRegister adds a secret name to the stats map so it appears in output
- // even if no connections have been made yet.
- func (s *ProxyStats) PreRegister(name string) {
- s.getOrCreate(name)
- }
-
- // OnConnect increments the active connection count for the given secret.
- func (s *ProxyStats) OnConnect(name string) {
- s.getOrCreate(name).connections.Add(1)
- }
-
- // OnDisconnect decrements the active connection count for the given secret.
- func (s *ProxyStats) OnDisconnect(name string) {
- s.getOrCreate(name).connections.Add(-1)
- }
-
- // AddBytesIn adds to the bytes-in counter for the given secret.
- func (s *ProxyStats) AddBytesIn(name string, n int64) {
- s.getOrCreate(name).bytesIn.Add(n)
- }
-
- // AddBytesOut adds to the bytes-out counter for the given secret.
- func (s *ProxyStats) AddBytesOut(name string, n int64) {
- s.getOrCreate(name).bytesOut.Add(n)
- }
-
- // UpdateLastSeen sets the last-seen timestamp for the given secret to now.
- func (s *ProxyStats) UpdateLastSeen(name string) {
- s.getOrCreate(name).lastSeen.Store(time.Now())
- }
-
- // StatsResponse is the JSON response for the stats endpoint.
- type StatsResponse struct {
- StartedAt time.Time `json:"started_at"`
- UptimeSeconds int64 `json:"uptime_seconds"`
- TotalConnections int64 `json:"total_connections"`
- Users map[string]UserStatsJSON `json:"users"`
- }
-
- // UserStatsJSON is the per-user portion of the stats JSON response.
- type UserStatsJSON struct {
- Connections int64 `json:"connections"`
- BytesIn int64 `json:"bytes_in"`
- BytesOut int64 `json:"bytes_out"`
- LastSeen *time.Time `json:"last_seen"`
- }
-
- func (s *ProxyStats) ServeHTTP(w http.ResponseWriter, r *http.Request) {
- s.mu.RLock()
- defer s.mu.RUnlock()
-
- var totalConns int64
-
- users := make(map[string]UserStatsJSON, len(s.users))
-
- for name, st := range s.users {
- conns := st.connections.Load()
- totalConns += conns
-
- lastSeen := st.lastSeen.Load().(time.Time) //nolint: forcetypeassert
- var lastSeenPtr *time.Time
- if !lastSeen.IsZero() {
- lastSeenPtr = &lastSeen
- }
-
- users[name] = UserStatsJSON{
- Connections: conns,
- BytesIn: st.bytesIn.Load(),
- BytesOut: st.bytesOut.Load(),
- LastSeen: lastSeenPtr,
- }
- }
-
- resp := StatsResponse{
- StartedAt: s.startedAt,
- UptimeSeconds: int64(time.Since(s.startedAt).Seconds()),
- TotalConnections: totalConns,
- Users: users,
- }
-
- w.Header().Set("Content-Type", "application/json")
-
- if err := json.NewEncoder(w).Encode(resp); err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- }
- }
-
- // StartServer starts an HTTP server for the stats API in a background goroutine.
- // The server is shut down when ctx is cancelled.
- func (s *ProxyStats) StartServer(ctx context.Context, bindTo string, logger Logger) {
- mux := http.NewServeMux()
- mux.Handle("/stats", s)
-
- srv := &http.Server{
- Addr: bindTo,
- Handler: mux,
- }
-
- ln, err := net.Listen("tcp", bindTo)
- if err != nil {
- logger.WarningError("cannot start stats API listener", err)
- return
- }
-
- go func() {
- if err := srv.Serve(ln); err != nil && err != http.ErrServerClosed {
- logger.WarningError("stats API server error", err)
- }
- }()
-
- go func() {
- <-ctx.Done()
-
- shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second) //nolint: mnd
- defer cancel()
-
- srv.Shutdown(shutdownCtx) //nolint: errcheck
- }()
-
- logger.BindStr("bind", bindTo).Info("Stats API server started")
- }
|