stats.go 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243
  1. // Copyright (C) 2018 Audrius Butkevicius and Contributors (see the CONTRIBUTORS file).
  2. package main
  3. import (
  4. "encoding/json"
  5. "net"
  6. "net/http"
  7. "sync"
  8. "time"
  9. "github.com/prometheus/client_golang/prometheus"
  10. )
  11. var (
  12. statusClient = http.Client{
  13. Timeout: 5 * time.Second,
  14. }
  15. apiRequestsTotal = makeCounter("api_requests_total", "Number of API requests.", "type", "result")
  16. apiRequestsSeconds = makeSummary("api_requests_seconds", "Latency of API requests.", "type")
  17. relayTestsTotal = makeCounter("tests_total", "Number of relay tests.", "result")
  18. relayTestActionsSeconds = makeSummary("test_actions_seconds", "Latency of relay test actions.", "type")
  19. locationLookupSeconds = makeSummary("location_lookup_seconds", "Latency of location lookups.").WithLabelValues()
  20. metricsRequestsSeconds = makeSummary("metrics_requests_seconds", "Latency of metric requests.").WithLabelValues()
  21. scrapeSeconds = makeSummary("relay_scrape_seconds", "Latency of metric scrapes from remote relays.", "result")
  22. relayUptime = makeGauge("relay_uptime", "Uptime of relay", "relay")
  23. relayPendingSessionKeys = makeGauge("relay_pending_session_keys", "Number of pending session keys (two keys per session, one per each side of the connection)", "relay")
  24. relayActiveSessions = makeGauge("relay_active_sessions", "Number of sessions that are happening, a session contains two parties", "relay")
  25. relayConnections = makeGauge("relay_connections", "Number of devices connected to the relay", "relay")
  26. relayProxies = makeGauge("relay_proxies", "Number of active proxy routines sending data between peers (two proxies per session, one for each way)", "relay")
  27. relayBytesProxied = makeGauge("relay_bytes_proxied", "Number of bytes proxied by the relay", "relay")
  28. relayGoRoutines = makeGauge("relay_go_routines", "Number of Go routines in the process", "relay")
  29. relaySessionRate = makeGauge("relay_session_rate", "Rate applied per session", "relay")
  30. relayGlobalRate = makeGauge("relay_global_rate", "Global rate applied on the whole relay", "relay")
  31. relayBuildInfo = makeGauge("relay_build_info", "Build information about a relay", "relay", "go_version", "go_os", "go_arch")
  32. relayLocationInfo = makeGauge("relay_location_info", "Location information about a relay", "relay", "city", "country", "continent")
  33. lastStats = make(map[string]stats)
  34. )
  35. func makeGauge(name string, help string, labels ...string) *prometheus.GaugeVec {
  36. gauge := prometheus.NewGaugeVec(
  37. prometheus.GaugeOpts{
  38. Namespace: "syncthing",
  39. Subsystem: "relaypoolsrv",
  40. Name: name,
  41. Help: help,
  42. },
  43. labels,
  44. )
  45. prometheus.MustRegister(gauge)
  46. return gauge
  47. }
  48. func makeSummary(name string, help string, labels ...string) *prometheus.SummaryVec {
  49. summary := prometheus.NewSummaryVec(
  50. prometheus.SummaryOpts{
  51. Namespace: "syncthing",
  52. Subsystem: "relaypoolsrv",
  53. Name: name,
  54. Help: help,
  55. Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001},
  56. },
  57. labels,
  58. )
  59. prometheus.MustRegister(summary)
  60. return summary
  61. }
  62. func makeCounter(name string, help string, labels ...string) *prometheus.CounterVec {
  63. counter := prometheus.NewCounterVec(
  64. prometheus.CounterOpts{
  65. Namespace: "syncthing",
  66. Subsystem: "relaypoolsrv",
  67. Name: name,
  68. Help: help,
  69. },
  70. labels,
  71. )
  72. prometheus.MustRegister(counter)
  73. return counter
  74. }
  75. func statsRefresher(interval time.Duration) {
  76. ticker := time.NewTicker(interval)
  77. for range ticker.C {
  78. refreshStats()
  79. }
  80. }
  81. type statsFetchResult struct {
  82. relay *relay
  83. stats *stats
  84. }
  85. func refreshStats() {
  86. mut.RLock()
  87. relays := append(permanentRelays, knownRelays...)
  88. mut.RUnlock()
  89. now := time.Now()
  90. var wg sync.WaitGroup
  91. results := make(chan statsFetchResult, len(relays))
  92. for _, rel := range relays {
  93. wg.Go(func() {
  94. t0 := time.Now()
  95. stats := fetchStats(rel)
  96. duration := time.Since(t0).Seconds()
  97. result := "success"
  98. if stats == nil {
  99. result = "failed"
  100. }
  101. scrapeSeconds.WithLabelValues(result).Observe(duration)
  102. results <- statsFetchResult{
  103. relay: rel,
  104. stats: fetchStats(rel),
  105. }
  106. })
  107. }
  108. wg.Wait()
  109. close(results)
  110. mut.Lock()
  111. relayBuildInfo.Reset()
  112. relayLocationInfo.Reset()
  113. for result := range results {
  114. result.relay.StatsRetrieved = now
  115. result.relay.Stats = result.stats
  116. if result.stats == nil {
  117. deleteMetrics(result.relay.uri.Host)
  118. } else {
  119. updateMetrics(result.relay.uri.Host, *result.stats, result.relay.Location)
  120. }
  121. }
  122. mut.Unlock()
  123. }
  124. func fetchStats(relay *relay) *stats {
  125. statusAddr := relay.uri.Query().Get("statusAddr")
  126. if statusAddr == "" {
  127. statusAddr = ":22070"
  128. }
  129. statusHost, statusPort, err := net.SplitHostPort(statusAddr)
  130. if err != nil {
  131. return nil
  132. }
  133. if statusHost == "" {
  134. if host, _, err := net.SplitHostPort(relay.uri.Host); err != nil {
  135. return nil
  136. } else {
  137. statusHost = host
  138. }
  139. }
  140. url := "http://" + net.JoinHostPort(statusHost, statusPort) + "/status"
  141. response, err := statusClient.Get(url)
  142. if err != nil {
  143. return nil
  144. }
  145. var stats stats
  146. if err := json.NewDecoder(response.Body).Decode(&stats); err != nil {
  147. return nil
  148. }
  149. return &stats
  150. }
  151. func updateMetrics(host string, stats stats, location location) {
  152. if stats.GoVersion != "" || stats.GoOS != "" || stats.GoArch != "" {
  153. relayBuildInfo.WithLabelValues(host, stats.GoVersion, stats.GoOS, stats.GoArch).Add(1)
  154. }
  155. if location.City != "" || location.Country != "" || location.Continent != "" {
  156. relayLocationInfo.WithLabelValues(host, location.City, location.Country, location.Continent).Add(1)
  157. }
  158. if lastStat, ok := lastStats[host]; ok {
  159. stats = mergeStats(stats, lastStat)
  160. }
  161. relayUptime.WithLabelValues(host).Set(float64(stats.UptimeSeconds))
  162. relayPendingSessionKeys.WithLabelValues(host).Set(float64(stats.PendingSessionKeys))
  163. relayActiveSessions.WithLabelValues(host).Set(float64(stats.ActiveSessions))
  164. relayConnections.WithLabelValues(host).Set(float64(stats.Connections))
  165. relayProxies.WithLabelValues(host).Set(float64(stats.Proxies))
  166. relayBytesProxied.WithLabelValues(host).Set(float64(stats.BytesProxied))
  167. relayGoRoutines.WithLabelValues(host).Set(float64(stats.GoRoutines))
  168. relaySessionRate.WithLabelValues(host).Set(float64(stats.Options.SessionRate))
  169. relayGlobalRate.WithLabelValues(host).Set(float64(stats.Options.GlobalRate))
  170. lastStats[host] = stats
  171. }
  172. func deleteMetrics(host string) {
  173. relayUptime.DeleteLabelValues(host)
  174. relayPendingSessionKeys.DeleteLabelValues(host)
  175. relayActiveSessions.DeleteLabelValues(host)
  176. relayConnections.DeleteLabelValues(host)
  177. relayProxies.DeleteLabelValues(host)
  178. relayBytesProxied.DeleteLabelValues(host)
  179. relayGoRoutines.DeleteLabelValues(host)
  180. relaySessionRate.DeleteLabelValues(host)
  181. relayGlobalRate.DeleteLabelValues(host)
  182. delete(lastStats, host)
  183. }
  184. // Due to some unexplainable behaviour, some of the numbers sometimes travel slightly backwards (by less than 1%)
  185. // This happens between scrapes, which is 30s, so this can't be a race.
  186. // This causes prometheus to assume a "rate reset", hence causes phenomenal spikes.
  187. // One of the number that moves backwards is BytesProxied, which atomically increments a counter with numeric value
  188. // returned by net.Conn.Read(). I don't think that can return a negative value, so I have no idea what's going on.
  189. func mergeStats(new stats, old stats) stats {
  190. new.UptimeSeconds = mergeValue(new.UptimeSeconds, old.UptimeSeconds)
  191. new.PendingSessionKeys = mergeValue(new.PendingSessionKeys, old.PendingSessionKeys)
  192. new.ActiveSessions = mergeValue(new.ActiveSessions, old.ActiveSessions)
  193. new.Connections = mergeValue(new.Connections, old.Connections)
  194. new.Proxies = mergeValue(new.Proxies, old.Proxies)
  195. new.BytesProxied = mergeValue(new.BytesProxied, old.BytesProxied)
  196. new.GoRoutines = mergeValue(new.GoRoutines, old.GoRoutines)
  197. new.Options.SessionRate = mergeValue(new.Options.SessionRate, old.Options.SessionRate)
  198. new.Options.GlobalRate = mergeValue(new.Options.GlobalRate, old.Options.GlobalRate)
  199. return new
  200. }
  201. func mergeValue(new, old int) int {
  202. if new >= old {
  203. return new // normal increase
  204. }
  205. if float64(new) > 0.99*float64(old) {
  206. return old // slight backward movement
  207. }
  208. return new // reset (relay restart)
  209. }