metrics.go 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318
  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. )
  16. const (
  17. prometheusNamespace = "snowflake"
  18. metricsResolution = 60 * 60 * 24 * time.Second //86400 seconds
  19. )
  20. type CountryStats struct {
  21. standalone map[string]bool
  22. badge map[string]bool
  23. webext map[string]bool
  24. unknown map[string]bool
  25. natRestricted map[string]bool
  26. natUnrestricted map[string]bool
  27. natUnknown map[string]bool
  28. counts map[string]int
  29. }
  30. // Implements Observable
  31. type Metrics struct {
  32. logger *log.Logger
  33. tablev4 *GeoIPv4Table
  34. tablev6 *GeoIPv6Table
  35. countryStats CountryStats
  36. clientRoundtripEstimate time.Duration
  37. proxyIdleCount uint
  38. clientDeniedCount uint
  39. clientRestrictedDeniedCount uint
  40. clientUnrestrictedDeniedCount uint
  41. clientProxyMatchCount uint
  42. // synchronization for access to snowflake metrics
  43. lock sync.Mutex
  44. promMetrics *PromMetrics
  45. }
  46. type record struct {
  47. cc string
  48. count int
  49. }
  50. type records []record
  51. func (r records) Len() int { return len(r) }
  52. func (r records) Swap(i, j int) { r[i], r[j] = r[j], r[i] }
  53. func (r records) Less(i, j int) bool {
  54. if r[i].count == r[j].count {
  55. return r[i].cc > r[j].cc
  56. }
  57. return r[i].count < r[j].count
  58. }
  59. func (s CountryStats) Display() string {
  60. output := ""
  61. // Use the records struct to sort our counts map by value.
  62. rs := records{}
  63. for cc, count := range s.counts {
  64. rs = append(rs, record{cc: cc, count: count})
  65. }
  66. sort.Sort(sort.Reverse(rs))
  67. for _, r := range rs {
  68. output += fmt.Sprintf("%s=%d,", r.cc, r.count)
  69. }
  70. // cut off trailing ","
  71. if len(output) > 0 {
  72. return output[:len(output)-1]
  73. }
  74. return output
  75. }
  76. func (m *Metrics) UpdateCountryStats(addr string, proxyType string, natType string) {
  77. var country string
  78. var ok bool
  79. if proxyType == "standalone" {
  80. if m.countryStats.standalone[addr] {
  81. return
  82. }
  83. } else if proxyType == "badge" {
  84. if m.countryStats.badge[addr] {
  85. return
  86. }
  87. } else if proxyType == "webext" {
  88. if m.countryStats.webext[addr] {
  89. return
  90. }
  91. } else {
  92. if m.countryStats.unknown[addr] {
  93. return
  94. }
  95. }
  96. ip := net.ParseIP(addr)
  97. if ip.To4() != nil {
  98. //This is an IPv4 address
  99. if m.tablev4 == nil {
  100. return
  101. }
  102. country, ok = GetCountryByAddr(m.tablev4, ip)
  103. } else {
  104. if m.tablev6 == nil {
  105. return
  106. }
  107. country, ok = GetCountryByAddr(m.tablev6, ip)
  108. }
  109. if !ok {
  110. country = "??"
  111. }
  112. //update map of unique ips and counts
  113. m.countryStats.counts[country]++
  114. if proxyType == "standalone" {
  115. m.countryStats.standalone[addr] = true
  116. } else if proxyType == "badge" {
  117. m.countryStats.badge[addr] = true
  118. } else if proxyType == "webext" {
  119. m.countryStats.webext[addr] = true
  120. } else {
  121. m.countryStats.unknown[addr] = true
  122. }
  123. m.promMetrics.ProxyTotal.With(prometheus.Labels{
  124. "nat": natType,
  125. "type": proxyType,
  126. "cc": country,
  127. }).Inc()
  128. switch natType {
  129. case NATRestricted:
  130. m.countryStats.natRestricted[addr] = true
  131. case NATUnrestricted:
  132. m.countryStats.natUnrestricted[addr] = true
  133. default:
  134. m.countryStats.natUnknown[addr] = true
  135. }
  136. }
  137. func (m *Metrics) LoadGeoipDatabases(geoipDB string, geoip6DB string) error {
  138. // Load geoip databases
  139. log.Println("Loading geoip databases")
  140. tablev4 := new(GeoIPv4Table)
  141. err := GeoIPLoadFile(tablev4, geoipDB)
  142. if err != nil {
  143. m.tablev4 = nil
  144. return err
  145. }
  146. m.tablev4 = tablev4
  147. tablev6 := new(GeoIPv6Table)
  148. err = GeoIPLoadFile(tablev6, geoip6DB)
  149. if err != nil {
  150. m.tablev6 = nil
  151. return err
  152. }
  153. m.tablev6 = tablev6
  154. return nil
  155. }
  156. func NewMetrics(metricsLogger *log.Logger) (*Metrics, error) {
  157. m := new(Metrics)
  158. m.countryStats = CountryStats{
  159. counts: make(map[string]int),
  160. standalone: make(map[string]bool),
  161. badge: make(map[string]bool),
  162. webext: make(map[string]bool),
  163. unknown: make(map[string]bool),
  164. natRestricted: make(map[string]bool),
  165. natUnrestricted: make(map[string]bool),
  166. natUnknown: make(map[string]bool),
  167. }
  168. m.logger = metricsLogger
  169. m.promMetrics = initPrometheus()
  170. // Write to log file every hour with updated metrics
  171. go m.logMetrics()
  172. return m, nil
  173. }
  174. // Logs metrics in intervals specified by metricsResolution
  175. func (m *Metrics) logMetrics() {
  176. heartbeat := time.Tick(metricsResolution)
  177. for range heartbeat {
  178. m.printMetrics()
  179. m.zeroMetrics()
  180. }
  181. }
  182. func (m *Metrics) printMetrics() {
  183. m.lock.Lock()
  184. m.logger.Println("snowflake-stats-end", time.Now().UTC().Format("2006-01-02 15:04:05"), fmt.Sprintf("(%d s)", int(metricsResolution.Seconds())))
  185. m.logger.Println("snowflake-ips", m.countryStats.Display())
  186. m.logger.Println("snowflake-ips-total", len(m.countryStats.standalone)+
  187. len(m.countryStats.badge)+len(m.countryStats.webext)+len(m.countryStats.unknown))
  188. m.logger.Println("snowflake-ips-standalone", len(m.countryStats.standalone))
  189. m.logger.Println("snowflake-ips-badge", len(m.countryStats.badge))
  190. m.logger.Println("snowflake-ips-webext", len(m.countryStats.webext))
  191. m.logger.Println("snowflake-idle-count", binCount(m.proxyIdleCount))
  192. m.logger.Println("client-denied-count", binCount(m.clientDeniedCount))
  193. m.logger.Println("client-restricted-denied-count", binCount(m.clientRestrictedDeniedCount))
  194. m.logger.Println("client-unrestricted-denied-count", binCount(m.clientUnrestrictedDeniedCount))
  195. m.logger.Println("client-snowflake-match-count", binCount(m.clientProxyMatchCount))
  196. m.logger.Println("snowflake-ips-nat-restricted", len(m.countryStats.natRestricted))
  197. m.logger.Println("snowflake-ips-nat-unrestricted", len(m.countryStats.natUnrestricted))
  198. m.logger.Println("snowflake-ips-nat-unknown", len(m.countryStats.natUnknown))
  199. m.lock.Unlock()
  200. }
  201. // Restores all metrics to original values
  202. func (m *Metrics) zeroMetrics() {
  203. m.proxyIdleCount = 0
  204. m.clientDeniedCount = 0
  205. m.clientRestrictedDeniedCount = 0
  206. m.clientUnrestrictedDeniedCount = 0
  207. m.clientProxyMatchCount = 0
  208. m.countryStats.counts = make(map[string]int)
  209. m.countryStats.standalone = make(map[string]bool)
  210. m.countryStats.badge = make(map[string]bool)
  211. m.countryStats.webext = make(map[string]bool)
  212. m.countryStats.unknown = make(map[string]bool)
  213. m.countryStats.natRestricted = make(map[string]bool)
  214. m.countryStats.natUnrestricted = make(map[string]bool)
  215. m.countryStats.natUnknown = make(map[string]bool)
  216. }
  217. // Rounds up a count to the nearest multiple of 8.
  218. func binCount(count uint) uint {
  219. return uint((math.Ceil(float64(count) / 8)) * 8)
  220. }
  221. type PromMetrics struct {
  222. registry *prometheus.Registry
  223. ProxyTotal *prometheus.CounterVec
  224. ProxyPollTotal *RoundedCounterVec
  225. ClientPollTotal *RoundedCounterVec
  226. AvailableProxies *prometheus.GaugeVec
  227. }
  228. // Initialize metrics for prometheus exporter
  229. func initPrometheus() *PromMetrics {
  230. promMetrics := &PromMetrics{}
  231. promMetrics.registry = prometheus.NewRegistry()
  232. promMetrics.ProxyTotal = prometheus.NewCounterVec(
  233. prometheus.CounterOpts{
  234. Namespace: prometheusNamespace,
  235. Name: "proxy_total",
  236. Help: "The number of unique snowflake IPs",
  237. },
  238. []string{"type", "nat", "cc"},
  239. )
  240. promMetrics.AvailableProxies = prometheus.NewGaugeVec(
  241. prometheus.GaugeOpts{
  242. Namespace: prometheusNamespace,
  243. Name: "available_proxies",
  244. Help: "The number of currently available snowflake proxies",
  245. },
  246. []string{"type", "nat"},
  247. )
  248. promMetrics.ProxyPollTotal = NewRoundedCounterVec(
  249. prometheus.CounterOpts{
  250. Namespace: prometheusNamespace,
  251. Name: "rounded_proxy_poll_total",
  252. Help: "The number of snowflake proxy polls, rounded up to a multiple of 8",
  253. },
  254. []string{"nat", "status"},
  255. )
  256. promMetrics.ClientPollTotal = NewRoundedCounterVec(
  257. prometheus.CounterOpts{
  258. Namespace: prometheusNamespace,
  259. Name: "rounded_client_poll_total",
  260. Help: "The number of snowflake client polls, rounded up to a multiple of 8",
  261. },
  262. []string{"nat", "status"},
  263. )
  264. // We need to register our metrics so they can be exported.
  265. promMetrics.registry.MustRegister(
  266. promMetrics.ClientPollTotal, promMetrics.ProxyPollTotal,
  267. promMetrics.ProxyTotal, promMetrics.AvailableProxies,
  268. )
  269. return promMetrics
  270. }