-
Notifications
You must be signed in to change notification settings - Fork 0
/
kero.go
312 lines (282 loc) · 7.42 KB
/
kero.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
package kero
import (
"errors"
"strings"
"time"
"github.com/oschwald/geoip2-golang"
"github.com/prometheus/prometheus/model/labels"
"github.com/prometheus/prometheus/tsdb"
)
type Kero struct {
dbPath string
dbRetentionDuration int64
db *tsdb.DB
geoDB *geoip2.Reader
reverseLookupIP bool
DashboardPath string
PixelPath string
MeasureRequestDuration bool
IgnoreCommonPaths bool
IgnoreBots bool
IgnoreDNT bool
// path prefixes to which requests will be ignored. see file for default list.
IgnoredPrefixes []string
// path suffixes to which requestw will be ignored. see file for default list.
IgnoredSuffixes []string
// user-agent values to be ignored. see file for default list.
IgnoredAgents []string
}
type MetricLabels map[string]string
const MetricName = labels.MetricName
const HttpReqMetricName = "http_req"
const HttpReqDurationMetricName = "http_req_dur"
const HttpMethodLabel = "$http_method"
const HttpPathLabel = "$http_path"
const HttpRouteLabel = "$http_route"
const BrowserNameLabel = "$browser_name"
const BrowserVersionLabel = "$browser_version"
const BrowserDeviceLabel = "$browser_device"
const BrowserOSLabel = "$browser_os"
const BrowserOSVersionLabel = "$browser_os_version"
const BrowserFormFactorLabel = "$browser_form_factor"
const ReferrerLabel = "$referrer"
const ReferrerDomainLabel = "$referrer_domain"
const UTMContentLabel = "$utm_content"
const UTMMediumLabel = "$utm_medium"
const UTMSourceLabel = "$utm_source"
const UTMCampaignLabel = "$utm_campaign"
const UTMTermLabel = "$utm_term"
const ClickIdGoogleLabel = "$clid_go"
const ClickIdFbLabel = "$clid_fb"
const ClickIdMsLabel = "$clid_ms"
const ClickIdTwLabel = "$clid_tw"
const CountryLabel = "$country"
const RegionLabel = "$region"
const CityLabel = "$city"
const IsBotLabel = "$is_bot"
const VisitorIdLabel = "$visitor_id"
var defaultIgnoredPathPrefixes = []string{
"/.",
"/_",
// various bad bots testing for wordpress
"//",
"/wp",
"/public",
"/wordpress",
}
var defaultIgnoredPathSuffixes = []string{
".js",
".js.map",
".css",
".css.map",
".png",
".jpg",
".jpeg",
".webp",
".gif",
".svg",
".woff",
".woff2",
".otf",
".ttf",
".ico",
".mov",
".mpg",
".mpg3",
".mpg4",
".wav",
".ogg",
// various bad bots
".php",
".asp",
".aspx",
".wlwmanifest.xml",
}
var defaultIgnoredAgents = []string{
// go
"go-http-client",
"github.com/monaco-io",
"gentleman",
// node.js
"node-fetch",
"undici",
"axios",
// objective-c + swift
"alamofire",
"nsurlconnection",
"nsurlsession",
"urlsession",
"swifthttp",
// python
"python-", //-urlib3, -requests
// java
"apache-httpclient",
// php requests
"php-",
"zend",
"laminas",
"guzzlehttp",
// c#/.net todo
// C/c++ todo
// apps
"curl",
"wget",
"rapidapi",
"postman",
// Apple App Site Association
"aasa",
// RSS readers
"linkship",
"feedbin",
"feedly",
"artykul",
// others
"x11",
// render.com health check
"render",
"dataprovider.com",
"researchscan",
"zgrab",
"NetcraftSurveyAgent",
}
type KeroOption func(*Kero) error
// New automatically creates a new Kero database on-disk if one doesn't exist already.
// See WithXXX functions for option configuration.
func New(options ...KeroOption) (*Kero, error) {
k := &Kero{}
for _, option := range options {
if err := option(k); err != nil {
return nil, err
}
}
if len(k.dbPath) == 0 {
return nil, errors.New("missing Kero database path")
}
tsdbOpts := tsdb.DefaultOptions()
if k.dbRetentionDuration > 0 {
tsdbOpts.RetentionDuration = k.dbRetentionDuration
}
db, err := tsdb.Open(k.dbPath, nil, nil, tsdbOpts, nil)
if err != nil {
return nil, err
} else {
k.db = db
}
if len(k.DashboardPath) == 0 {
k.DashboardPath = "/_kero"
}
k.IgnoredPrefixes = defaultIgnoredPathPrefixes
k.IgnoredSuffixes = defaultIgnoredPathSuffixes
k.IgnoredAgents = defaultIgnoredAgents
return k, nil
}
// WithDB sets the location of the database folder. Automatically created if it doesn't exist.
func WithDB(dbPath string) KeroOption {
return func(k *Kero) error {
k.dbPath = dbPath
return nil
}
}
// WithDashboardPath sets the URL at which the dashboard will be mounted. Defaults to `"/_kero"`.
func WithDashboardPath(path string) KeroOption {
return func(k *Kero) error {
if !isValidPathArg(path) {
return errors.New("DashboardPath must start with / and have at least one more character")
}
k.DashboardPath = path
return nil
}
}
// WithGeoIPDB loads the MaxMind GeoLine2 and GeoIP2 database for IP-reverse lookup of visitors.
// If not provided, IP reverse-lookup is disabled.
func WithGeoIPDB(geoIPDBPath string) KeroOption {
return func(k *Kero) error {
if len(geoIPDBPath) == 0 {
return errors.New("GeoIP database path is empty")
}
geoDB, err := geoip2.Open(geoIPDBPath)
if err != nil {
return err
}
k.geoDB = geoDB
k.reverseLookupIP = true
return nil
}
}
// WithRequestMeasurements sets whether Kero should automatically measure response of handlers time.
func WithRequestMeasurements(value bool) KeroOption {
return func(k *Kero) error {
k.MeasureRequestDuration = value
return nil
}
}
// WithWebAssetsIgnored sets whether Kero should ignore requests made to common web assets.
//
// Ignored paths include:
//
// - .css and .js files
// - images (.png, .svg, .jpg, .webp, etc.)
// - fonts (.woff2, .ttf, .otf, etc.)
// - common URLs accessed by web scrapers (.php, .asp, etc.)
// - any path starting with /., /_, /wp- and /public.
//
// Kero's resources and paths are always excluded.
func WithWebAssetsIgnored(value bool) KeroOption {
return func(k *Kero) error {
k.IgnoreCommonPaths = value
return nil
}
}
// WithBotsIgnored sets whether Kero should ignore requests made by bots, web scrapers and HTTP libraries.
// Bots and libraries are detected using their `User-Agent` header.
func WithBotsIgnored(value bool) KeroOption {
return func(k *Kero) error {
k.IgnoreBots = value
return nil
}
}
// WithDntIgnored sets whether Kero should ignore value of DNT header. If configured to ignore,
// requests with DNT: 1 (opt-out of tracking) will be still tracked.
func WithDntIgnored(value bool) KeroOption {
return func(k *Kero) error {
k.IgnoreDNT = value
return nil
}
}
// WithRetention defines the how long data points should be kept. Defaults to 15 days.
func WithRetention(duration time.Duration) KeroOption {
return func(k *Kero) error {
k.dbRetentionDuration = duration.Milliseconds()
return nil
}
}
// WithPixelPath defines the route at which the pixel tracker will be available to external applications.
// The pixel can be referenced from static websites or services not directly served by the Go server.
// Requests referer header will be used as the path, with other headers and query parameters used unchanged.
// Response of the pixel path will be a 1x1px transparent GIF.
func WithPixelPath(path string) KeroOption {
return func(k *Kero) error {
if !isValidPathArg(path) {
return errors.New("PixelPath must start with / and have at least one more character")
}
k.PixelPath = path
return nil
}
}
func (k *Kero) Close() error {
return k.db.Close()
}
func mergeMaps(maps ...map[string]string) map[string]string {
result := make(map[string]string)
for _, m := range maps {
for k, v := range m {
if len(v) > 0 {
result[k] = v
}
}
}
return result
}
func isValidPathArg(path string) bool {
return len(path) >= 2 && strings.HasPrefix(path, "/")
}