Highly-opinionated (ex-bullshit-free) MTPROTO proxy for Telegram. If you use v1.0 or upgrade broke you proxy, please read the chapter Version 2
Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.

proxy_stats.go 4.4KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179
  1. package mtglib
  2. import (
  3. "context"
  4. "encoding/json"
  5. "net"
  6. "net/http"
  7. "sync"
  8. "sync/atomic"
  9. "time"
  10. )
  11. type secretStats struct {
  12. connections atomic.Int64
  13. bytesIn atomic.Int64
  14. bytesOut atomic.Int64
  15. lastSeen atomic.Value // stores time.Time
  16. }
  17. // ProxyStats tracks per-secret connection stats with atomic counters.
  18. // Thread-safe for concurrent access from proxy goroutines.
  19. type ProxyStats struct {
  20. mu sync.RWMutex
  21. users map[string]*secretStats
  22. startedAt time.Time
  23. }
  24. // NewProxyStats creates a new ProxyStats instance.
  25. func NewProxyStats() *ProxyStats {
  26. return &ProxyStats{
  27. users: make(map[string]*secretStats),
  28. startedAt: time.Now(),
  29. }
  30. }
  31. func (s *ProxyStats) getOrCreate(name string) *secretStats {
  32. s.mu.RLock()
  33. st, ok := s.users[name]
  34. s.mu.RUnlock()
  35. if ok {
  36. return st
  37. }
  38. s.mu.Lock()
  39. defer s.mu.Unlock()
  40. if st, ok = s.users[name]; ok {
  41. return st
  42. }
  43. st = &secretStats{}
  44. st.lastSeen.Store(time.Time{})
  45. s.users[name] = st
  46. return st
  47. }
  48. // PreRegister adds a secret name to the stats map so it appears in output
  49. // even if no connections have been made yet.
  50. func (s *ProxyStats) PreRegister(name string) {
  51. s.getOrCreate(name)
  52. }
  53. // OnConnect increments the active connection count for the given secret.
  54. func (s *ProxyStats) OnConnect(name string) {
  55. s.getOrCreate(name).connections.Add(1)
  56. }
  57. // OnDisconnect decrements the active connection count for the given secret.
  58. func (s *ProxyStats) OnDisconnect(name string) {
  59. s.getOrCreate(name).connections.Add(-1)
  60. }
  61. // AddBytesIn adds to the bytes-in counter for the given secret.
  62. func (s *ProxyStats) AddBytesIn(name string, n int64) {
  63. s.getOrCreate(name).bytesIn.Add(n)
  64. }
  65. // AddBytesOut adds to the bytes-out counter for the given secret.
  66. func (s *ProxyStats) AddBytesOut(name string, n int64) {
  67. s.getOrCreate(name).bytesOut.Add(n)
  68. }
  69. // UpdateLastSeen sets the last-seen timestamp for the given secret to now.
  70. func (s *ProxyStats) UpdateLastSeen(name string) {
  71. s.getOrCreate(name).lastSeen.Store(time.Now())
  72. }
  73. // StatsResponse is the JSON response for the stats endpoint.
  74. type StatsResponse struct {
  75. StartedAt time.Time `json:"started_at"`
  76. UptimeSeconds int64 `json:"uptime_seconds"`
  77. TotalConnections int64 `json:"total_connections"`
  78. Users map[string]UserStatsJSON `json:"users"`
  79. }
  80. // UserStatsJSON is the per-user portion of the stats JSON response.
  81. type UserStatsJSON struct {
  82. Connections int64 `json:"connections"`
  83. BytesIn int64 `json:"bytes_in"`
  84. BytesOut int64 `json:"bytes_out"`
  85. LastSeen *time.Time `json:"last_seen"`
  86. }
  87. func (s *ProxyStats) ServeHTTP(w http.ResponseWriter, r *http.Request) {
  88. s.mu.RLock()
  89. defer s.mu.RUnlock()
  90. var totalConns int64
  91. users := make(map[string]UserStatsJSON, len(s.users))
  92. for name, st := range s.users {
  93. conns := st.connections.Load()
  94. totalConns += conns
  95. lastSeen := st.lastSeen.Load().(time.Time) //nolint: forcetypeassert
  96. var lastSeenPtr *time.Time
  97. if !lastSeen.IsZero() {
  98. lastSeenPtr = &lastSeen
  99. }
  100. users[name] = UserStatsJSON{
  101. Connections: conns,
  102. BytesIn: st.bytesIn.Load(),
  103. BytesOut: st.bytesOut.Load(),
  104. LastSeen: lastSeenPtr,
  105. }
  106. }
  107. resp := StatsResponse{
  108. StartedAt: s.startedAt,
  109. UptimeSeconds: int64(time.Since(s.startedAt).Seconds()),
  110. TotalConnections: totalConns,
  111. Users: users,
  112. }
  113. w.Header().Set("Content-Type", "application/json")
  114. if err := json.NewEncoder(w).Encode(resp); err != nil {
  115. http.Error(w, err.Error(), http.StatusInternalServerError)
  116. }
  117. }
  118. // StartServer starts an HTTP server for the stats API in a background goroutine.
  119. // The server is shut down when ctx is cancelled.
  120. func (s *ProxyStats) StartServer(ctx context.Context, bindTo string, logger Logger) {
  121. mux := http.NewServeMux()
  122. mux.Handle("/stats", s)
  123. srv := &http.Server{
  124. Addr: bindTo,
  125. Handler: mux,
  126. }
  127. ln, err := net.Listen("tcp", bindTo)
  128. if err != nil {
  129. logger.WarningError("cannot start stats API listener", err)
  130. return
  131. }
  132. go func() {
  133. if err := srv.Serve(ln); err != nil && err != http.ErrServerClosed {
  134. logger.WarningError("stats API server error", err)
  135. }
  136. }()
  137. go func() {
  138. <-ctx.Done()
  139. shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second) //nolint: mnd
  140. defer cancel()
  141. srv.Shutdown(shutdownCtx) //nolint: errcheck
  142. }()
  143. logger.BindStr("bind", bindTo).Info("Stats API server started")
  144. }