stats.go 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245
  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.Add(1)
  94. go func(rel *relay) {
  95. t0 := time.Now()
  96. stats := fetchStats(rel)
  97. duration := time.Since(t0).Seconds()
  98. result := "success"
  99. if stats == nil {
  100. result = "failed"
  101. }
  102. scrapeSeconds.WithLabelValues(result).Observe(duration)
  103. results <- statsFetchResult{
  104. relay: rel,
  105. stats: fetchStats(rel),
  106. }
  107. wg.Done()
  108. }(rel)
  109. }
  110. wg.Wait()
  111. close(results)
  112. mut.Lock()
  113. relayBuildInfo.Reset()
  114. relayLocationInfo.Reset()
  115. for result := range results {
  116. result.relay.StatsRetrieved = now
  117. result.relay.Stats = result.stats
  118. if result.stats == nil {
  119. deleteMetrics(result.relay.uri.Host)
  120. } else {
  121. updateMetrics(result.relay.uri.Host, *result.stats, result.relay.Location)
  122. }
  123. }
  124. mut.Unlock()
  125. }
  126. func fetchStats(relay *relay) *stats {
  127. statusAddr := relay.uri.Query().Get("statusAddr")
  128. if statusAddr == "" {
  129. statusAddr = ":22070"
  130. }
  131. statusHost, statusPort, err := net.SplitHostPort(statusAddr)
  132. if err != nil {
  133. return nil
  134. }
  135. if statusHost == "" {
  136. if host, _, err := net.SplitHostPort(relay.uri.Host); err != nil {
  137. return nil
  138. } else {
  139. statusHost = host
  140. }
  141. }
  142. url := "http://" + net.JoinHostPort(statusHost, statusPort) + "/status"
  143. response, err := statusClient.Get(url)
  144. if err != nil {
  145. return nil
  146. }
  147. var stats stats
  148. if err := json.NewDecoder(response.Body).Decode(&stats); err != nil {
  149. return nil
  150. }
  151. return &stats
  152. }
  153. func updateMetrics(host string, stats stats, location location) {
  154. if stats.GoVersion != "" || stats.GoOS != "" || stats.GoArch != "" {
  155. relayBuildInfo.WithLabelValues(host, stats.GoVersion, stats.GoOS, stats.GoArch).Add(1)
  156. }
  157. if location.City != "" || location.Country != "" || location.Continent != "" {
  158. relayLocationInfo.WithLabelValues(host, location.City, location.Country, location.Continent).Add(1)
  159. }
  160. if lastStat, ok := lastStats[host]; ok {
  161. stats = mergeStats(stats, lastStat)
  162. }
  163. relayUptime.WithLabelValues(host).Set(float64(stats.UptimeSeconds))
  164. relayPendingSessionKeys.WithLabelValues(host).Set(float64(stats.PendingSessionKeys))
  165. relayActiveSessions.WithLabelValues(host).Set(float64(stats.ActiveSessions))
  166. relayConnections.WithLabelValues(host).Set(float64(stats.Connections))
  167. relayProxies.WithLabelValues(host).Set(float64(stats.Proxies))
  168. relayBytesProxied.WithLabelValues(host).Set(float64(stats.BytesProxied))
  169. relayGoRoutines.WithLabelValues(host).Set(float64(stats.GoRoutines))
  170. relaySessionRate.WithLabelValues(host).Set(float64(stats.Options.SessionRate))
  171. relayGlobalRate.WithLabelValues(host).Set(float64(stats.Options.GlobalRate))
  172. lastStats[host] = stats
  173. }
  174. func deleteMetrics(host string) {
  175. relayUptime.DeleteLabelValues(host)
  176. relayPendingSessionKeys.DeleteLabelValues(host)
  177. relayActiveSessions.DeleteLabelValues(host)
  178. relayConnections.DeleteLabelValues(host)
  179. relayProxies.DeleteLabelValues(host)
  180. relayBytesProxied.DeleteLabelValues(host)
  181. relayGoRoutines.DeleteLabelValues(host)
  182. relaySessionRate.DeleteLabelValues(host)
  183. relayGlobalRate.DeleteLabelValues(host)
  184. delete(lastStats, host)
  185. }
  186. // Due to some unexplainable behaviour, some of the numbers sometimes travel slightly backwards (by less than 1%)
  187. // This happens between scrapes, which is 30s, so this can't be a race.
  188. // This causes prometheus to assume a "rate reset", hence causes phenomenal spikes.
  189. // One of the number that moves backwards is BytesProxied, which atomically increments a counter with numeric value
  190. // returned by net.Conn.Read(). I don't think that can return a negative value, so I have no idea what's going on.
  191. func mergeStats(new stats, old stats) stats {
  192. new.UptimeSeconds = mergeValue(new.UptimeSeconds, old.UptimeSeconds)
  193. new.PendingSessionKeys = mergeValue(new.PendingSessionKeys, old.PendingSessionKeys)
  194. new.ActiveSessions = mergeValue(new.ActiveSessions, old.ActiveSessions)
  195. new.Connections = mergeValue(new.Connections, old.Connections)
  196. new.Proxies = mergeValue(new.Proxies, old.Proxies)
  197. new.BytesProxied = mergeValue(new.BytesProxied, old.BytesProxied)
  198. new.GoRoutines = mergeValue(new.GoRoutines, old.GoRoutines)
  199. new.Options.SessionRate = mergeValue(new.Options.SessionRate, old.Options.SessionRate)
  200. new.Options.GlobalRate = mergeValue(new.Options.GlobalRate, old.Options.GlobalRate)
  201. return new
  202. }
  203. func mergeValue(new, old int) int {
  204. if new >= old {
  205. return new // normal increase
  206. }
  207. if float64(new) > 0.99*float64(old) {
  208. return old // slight backward movement
  209. }
  210. return new // reset (relay restart)
  211. }