metrics.go 4.52 KB
Newer Older
1
/*
2
3
We export metrics in the format specified in our broker spec:
https://gitweb.torproject.org/pluggable-transports/snowflake.git/tree/doc/broker-spec.txt
4
5
*/

Hooman's avatar
Hooman committed
6
package main
7
8

import (
9
10
	"fmt"
	"log"
11
	"math"
12
13
	"net"
	"sync"
14
15
16
	"time"
)

17
18
19
20
var (
	once sync.Once
)

21
const metricsResolution = 60 * 60 * 24 * time.Second //86400 seconds
22

23
type CountryStats struct {
24
25
26
27
28
	standalone map[string]bool
	badge      map[string]bool
	webext     map[string]bool
	unknown    map[string]bool
	counts     map[string]int
29
30
}

31
32
// Implements Observable
type Metrics struct {
33
34
35
36
37
	logger  *log.Logger
	tablev4 *GeoIPv4Table
	tablev6 *GeoIPv6Table

	countryStats            CountryStats
38
	clientRoundtripEstimate time.Duration
39
40
41
	proxyIdleCount          uint
	clientDeniedCount       uint
	clientProxyMatchCount   uint
42
43
44

	//synchronization for access to snowflake metrics
	lock sync.Mutex
45
46
}

47
func (s CountryStats) Display() string {
48
49
50
51
	output := ""
	for cc, count := range s.counts {
		output += fmt.Sprintf("%s=%d,", cc, count)
	}
52
53
54
55
56
57

	// cut off trailing ","
	if len(output) > 0 {
		return output[:len(output)-1]
	}

58
	return output
59
60
}

61
func (m *Metrics) UpdateCountryStats(addr string, proxyType string) {
62
63
64
65

	var country string
	var ok bool

66
	if proxyType == "standalone" {
67
68
69
		if m.countryStats.standalone[addr] {
			return
		}
70
	} else if proxyType == "badge" {
71
72
73
		if m.countryStats.badge[addr] {
			return
		}
74
	} else if proxyType == "webext" {
75
76
77
78
79
80
81
		if m.countryStats.webext[addr] {
			return
		}
	} else {
		if m.countryStats.unknown[addr] {
			return
		}
82
83
	}

84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
	ip := net.ParseIP(addr)
	if ip.To4() != nil {
		//This is an IPv4 address
		if m.tablev4 == nil {
			return
		}
		country, ok = GetCountryByAddr(m.tablev4, ip)
	} else {
		if m.tablev6 == nil {
			return
		}
		country, ok = GetCountryByAddr(m.tablev6, ip)
	}

	if !ok {
		country = "??"
	}

102
	//update map of unique ips and counts
103
	m.countryStats.counts[country]++
104
	if proxyType == "standalone" {
105
		m.countryStats.standalone[addr] = true
106
	} else if proxyType == "badge" {
107
		m.countryStats.badge[addr] = true
108
	} else if proxyType == "webext" {
109
110
111
112
		m.countryStats.webext[addr] = true
	} else {
		m.countryStats.unknown[addr] = true
	}
113
114
115
116
117
118
119
120
121
122
123
124
125

}

func (m *Metrics) LoadGeoipDatabases(geoipDB string, geoip6DB string) error {

	// Load geoip databases
	log.Println("Loading geoip databases")
	tablev4 := new(GeoIPv4Table)
	err := GeoIPLoadFile(tablev4, geoipDB)
	if err != nil {
		m.tablev4 = nil
		return err
	}
126
	m.tablev4 = tablev4
127
128
129
130
131
132
133

	tablev6 := new(GeoIPv6Table)
	err = GeoIPLoadFile(tablev6, geoip6DB)
	if err != nil {
		m.tablev6 = nil
		return err
	}
134
	m.tablev6 = tablev6
135
136
137
	return nil
}

138
func NewMetrics(metricsLogger *log.Logger) (*Metrics, error) {
139
	m := new(Metrics)
140
141

	m.countryStats = CountryStats{
142
143
144
145
146
		counts:     make(map[string]int),
		standalone: make(map[string]bool),
		badge:      make(map[string]bool),
		webext:     make(map[string]bool),
		unknown:    make(map[string]bool),
147
148
	}

149
150
	m.logger = metricsLogger

151
	// Write to log file every hour with updated metrics
152
153
154
155
	go once.Do(m.logMetrics)

	return m, nil
}
156

Cecylia Bocovich's avatar
Cecylia Bocovich committed
157
// Logs metrics in intervals specified by metricsResolution
158
159
160
func (m *Metrics) logMetrics() {
	heartbeat := time.Tick(metricsResolution)
	for range heartbeat {
Cecylia Bocovich's avatar
Cecylia Bocovich committed
161
162
		m.printMetrics()
		m.zeroMetrics()
163
	}
164
}
165

Cecylia Bocovich's avatar
Cecylia Bocovich committed
166
func (m *Metrics) printMetrics() {
167
	m.lock.Lock()
168
	m.logger.Println("snowflake-stats-end", time.Now().UTC().Format("2006-01-02 15:04:05"), fmt.Sprintf("(%d s)", int(metricsResolution.Seconds())))
Cecylia Bocovich's avatar
Cecylia Bocovich committed
169
	m.logger.Println("snowflake-ips", m.countryStats.Display())
170
171
172
173
174
	m.logger.Println("snowflake-ips-total", len(m.countryStats.standalone)+
		len(m.countryStats.badge)+len(m.countryStats.webext)+len(m.countryStats.unknown))
	m.logger.Println("snowflake-ips-standalone", len(m.countryStats.standalone))
	m.logger.Println("snowflake-ips-badge", len(m.countryStats.badge))
	m.logger.Println("snowflake-ips-webext", len(m.countryStats.webext))
Cecylia Bocovich's avatar
Cecylia Bocovich committed
175
176
177
	m.logger.Println("snowflake-idle-count", binCount(m.proxyIdleCount))
	m.logger.Println("client-denied-count", binCount(m.clientDeniedCount))
	m.logger.Println("client-snowflake-match-count", binCount(m.clientProxyMatchCount))
178
	m.lock.Unlock()
Cecylia Bocovich's avatar
Cecylia Bocovich committed
179
180
181
182
183
184
185
186
}

// Restores all metrics to original values
func (m *Metrics) zeroMetrics() {
	m.proxyIdleCount = 0
	m.clientDeniedCount = 0
	m.clientProxyMatchCount = 0
	m.countryStats.counts = make(map[string]int)
187
188
189
190
	m.countryStats.standalone = make(map[string]bool)
	m.countryStats.badge = make(map[string]bool)
	m.countryStats.webext = make(map[string]bool)
	m.countryStats.unknown = make(map[string]bool)
Cecylia Bocovich's avatar
Cecylia Bocovich committed
191
192
}

193
// Rounds up a count to the nearest multiple of 8.
194
195
func binCount(count uint) uint {
	return uint((math.Ceil(float64(count) / 8)) * 8)
196
}