stats.go 8.4 KB

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