metrics.go 5.74 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
	standalone map[string]bool
	badge      map[string]bool
	webext     map[string]bool
	unknown    map[string]bool
28
29
30
31
32
33

	natRestricted   map[string]bool
	natUnrestricted map[string]bool
	natUnknown      map[string]bool

	counts map[string]int
34
35
}

36
37
// Implements Observable
type Metrics struct {
38
39
40
41
	logger  *log.Logger
	tablev4 *GeoIPv4Table
	tablev6 *GeoIPv6Table

42
43
44
45
46
47
48
	countryStats                  CountryStats
	clientRoundtripEstimate       time.Duration
	proxyIdleCount                uint
	clientDeniedCount             uint
	clientRestrictedDeniedCount   uint
	clientUnrestrictedDeniedCount uint
	clientProxyMatchCount         uint
49
50
51

	//synchronization for access to snowflake metrics
	lock sync.Mutex
52
53
}

54
func (s CountryStats) Display() string {
55
56
57
58
	output := ""
	for cc, count := range s.counts {
		output += fmt.Sprintf("%s=%d,", cc, count)
	}
59
60
61
62
63
64

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

65
	return output
66
67
}

68
func (m *Metrics) UpdateCountryStats(addr string, proxyType string, natType string) {
69
70
71
72

	var country string
	var ok bool

73
	if proxyType == "standalone" {
74
75
76
		if m.countryStats.standalone[addr] {
			return
		}
77
	} else if proxyType == "badge" {
78
79
80
		if m.countryStats.badge[addr] {
			return
		}
81
	} else if proxyType == "webext" {
82
83
84
85
86
87
88
		if m.countryStats.webext[addr] {
			return
		}
	} else {
		if m.countryStats.unknown[addr] {
			return
		}
89
90
	}

91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
	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 = "??"
	}

109
	//update map of unique ips and counts
110
	m.countryStats.counts[country]++
111
	if proxyType == "standalone" {
112
		m.countryStats.standalone[addr] = true
113
	} else if proxyType == "badge" {
114
		m.countryStats.badge[addr] = true
115
	} else if proxyType == "webext" {
116
117
118
119
		m.countryStats.webext[addr] = true
	} else {
		m.countryStats.unknown[addr] = true
	}
120

121
122
123
124
125
126
127
128
129
	switch natType {
	case NATRestricted:
		m.countryStats.natRestricted[addr] = true
	case NATUnrestricted:
		m.countryStats.natUnrestricted[addr] = true
	default:
		m.countryStats.natUnknown[addr] = true
	}

130
131
132
133
134
135
136
137
138
139
140
141
}

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
	}
142
	m.tablev4 = tablev4
143
144
145
146
147
148
149

	tablev6 := new(GeoIPv6Table)
	err = GeoIPLoadFile(tablev6, geoip6DB)
	if err != nil {
		m.tablev6 = nil
		return err
	}
150
	m.tablev6 = tablev6
151
152
153
	return nil
}

154
func NewMetrics(metricsLogger *log.Logger) (*Metrics, error) {
155
	m := new(Metrics)
156
157

	m.countryStats = CountryStats{
158
159
160
161
162
163
164
165
		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),
		natRestricted:   make(map[string]bool),
		natUnrestricted: make(map[string]bool),
		natUnknown:      make(map[string]bool),
166
167
	}

168
169
	m.logger = metricsLogger

170
	// Write to log file every hour with updated metrics
171
172
173
174
	go once.Do(m.logMetrics)

	return m, nil
}
175

Cecylia Bocovich's avatar
Cecylia Bocovich committed
176
// Logs metrics in intervals specified by metricsResolution
177
178
179
func (m *Metrics) logMetrics() {
	heartbeat := time.Tick(metricsResolution)
	for range heartbeat {
Cecylia Bocovich's avatar
Cecylia Bocovich committed
180
181
		m.printMetrics()
		m.zeroMetrics()
182
	}
183
}
184

Cecylia Bocovich's avatar
Cecylia Bocovich committed
185
func (m *Metrics) printMetrics() {
186
	m.lock.Lock()
187
	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
188
	m.logger.Println("snowflake-ips", m.countryStats.Display())
189
190
191
192
193
	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
194
195
	m.logger.Println("snowflake-idle-count", binCount(m.proxyIdleCount))
	m.logger.Println("client-denied-count", binCount(m.clientDeniedCount))
196
197
	m.logger.Println("client-restricted-denied-count", binCount(m.clientRestrictedDeniedCount))
	m.logger.Println("client-unrestricted-denied-count", binCount(m.clientUnrestrictedDeniedCount))
Cecylia Bocovich's avatar
Cecylia Bocovich committed
198
	m.logger.Println("client-snowflake-match-count", binCount(m.clientProxyMatchCount))
199
200
201
	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))
202
	m.lock.Unlock()
Cecylia Bocovich's avatar
Cecylia Bocovich committed
203
204
205
206
207
208
}

// Restores all metrics to original values
func (m *Metrics) zeroMetrics() {
	m.proxyIdleCount = 0
	m.clientDeniedCount = 0
209
210
	m.clientRestrictedDeniedCount = 0
	m.clientUnrestrictedDeniedCount = 0
Cecylia Bocovich's avatar
Cecylia Bocovich committed
211
212
	m.clientProxyMatchCount = 0
	m.countryStats.counts = make(map[string]int)
213
214
215
216
	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)
217
218
219
	m.countryStats.natRestricted = make(map[string]bool)
	m.countryStats.natUnrestricted = make(map[string]bool)
	m.countryStats.natUnknown = make(map[string]bool)
Cecylia Bocovich's avatar
Cecylia Bocovich committed
220
221
}

222
// Rounds up a count to the nearest multiple of 8.
223
224
func binCount(count uint) uint {
	return uint((math.Ceil(float64(count) / 8)) * 8)
225
}