123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424 |
- /*
- We export metrics in the format specified in our broker spec:
- https://gitweb.torproject.org/pluggable-transports/snowflake.git/tree/doc/broker-spec.txt
- */
- package main
- import (
- "fmt"
- "log"
- "math"
- "net"
- "sort"
- "sync"
- "time"
- "github.com/prometheus/client_golang/prometheus"
- "gitlab.torproject.org/tpo/anti-censorship/geoip"
- "gitlab.torproject.org/tpo/anti-censorship/pluggable-transports/ptutil/safeprom"
- "gitlab.torproject.org/tpo/anti-censorship/pluggable-transports/snowflake/v2/common/messages"
- )
- const (
- prometheusNamespace = "snowflake"
- metricsResolution = 60 * 60 * 24 * time.Second //86400 seconds
- )
- var rendezvoudMethodList = [...]messages.RendezvousMethod{
- messages.RendezvousHttp,
- messages.RendezvousAmpCache,
- messages.RendezvousSqs,
- }
- type CountryStats struct {
- // map[proxyType][address]bool
- proxies map[string]map[string]bool
- unknown map[string]bool
- natRestricted map[string]bool
- natUnrestricted map[string]bool
- natUnknown map[string]bool
- counts map[string]int
- }
- // Implements Observable
- type Metrics struct {
- logger *log.Logger
- geoipdb *geoip.Geoip
- countryStats CountryStats
- clientRoundtripEstimate time.Duration
- proxyIdleCount uint
- clientDeniedCount map[messages.RendezvousMethod]uint
- clientRestrictedDeniedCount map[messages.RendezvousMethod]uint
- clientUnrestrictedDeniedCount map[messages.RendezvousMethod]uint
- clientProxyMatchCount map[messages.RendezvousMethod]uint
- rendezvousCountryStats map[messages.RendezvousMethod]map[string]int
- proxyPollWithRelayURLExtension uint
- proxyPollWithoutRelayURLExtension uint
- proxyPollRejectedWithRelayURLExtension uint
- // synchronization for access to snowflake metrics
- lock sync.Mutex
- promMetrics *PromMetrics
- }
- type record struct {
- cc string
- count int
- }
- type records []record
- func (r records) Len() int { return len(r) }
- func (r records) Swap(i, j int) { r[i], r[j] = r[j], r[i] }
- func (r records) Less(i, j int) bool {
- if r[i].count == r[j].count {
- return r[i].cc > r[j].cc
- }
- return r[i].count < r[j].count
- }
- func (s CountryStats) Display() string {
- output := ""
- // Use the records struct to sort our counts map by value.
- rs := records{}
- for cc, count := range s.counts {
- rs = append(rs, record{cc: cc, count: count})
- }
- sort.Sort(sort.Reverse(rs))
- for _, r := range rs {
- output += fmt.Sprintf("%s=%d,", r.cc, r.count)
- }
- // cut off trailing ","
- if len(output) > 0 {
- return output[:len(output)-1]
- }
- return output
- }
- func (m *Metrics) UpdateCountryStats(addr string, proxyType string, natType string) {
- var country string
- var ok bool
- addresses, ok := m.countryStats.proxies[proxyType]
- if !ok {
- if m.countryStats.unknown[addr] {
- return
- }
- m.countryStats.unknown[addr] = true
- } else {
- if addresses[addr] {
- return
- }
- addresses[addr] = true
- }
- ip := net.ParseIP(addr)
- if m.geoipdb == nil {
- return
- }
- country, ok = m.geoipdb.GetCountryByAddr(ip)
- if !ok {
- country = "??"
- }
- m.countryStats.counts[country]++
- m.promMetrics.ProxyTotal.With(prometheus.Labels{
- "nat": natType,
- "type": proxyType,
- "cc": country,
- }).Inc()
- switch natType {
- case NATRestricted:
- m.countryStats.natRestricted[addr] = true
- case NATUnrestricted:
- m.countryStats.natUnrestricted[addr] = true
- default:
- m.countryStats.natUnknown[addr] = true
- }
- }
- func (m *Metrics) UpdateRendezvousStats(addr string, rendezvousMethod messages.RendezvousMethod, natType string, matched bool) {
- ip := net.ParseIP(addr)
- country := "??"
- if m.geoipdb != nil {
- country_by_addr, ok := m.geoipdb.GetCountryByAddr(ip)
- if ok {
- country = country_by_addr
- }
- }
- var status string
- if !matched {
- m.clientDeniedCount[rendezvousMethod]++
- if natType == NATUnrestricted {
- m.clientUnrestrictedDeniedCount[rendezvousMethod]++
- } else {
- m.clientRestrictedDeniedCount[rendezvousMethod]++
- }
- status = "denied"
- } else {
- status = "matched"
- m.clientProxyMatchCount[rendezvousMethod]++
- }
- m.rendezvousCountryStats[rendezvousMethod][country]++
- m.promMetrics.ClientPollTotal.With(prometheus.Labels{
- "nat": natType,
- "status": status,
- "rendezvous_method": string(rendezvousMethod),
- "cc": country,
- }).Inc()
- }
- func (m *Metrics) DisplayRendezvousStatsByCountry(rendezvoudMethod messages.RendezvousMethod) string {
- output := ""
- // Use the records struct to sort our counts map by value.
- rs := records{}
- for cc, count := range m.rendezvousCountryStats[rendezvoudMethod] {
- rs = append(rs, record{cc: cc, count: count})
- }
- sort.Sort(sort.Reverse(rs))
- for _, r := range rs {
- output += fmt.Sprintf("%s=%d,", r.cc, binCount(uint(r.count)))
- }
- // cut off trailing ","
- if len(output) > 0 {
- return output[:len(output)-1]
- }
- return output
- }
- func (m *Metrics) LoadGeoipDatabases(geoipDB string, geoip6DB string) error {
- // Load geoip databases
- var err error
- log.Println("Loading geoip databases")
- m.geoipdb, err = geoip.New(geoipDB, geoip6DB)
- return err
- }
- func NewMetrics(metricsLogger *log.Logger) (*Metrics, error) {
- m := new(Metrics)
- m.clientDeniedCount = make(map[messages.RendezvousMethod]uint)
- m.clientRestrictedDeniedCount = make(map[messages.RendezvousMethod]uint)
- m.clientUnrestrictedDeniedCount = make(map[messages.RendezvousMethod]uint)
- m.clientProxyMatchCount = make(map[messages.RendezvousMethod]uint)
- m.rendezvousCountryStats = make(map[messages.RendezvousMethod]map[string]int)
- for _, rendezvousMethod := range rendezvoudMethodList {
- m.rendezvousCountryStats[rendezvousMethod] = make(map[string]int)
- }
- m.countryStats = CountryStats{
- counts: make(map[string]int),
- proxies: make(map[string]map[string]bool),
- unknown: make(map[string]bool),
- natRestricted: make(map[string]bool),
- natUnrestricted: make(map[string]bool),
- natUnknown: make(map[string]bool),
- }
- for pType := range messages.KnownProxyTypes {
- m.countryStats.proxies[pType] = make(map[string]bool)
- }
- m.logger = metricsLogger
- m.promMetrics = initPrometheus()
- // Write to log file every day with updated metrics
- go m.logMetrics()
- return m, nil
- }
- // Logs metrics in intervals specified by metricsResolution
- func (m *Metrics) logMetrics() {
- heartbeat := time.Tick(metricsResolution)
- for range heartbeat {
- m.printMetrics()
- m.zeroMetrics()
- }
- }
- func (m *Metrics) printMetrics() {
- m.lock.Lock()
- m.logger.Println(
- "snowflake-stats-end",
- time.Now().UTC().Format("2006-01-02 15:04:05"),
- fmt.Sprintf("(%d s)", int(metricsResolution.Seconds())),
- )
- m.logger.Println("snowflake-ips", m.countryStats.Display())
- total := len(m.countryStats.unknown)
- for pType, addresses := range m.countryStats.proxies {
- m.logger.Printf("snowflake-ips-%s %d\n", pType, len(addresses))
- total += len(addresses)
- }
- m.logger.Println("snowflake-ips-total", total)
- m.logger.Println("snowflake-idle-count", binCount(m.proxyIdleCount))
- m.logger.Println("snowflake-proxy-poll-with-relay-url-count", binCount(m.proxyPollWithRelayURLExtension))
- m.logger.Println("snowflake-proxy-poll-without-relay-url-count", binCount(m.proxyPollWithoutRelayURLExtension))
- m.logger.Println("snowflake-proxy-rejected-for-relay-url-count", binCount(m.proxyPollRejectedWithRelayURLExtension))
- m.logger.Println("client-denied-count", binCount(sumMapValues(&m.clientDeniedCount)))
- m.logger.Println("client-restricted-denied-count", binCount(sumMapValues(&m.clientRestrictedDeniedCount)))
- m.logger.Println("client-unrestricted-denied-count", binCount(sumMapValues(&m.clientUnrestrictedDeniedCount)))
- m.logger.Println("client-snowflake-match-count", binCount(sumMapValues(&m.clientProxyMatchCount)))
- for _, rendezvousMethod := range rendezvoudMethodList {
- m.logger.Printf("client-%s-count %d\n", rendezvousMethod, binCount(
- m.clientDeniedCount[rendezvousMethod]+m.clientProxyMatchCount[rendezvousMethod],
- ))
- m.logger.Printf("client-%s-ips %s\n", rendezvousMethod, m.DisplayRendezvousStatsByCountry(rendezvousMethod))
- }
- m.logger.Println("snowflake-ips-nat-restricted", len(m.countryStats.natRestricted))
- m.logger.Println("snowflake-ips-nat-unrestricted", len(m.countryStats.natUnrestricted))
- m.logger.Println("snowflake-ips-nat-unknown", len(m.countryStats.natUnknown))
- m.lock.Unlock()
- }
- // Restores all metrics to original values
- func (m *Metrics) zeroMetrics() {
- m.proxyIdleCount = 0
- m.clientDeniedCount = make(map[messages.RendezvousMethod]uint)
- m.clientRestrictedDeniedCount = make(map[messages.RendezvousMethod]uint)
- m.clientUnrestrictedDeniedCount = make(map[messages.RendezvousMethod]uint)
- m.proxyPollRejectedWithRelayURLExtension = 0
- m.proxyPollWithRelayURLExtension = 0
- m.proxyPollWithoutRelayURLExtension = 0
- m.clientProxyMatchCount = make(map[messages.RendezvousMethod]uint)
- m.rendezvousCountryStats = make(map[messages.RendezvousMethod]map[string]int)
- for _, rendezvousMethod := range rendezvoudMethodList {
- m.rendezvousCountryStats[rendezvousMethod] = make(map[string]int)
- }
- m.countryStats.counts = make(map[string]int)
- for pType := range m.countryStats.proxies {
- m.countryStats.proxies[pType] = make(map[string]bool)
- }
- m.countryStats.unknown = make(map[string]bool)
- m.countryStats.natRestricted = make(map[string]bool)
- m.countryStats.natUnrestricted = make(map[string]bool)
- m.countryStats.natUnknown = make(map[string]bool)
- }
- // Rounds up a count to the nearest multiple of 8.
- func binCount(count uint) uint {
- return uint((math.Ceil(float64(count) / 8)) * 8)
- }
- func sumMapValues(m *map[messages.RendezvousMethod]uint) uint {
- var s uint = 0
- for _, v := range *m {
- s += v
- }
- return s
- }
- type PromMetrics struct {
- registry *prometheus.Registry
- ProxyTotal *prometheus.CounterVec
- ProxyPollTotal *safeprom.CounterVec
- ClientPollTotal *safeprom.CounterVec
- AvailableProxies *prometheus.GaugeVec
- ProxyPollWithRelayURLExtensionTotal *safeprom.CounterVec
- ProxyPollWithoutRelayURLExtensionTotal *safeprom.CounterVec
- ProxyPollRejectedForRelayURLExtensionTotal *safeprom.CounterVec
- }
- // Initialize metrics for prometheus exporter
- func initPrometheus() *PromMetrics {
- promMetrics := &PromMetrics{}
- promMetrics.registry = prometheus.NewRegistry()
- promMetrics.ProxyTotal = prometheus.NewCounterVec(
- prometheus.CounterOpts{
- Namespace: prometheusNamespace,
- Name: "proxy_total",
- Help: "The number of unique snowflake IPs",
- },
- []string{"type", "nat", "cc"},
- )
- promMetrics.AvailableProxies = prometheus.NewGaugeVec(
- prometheus.GaugeOpts{
- Namespace: prometheusNamespace,
- Name: "available_proxies",
- Help: "The number of currently available snowflake proxies",
- },
- []string{"type", "nat"},
- )
- promMetrics.ProxyPollTotal = safeprom.NewCounterVec(
- prometheus.CounterOpts{
- Namespace: prometheusNamespace,
- Name: "rounded_proxy_poll_total",
- Help: "The number of snowflake proxy polls, rounded up to a multiple of 8",
- },
- []string{"nat", "status"},
- )
- promMetrics.ProxyPollWithRelayURLExtensionTotal = safeprom.NewCounterVec(
- prometheus.CounterOpts{
- Namespace: prometheusNamespace,
- Name: "rounded_proxy_poll_with_relay_url_extension_total",
- Help: "The number of snowflake proxy polls with Relay URL Extension, rounded up to a multiple of 8",
- },
- []string{"nat", "type"},
- )
- promMetrics.ProxyPollWithoutRelayURLExtensionTotal = safeprom.NewCounterVec(
- prometheus.CounterOpts{
- Namespace: prometheusNamespace,
- Name: "rounded_proxy_poll_without_relay_url_extension_total",
- Help: "The number of snowflake proxy polls without Relay URL Extension, rounded up to a multiple of 8",
- },
- []string{"nat", "type"},
- )
- promMetrics.ProxyPollRejectedForRelayURLExtensionTotal = safeprom.NewCounterVec(
- prometheus.CounterOpts{
- Namespace: prometheusNamespace,
- Name: "rounded_proxy_poll_rejected_relay_url_extension_total",
- Help: "The number of snowflake proxy polls rejected by Relay URL Extension, rounded up to a multiple of 8",
- },
- []string{"nat", "type"},
- )
- promMetrics.ClientPollTotal = safeprom.NewCounterVec(
- prometheus.CounterOpts{
- Namespace: prometheusNamespace,
- Name: "rounded_client_poll_total",
- Help: "The number of snowflake client polls, rounded up to a multiple of 8",
- },
- []string{"nat", "status", "cc", "rendezvous_method"},
- )
- // We need to register our metrics so they can be exported.
- promMetrics.registry.MustRegister(
- promMetrics.ClientPollTotal, promMetrics.ProxyPollTotal,
- promMetrics.ProxyTotal, promMetrics.AvailableProxies,
- promMetrics.ProxyPollWithRelayURLExtensionTotal,
- promMetrics.ProxyPollWithoutRelayURLExtensionTotal,
- promMetrics.ProxyPollRejectedForRelayURLExtensionTotal,
- )
- return promMetrics
- }
|