metrics.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424
  1. /*
  2. We export metrics in the format specified in our broker spec:
  3. https://gitweb.torproject.org/pluggable-transports/snowflake.git/tree/doc/broker-spec.txt
  4. */
  5. package main
  6. import (
  7. "fmt"
  8. "log"
  9. "math"
  10. "net"
  11. "sort"
  12. "sync"
  13. "time"
  14. "github.com/prometheus/client_golang/prometheus"
  15. "gitlab.torproject.org/tpo/anti-censorship/geoip"
  16. "gitlab.torproject.org/tpo/anti-censorship/pluggable-transports/ptutil/safeprom"
  17. "gitlab.torproject.org/tpo/anti-censorship/pluggable-transports/snowflake/v2/common/messages"
  18. )
  19. const (
  20. prometheusNamespace = "snowflake"
  21. metricsResolution = 60 * 60 * 24 * time.Second //86400 seconds
  22. )
  23. var rendezvoudMethodList = [...]messages.RendezvousMethod{
  24. messages.RendezvousHttp,
  25. messages.RendezvousAmpCache,
  26. messages.RendezvousSqs,
  27. }
  28. type CountryStats struct {
  29. // map[proxyType][address]bool
  30. proxies map[string]map[string]bool
  31. unknown map[string]bool
  32. natRestricted map[string]bool
  33. natUnrestricted map[string]bool
  34. natUnknown map[string]bool
  35. counts map[string]int
  36. }
  37. // Implements Observable
  38. type Metrics struct {
  39. logger *log.Logger
  40. geoipdb *geoip.Geoip
  41. countryStats CountryStats
  42. clientRoundtripEstimate time.Duration
  43. proxyIdleCount uint
  44. clientDeniedCount map[messages.RendezvousMethod]uint
  45. clientRestrictedDeniedCount map[messages.RendezvousMethod]uint
  46. clientUnrestrictedDeniedCount map[messages.RendezvousMethod]uint
  47. clientProxyMatchCount map[messages.RendezvousMethod]uint
  48. rendezvousCountryStats map[messages.RendezvousMethod]map[string]int
  49. proxyPollWithRelayURLExtension uint
  50. proxyPollWithoutRelayURLExtension uint
  51. proxyPollRejectedWithRelayURLExtension uint
  52. // synchronization for access to snowflake metrics
  53. lock sync.Mutex
  54. promMetrics *PromMetrics
  55. }
  56. type record struct {
  57. cc string
  58. count int
  59. }
  60. type records []record
  61. func (r records) Len() int { return len(r) }
  62. func (r records) Swap(i, j int) { r[i], r[j] = r[j], r[i] }
  63. func (r records) Less(i, j int) bool {
  64. if r[i].count == r[j].count {
  65. return r[i].cc > r[j].cc
  66. }
  67. return r[i].count < r[j].count
  68. }
  69. func (s CountryStats) Display() string {
  70. output := ""
  71. // Use the records struct to sort our counts map by value.
  72. rs := records{}
  73. for cc, count := range s.counts {
  74. rs = append(rs, record{cc: cc, count: count})
  75. }
  76. sort.Sort(sort.Reverse(rs))
  77. for _, r := range rs {
  78. output += fmt.Sprintf("%s=%d,", r.cc, r.count)
  79. }
  80. // cut off trailing ","
  81. if len(output) > 0 {
  82. return output[:len(output)-1]
  83. }
  84. return output
  85. }
  86. func (m *Metrics) UpdateCountryStats(addr string, proxyType string, natType string) {
  87. var country string
  88. var ok bool
  89. addresses, ok := m.countryStats.proxies[proxyType]
  90. if !ok {
  91. if m.countryStats.unknown[addr] {
  92. return
  93. }
  94. m.countryStats.unknown[addr] = true
  95. } else {
  96. if addresses[addr] {
  97. return
  98. }
  99. addresses[addr] = true
  100. }
  101. ip := net.ParseIP(addr)
  102. if m.geoipdb == nil {
  103. return
  104. }
  105. country, ok = m.geoipdb.GetCountryByAddr(ip)
  106. if !ok {
  107. country = "??"
  108. }
  109. m.countryStats.counts[country]++
  110. m.promMetrics.ProxyTotal.With(prometheus.Labels{
  111. "nat": natType,
  112. "type": proxyType,
  113. "cc": country,
  114. }).Inc()
  115. switch natType {
  116. case NATRestricted:
  117. m.countryStats.natRestricted[addr] = true
  118. case NATUnrestricted:
  119. m.countryStats.natUnrestricted[addr] = true
  120. default:
  121. m.countryStats.natUnknown[addr] = true
  122. }
  123. }
  124. func (m *Metrics) UpdateRendezvousStats(addr string, rendezvousMethod messages.RendezvousMethod, natType string, matched bool) {
  125. ip := net.ParseIP(addr)
  126. country := "??"
  127. if m.geoipdb != nil {
  128. country_by_addr, ok := m.geoipdb.GetCountryByAddr(ip)
  129. if ok {
  130. country = country_by_addr
  131. }
  132. }
  133. var status string
  134. if !matched {
  135. m.clientDeniedCount[rendezvousMethod]++
  136. if natType == NATUnrestricted {
  137. m.clientUnrestrictedDeniedCount[rendezvousMethod]++
  138. } else {
  139. m.clientRestrictedDeniedCount[rendezvousMethod]++
  140. }
  141. status = "denied"
  142. } else {
  143. status = "matched"
  144. m.clientProxyMatchCount[rendezvousMethod]++
  145. }
  146. m.rendezvousCountryStats[rendezvousMethod][country]++
  147. m.promMetrics.ClientPollTotal.With(prometheus.Labels{
  148. "nat": natType,
  149. "status": status,
  150. "rendezvous_method": string(rendezvousMethod),
  151. "cc": country,
  152. }).Inc()
  153. }
  154. func (m *Metrics) DisplayRendezvousStatsByCountry(rendezvoudMethod messages.RendezvousMethod) string {
  155. output := ""
  156. // Use the records struct to sort our counts map by value.
  157. rs := records{}
  158. for cc, count := range m.rendezvousCountryStats[rendezvoudMethod] {
  159. rs = append(rs, record{cc: cc, count: count})
  160. }
  161. sort.Sort(sort.Reverse(rs))
  162. for _, r := range rs {
  163. output += fmt.Sprintf("%s=%d,", r.cc, binCount(uint(r.count)))
  164. }
  165. // cut off trailing ","
  166. if len(output) > 0 {
  167. return output[:len(output)-1]
  168. }
  169. return output
  170. }
  171. func (m *Metrics) LoadGeoipDatabases(geoipDB string, geoip6DB string) error {
  172. // Load geoip databases
  173. var err error
  174. log.Println("Loading geoip databases")
  175. m.geoipdb, err = geoip.New(geoipDB, geoip6DB)
  176. return err
  177. }
  178. func NewMetrics(metricsLogger *log.Logger) (*Metrics, error) {
  179. m := new(Metrics)
  180. m.clientDeniedCount = make(map[messages.RendezvousMethod]uint)
  181. m.clientRestrictedDeniedCount = make(map[messages.RendezvousMethod]uint)
  182. m.clientUnrestrictedDeniedCount = make(map[messages.RendezvousMethod]uint)
  183. m.clientProxyMatchCount = make(map[messages.RendezvousMethod]uint)
  184. m.rendezvousCountryStats = make(map[messages.RendezvousMethod]map[string]int)
  185. for _, rendezvousMethod := range rendezvoudMethodList {
  186. m.rendezvousCountryStats[rendezvousMethod] = make(map[string]int)
  187. }
  188. m.countryStats = CountryStats{
  189. counts: make(map[string]int),
  190. proxies: make(map[string]map[string]bool),
  191. unknown: make(map[string]bool),
  192. natRestricted: make(map[string]bool),
  193. natUnrestricted: make(map[string]bool),
  194. natUnknown: make(map[string]bool),
  195. }
  196. for pType := range messages.KnownProxyTypes {
  197. m.countryStats.proxies[pType] = make(map[string]bool)
  198. }
  199. m.logger = metricsLogger
  200. m.promMetrics = initPrometheus()
  201. // Write to log file every day with updated metrics
  202. go m.logMetrics()
  203. return m, nil
  204. }
  205. // Logs metrics in intervals specified by metricsResolution
  206. func (m *Metrics) logMetrics() {
  207. heartbeat := time.Tick(metricsResolution)
  208. for range heartbeat {
  209. m.printMetrics()
  210. m.zeroMetrics()
  211. }
  212. }
  213. func (m *Metrics) printMetrics() {
  214. m.lock.Lock()
  215. m.logger.Println(
  216. "snowflake-stats-end",
  217. time.Now().UTC().Format("2006-01-02 15:04:05"),
  218. fmt.Sprintf("(%d s)", int(metricsResolution.Seconds())),
  219. )
  220. m.logger.Println("snowflake-ips", m.countryStats.Display())
  221. total := len(m.countryStats.unknown)
  222. for pType, addresses := range m.countryStats.proxies {
  223. m.logger.Printf("snowflake-ips-%s %d\n", pType, len(addresses))
  224. total += len(addresses)
  225. }
  226. m.logger.Println("snowflake-ips-total", total)
  227. m.logger.Println("snowflake-idle-count", binCount(m.proxyIdleCount))
  228. m.logger.Println("snowflake-proxy-poll-with-relay-url-count", binCount(m.proxyPollWithRelayURLExtension))
  229. m.logger.Println("snowflake-proxy-poll-without-relay-url-count", binCount(m.proxyPollWithoutRelayURLExtension))
  230. m.logger.Println("snowflake-proxy-rejected-for-relay-url-count", binCount(m.proxyPollRejectedWithRelayURLExtension))
  231. m.logger.Println("client-denied-count", binCount(sumMapValues(&m.clientDeniedCount)))
  232. m.logger.Println("client-restricted-denied-count", binCount(sumMapValues(&m.clientRestrictedDeniedCount)))
  233. m.logger.Println("client-unrestricted-denied-count", binCount(sumMapValues(&m.clientUnrestrictedDeniedCount)))
  234. m.logger.Println("client-snowflake-match-count", binCount(sumMapValues(&m.clientProxyMatchCount)))
  235. for _, rendezvousMethod := range rendezvoudMethodList {
  236. m.logger.Printf("client-%s-count %d\n", rendezvousMethod, binCount(
  237. m.clientDeniedCount[rendezvousMethod]+m.clientProxyMatchCount[rendezvousMethod],
  238. ))
  239. m.logger.Printf("client-%s-ips %s\n", rendezvousMethod, m.DisplayRendezvousStatsByCountry(rendezvousMethod))
  240. }
  241. m.logger.Println("snowflake-ips-nat-restricted", len(m.countryStats.natRestricted))
  242. m.logger.Println("snowflake-ips-nat-unrestricted", len(m.countryStats.natUnrestricted))
  243. m.logger.Println("snowflake-ips-nat-unknown", len(m.countryStats.natUnknown))
  244. m.lock.Unlock()
  245. }
  246. // Restores all metrics to original values
  247. func (m *Metrics) zeroMetrics() {
  248. m.proxyIdleCount = 0
  249. m.clientDeniedCount = make(map[messages.RendezvousMethod]uint)
  250. m.clientRestrictedDeniedCount = make(map[messages.RendezvousMethod]uint)
  251. m.clientUnrestrictedDeniedCount = make(map[messages.RendezvousMethod]uint)
  252. m.proxyPollRejectedWithRelayURLExtension = 0
  253. m.proxyPollWithRelayURLExtension = 0
  254. m.proxyPollWithoutRelayURLExtension = 0
  255. m.clientProxyMatchCount = make(map[messages.RendezvousMethod]uint)
  256. m.rendezvousCountryStats = make(map[messages.RendezvousMethod]map[string]int)
  257. for _, rendezvousMethod := range rendezvoudMethodList {
  258. m.rendezvousCountryStats[rendezvousMethod] = make(map[string]int)
  259. }
  260. m.countryStats.counts = make(map[string]int)
  261. for pType := range m.countryStats.proxies {
  262. m.countryStats.proxies[pType] = make(map[string]bool)
  263. }
  264. m.countryStats.unknown = make(map[string]bool)
  265. m.countryStats.natRestricted = make(map[string]bool)
  266. m.countryStats.natUnrestricted = make(map[string]bool)
  267. m.countryStats.natUnknown = make(map[string]bool)
  268. }
  269. // Rounds up a count to the nearest multiple of 8.
  270. func binCount(count uint) uint {
  271. return uint((math.Ceil(float64(count) / 8)) * 8)
  272. }
  273. func sumMapValues(m *map[messages.RendezvousMethod]uint) uint {
  274. var s uint = 0
  275. for _, v := range *m {
  276. s += v
  277. }
  278. return s
  279. }
  280. type PromMetrics struct {
  281. registry *prometheus.Registry
  282. ProxyTotal *prometheus.CounterVec
  283. ProxyPollTotal *safeprom.CounterVec
  284. ClientPollTotal *safeprom.CounterVec
  285. AvailableProxies *prometheus.GaugeVec
  286. ProxyPollWithRelayURLExtensionTotal *safeprom.CounterVec
  287. ProxyPollWithoutRelayURLExtensionTotal *safeprom.CounterVec
  288. ProxyPollRejectedForRelayURLExtensionTotal *safeprom.CounterVec
  289. }
  290. // Initialize metrics for prometheus exporter
  291. func initPrometheus() *PromMetrics {
  292. promMetrics := &PromMetrics{}
  293. promMetrics.registry = prometheus.NewRegistry()
  294. promMetrics.ProxyTotal = prometheus.NewCounterVec(
  295. prometheus.CounterOpts{
  296. Namespace: prometheusNamespace,
  297. Name: "proxy_total",
  298. Help: "The number of unique snowflake IPs",
  299. },
  300. []string{"type", "nat", "cc"},
  301. )
  302. promMetrics.AvailableProxies = prometheus.NewGaugeVec(
  303. prometheus.GaugeOpts{
  304. Namespace: prometheusNamespace,
  305. Name: "available_proxies",
  306. Help: "The number of currently available snowflake proxies",
  307. },
  308. []string{"type", "nat"},
  309. )
  310. promMetrics.ProxyPollTotal = safeprom.NewCounterVec(
  311. prometheus.CounterOpts{
  312. Namespace: prometheusNamespace,
  313. Name: "rounded_proxy_poll_total",
  314. Help: "The number of snowflake proxy polls, rounded up to a multiple of 8",
  315. },
  316. []string{"nat", "status"},
  317. )
  318. promMetrics.ProxyPollWithRelayURLExtensionTotal = safeprom.NewCounterVec(
  319. prometheus.CounterOpts{
  320. Namespace: prometheusNamespace,
  321. Name: "rounded_proxy_poll_with_relay_url_extension_total",
  322. Help: "The number of snowflake proxy polls with Relay URL Extension, rounded up to a multiple of 8",
  323. },
  324. []string{"nat", "type"},
  325. )
  326. promMetrics.ProxyPollWithoutRelayURLExtensionTotal = safeprom.NewCounterVec(
  327. prometheus.CounterOpts{
  328. Namespace: prometheusNamespace,
  329. Name: "rounded_proxy_poll_without_relay_url_extension_total",
  330. Help: "The number of snowflake proxy polls without Relay URL Extension, rounded up to a multiple of 8",
  331. },
  332. []string{"nat", "type"},
  333. )
  334. promMetrics.ProxyPollRejectedForRelayURLExtensionTotal = safeprom.NewCounterVec(
  335. prometheus.CounterOpts{
  336. Namespace: prometheusNamespace,
  337. Name: "rounded_proxy_poll_rejected_relay_url_extension_total",
  338. Help: "The number of snowflake proxy polls rejected by Relay URL Extension, rounded up to a multiple of 8",
  339. },
  340. []string{"nat", "type"},
  341. )
  342. promMetrics.ClientPollTotal = safeprom.NewCounterVec(
  343. prometheus.CounterOpts{
  344. Namespace: prometheusNamespace,
  345. Name: "rounded_client_poll_total",
  346. Help: "The number of snowflake client polls, rounded up to a multiple of 8",
  347. },
  348. []string{"nat", "status", "cc", "rendezvous_method"},
  349. )
  350. // We need to register our metrics so they can be exported.
  351. promMetrics.registry.MustRegister(
  352. promMetrics.ClientPollTotal, promMetrics.ProxyPollTotal,
  353. promMetrics.ProxyTotal, promMetrics.AvailableProxies,
  354. promMetrics.ProxyPollWithRelayURLExtensionTotal,
  355. promMetrics.ProxyPollWithoutRelayURLExtensionTotal,
  356. promMetrics.ProxyPollRejectedForRelayURLExtensionTotal,
  357. )
  358. return promMetrics
  359. }