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
Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.

proxy_stats.go 8.1KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336
  1. package mtglib
  2. import (
  3. "context"
  4. "encoding/json"
  5. "fmt"
  6. "net"
  7. "net/http"
  8. "sync"
  9. "sync/atomic"
  10. "time"
  11. )
  12. type secretStats struct {
  13. connections atomic.Int64
  14. bytesIn atomic.Int64
  15. bytesOut atomic.Int64
  16. lastSeen atomic.Value // stores time.Time
  17. }
  18. // ProxyStats tracks per-secret connection stats with atomic counters.
  19. // Thread-safe for concurrent access from proxy goroutines.
  20. type ProxyStats struct {
  21. mu sync.RWMutex
  22. users map[string]*secretStats
  23. startedAt time.Time
  24. // Throttle: per-user connection caps recomputed every throttleInterval.
  25. throttleMu sync.RWMutex
  26. throttleCaps map[string]int64
  27. throttleLimit int64
  28. throttleInterval time.Duration
  29. throttleActive atomic.Bool
  30. }
  31. // NewProxyStats creates a new ProxyStats instance.
  32. func NewProxyStats() *ProxyStats {
  33. return &ProxyStats{
  34. users: make(map[string]*secretStats),
  35. startedAt: time.Now(),
  36. }
  37. }
  38. func (s *ProxyStats) getOrCreate(name string) *secretStats {
  39. s.mu.RLock()
  40. st, ok := s.users[name]
  41. s.mu.RUnlock()
  42. if ok {
  43. return st
  44. }
  45. s.mu.Lock()
  46. defer s.mu.Unlock()
  47. if st, ok = s.users[name]; ok {
  48. return st
  49. }
  50. st = &secretStats{}
  51. st.lastSeen.Store(time.Time{})
  52. s.users[name] = st
  53. return st
  54. }
  55. // PreRegister adds a secret name to the stats map so it appears in output
  56. // even if no connections have been made yet.
  57. func (s *ProxyStats) PreRegister(name string) {
  58. s.getOrCreate(name)
  59. }
  60. // OnConnect increments the active connection count for the given secret.
  61. func (s *ProxyStats) OnConnect(name string) {
  62. s.getOrCreate(name).connections.Add(1)
  63. }
  64. // OnDisconnect decrements the active connection count for the given secret.
  65. func (s *ProxyStats) OnDisconnect(name string) {
  66. s.getOrCreate(name).connections.Add(-1)
  67. }
  68. // AddBytesIn adds to the bytes-in counter for the given secret.
  69. func (s *ProxyStats) AddBytesIn(name string, n int64) {
  70. s.getOrCreate(name).bytesIn.Add(n)
  71. }
  72. // AddBytesOut adds to the bytes-out counter for the given secret.
  73. func (s *ProxyStats) AddBytesOut(name string, n int64) {
  74. s.getOrCreate(name).bytesOut.Add(n)
  75. }
  76. // UpdateLastSeen sets the last-seen timestamp for the given secret to now.
  77. func (s *ProxyStats) UpdateLastSeen(name string) {
  78. s.getOrCreate(name).lastSeen.Store(time.Now())
  79. }
  80. // SetThrottle configures connection throttling. Must be called before
  81. // startThrottleLoop and before any connections arrive.
  82. func (s *ProxyStats) SetThrottle(limit int64, interval time.Duration) {
  83. s.throttleLimit = limit
  84. s.throttleInterval = interval
  85. s.throttleCaps = make(map[string]int64)
  86. }
  87. // CanConnect returns true if the user is allowed to open a new connection
  88. // under the current throttle caps. If throttling is not configured or the
  89. // user has no cap, it always returns true.
  90. func (s *ProxyStats) CanConnect(name string) bool {
  91. if s.throttleLimit == 0 {
  92. return true
  93. }
  94. s.throttleMu.RLock()
  95. cap, hasCap := s.throttleCaps[name]
  96. s.throttleMu.RUnlock()
  97. if !hasCap {
  98. return true
  99. }
  100. return s.getOrCreate(name).connections.Load() < cap
  101. }
  102. // startThrottleLoop runs a background goroutine that recomputes per-user
  103. // caps every throttleInterval.
  104. func (s *ProxyStats) startThrottleLoop(ctx context.Context, logger Logger) {
  105. go func() {
  106. ticker := time.NewTicker(s.throttleInterval)
  107. defer ticker.Stop()
  108. for {
  109. select {
  110. case <-ctx.Done():
  111. return
  112. case <-ticker.C:
  113. s.recomputeCaps(logger)
  114. }
  115. }
  116. }()
  117. logger.BindStr("limit", fmt.Sprintf("%d", s.throttleLimit)).
  118. BindStr("interval", s.throttleInterval.String()).
  119. Info("throttle loop started")
  120. }
  121. func (s *ProxyStats) recomputeCaps(logger Logger) {
  122. s.mu.RLock()
  123. userConns := make(map[string]int64, len(s.users))
  124. for name, st := range s.users {
  125. userConns[name] = st.connections.Load()
  126. }
  127. s.mu.RUnlock()
  128. caps := computeFairCaps(userConns, s.throttleLimit)
  129. wasActive := s.throttleActive.Load()
  130. nowActive := len(caps) > 0
  131. s.throttleMu.Lock()
  132. s.throttleCaps = caps
  133. s.throttleActive.Store(nowActive)
  134. s.throttleMu.Unlock()
  135. if nowActive && !wasActive {
  136. logger.Warning("throttle activated")
  137. } else if !nowActive && wasActive {
  138. logger.Info("throttle deactivated")
  139. }
  140. }
  141. // computeFairCaps implements the fair-share algorithm. Users below the equal
  142. // share keep their connections; remaining budget is split equally among the
  143. // rest. Returns nil when no throttling is needed.
  144. func computeFairCaps(userConns map[string]int64, limit int64) map[string]int64 {
  145. var total int64
  146. for _, c := range userConns {
  147. total += c
  148. }
  149. if total <= limit {
  150. return nil
  151. }
  152. remaining := make(map[string]int64, len(userConns))
  153. for k, v := range userConns {
  154. remaining[k] = v
  155. }
  156. budget := limit
  157. caps := make(map[string]int64)
  158. for len(remaining) > 0 {
  159. fairShare := budget / int64(len(remaining))
  160. changed := false
  161. for name, conns := range remaining {
  162. if conns <= fairShare {
  163. budget -= conns
  164. delete(remaining, name)
  165. changed = true
  166. }
  167. }
  168. if !changed {
  169. for name := range remaining {
  170. caps[name] = fairShare
  171. }
  172. break
  173. }
  174. }
  175. return caps
  176. }
  177. // StatsResponse is the JSON response for the stats endpoint.
  178. type StatsResponse struct {
  179. StartedAt time.Time `json:"started_at"`
  180. UptimeSeconds int64 `json:"uptime_seconds"`
  181. TotalConnections int64 `json:"total_connections"`
  182. Throttle *ThrottleJSON `json:"throttle,omitempty"`
  183. Users map[string]UserStatsJSON `json:"users"`
  184. }
  185. // ThrottleJSON is the throttle portion of the stats JSON response.
  186. type ThrottleJSON struct {
  187. Active bool `json:"active"`
  188. Limit int64 `json:"limit"`
  189. Caps map[string]int64 `json:"caps,omitempty"`
  190. }
  191. // UserStatsJSON is the per-user portion of the stats JSON response.
  192. type UserStatsJSON struct {
  193. Connections int64 `json:"connections"`
  194. BytesIn int64 `json:"bytes_in"`
  195. BytesOut int64 `json:"bytes_out"`
  196. LastSeen *time.Time `json:"last_seen"`
  197. }
  198. func (s *ProxyStats) ServeHTTP(w http.ResponseWriter, r *http.Request) {
  199. s.mu.RLock()
  200. defer s.mu.RUnlock()
  201. var totalConns int64
  202. users := make(map[string]UserStatsJSON, len(s.users))
  203. for name, st := range s.users {
  204. conns := st.connections.Load()
  205. totalConns += conns
  206. lastSeen := st.lastSeen.Load().(time.Time) //nolint: forcetypeassert
  207. var lastSeenPtr *time.Time
  208. if !lastSeen.IsZero() {
  209. lastSeenPtr = &lastSeen
  210. }
  211. users[name] = UserStatsJSON{
  212. Connections: conns,
  213. BytesIn: st.bytesIn.Load(),
  214. BytesOut: st.bytesOut.Load(),
  215. LastSeen: lastSeenPtr,
  216. }
  217. }
  218. var throttle *ThrottleJSON
  219. if s.throttleLimit > 0 {
  220. s.throttleMu.RLock()
  221. active := s.throttleActive.Load()
  222. var capsCopy map[string]int64
  223. if len(s.throttleCaps) > 0 {
  224. capsCopy = make(map[string]int64, len(s.throttleCaps))
  225. for k, v := range s.throttleCaps {
  226. capsCopy[k] = v
  227. }
  228. }
  229. s.throttleMu.RUnlock()
  230. throttle = &ThrottleJSON{
  231. Active: active,
  232. Limit: s.throttleLimit,
  233. Caps: capsCopy,
  234. }
  235. }
  236. resp := StatsResponse{
  237. StartedAt: s.startedAt,
  238. UptimeSeconds: int64(time.Since(s.startedAt).Seconds()),
  239. TotalConnections: totalConns,
  240. Throttle: throttle,
  241. Users: users,
  242. }
  243. w.Header().Set("Content-Type", "application/json")
  244. if err := json.NewEncoder(w).Encode(resp); err != nil {
  245. http.Error(w, err.Error(), http.StatusInternalServerError)
  246. }
  247. }
  248. // StartServer starts an HTTP server for the stats API in a background goroutine.
  249. // The server is shut down when ctx is cancelled.
  250. func (s *ProxyStats) StartServer(ctx context.Context, bindTo string, logger Logger) {
  251. mux := http.NewServeMux()
  252. mux.Handle("/stats", s)
  253. srv := &http.Server{
  254. Addr: bindTo,
  255. Handler: mux,
  256. }
  257. ln, err := net.Listen("tcp", bindTo)
  258. if err != nil {
  259. logger.WarningError("cannot start stats API listener", err)
  260. return
  261. }
  262. go func() {
  263. if err := srv.Serve(ln); err != nil && err != http.ErrServerClosed {
  264. logger.WarningError("stats API server error", err)
  265. }
  266. }()
  267. go func() {
  268. <-ctx.Done()
  269. shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second) //nolint: mnd
  270. defer cancel()
  271. srv.Shutdown(shutdownCtx) //nolint: errcheck
  272. }()
  273. logger.BindStr("bind", bindTo).Info("Stats API server started")
  274. }