@hyperwatch/hyperwatch 4.2.0 → 5.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -4
- package/config/apache_syslog_example.js +29 -0
- package/config/default.js +2 -2
- package/config/example.js +5 -5
- package/config/express_websocket_example.js +28 -0
- package/config/websocket_client_example.js +52 -0
- package/docs/configuration.md +67 -6
- package/docs/express-embedding.md +137 -0
- package/docs/input.md +50 -24
- package/docs/tutorials/apache_input.md +16 -14
- package/docs/tutorials/express_input.md +10 -8
- package/package.json +42 -35
- package/scripts/fetch-anthropic-ips.js +60 -0
- package/scripts/fetch-cloudflare-ips.js +27 -0
- package/scripts/fetch-cloudfront-ips.js +29 -0
- package/scripts/fetch-openai-ips.js +34 -0
- package/src/app/api.js +149 -8
- package/src/app/index.js +8 -4
- package/src/app/mount.js +115 -0
- package/src/app/websocket.js +39 -10
- package/src/app/ws-server.js +123 -0
- package/src/constants.js +17 -5
- package/src/data/amazon-searchbot-ips.json +818 -0
- package/src/data/amazon-user-ips.json +1025 -0
- package/src/data/amazonbot-ips.json +1294 -0
- package/src/data/chatgpt-user-ips.json +231 -0
- package/src/data/claude-bot-ips.json +28 -0
- package/src/data/cloudflare-ips.json +24 -0
- package/src/data/cloudfront-ips.json +245 -0
- package/src/data/gptbot-ips.json +20 -0
- package/src/data/openai-searchbot-ips.json +41 -0
- package/src/index.js +24 -3
- package/src/input/http.js +4 -0
- package/src/input/syslog.js +5 -1
- package/src/input/websocket.js +35 -19
- package/src/lib/aggregator.js +169 -20
- package/src/lib/formatter.js +10 -1
- package/src/lib/index.js +2 -0
- package/src/lib/log-buffer.js +45 -0
- package/src/lib/persistence.js +82 -0
- package/src/lib/pipeline.js +122 -11
- package/src/lib/recent-map.js +23 -0
- package/src/lib/speed.js +43 -3
- package/src/lib/util.js +17 -1
- package/src/modules/address.js +59 -2
- package/src/modules/agent.js +1 -1
- package/src/modules/dnsbl.js +11 -1
- package/src/modules/history.js +62 -0
- package/src/modules/identity.js +154 -26
- package/src/modules/index.js +16 -5
- package/src/modules/language.js +1 -1
- package/src/modules/signature.js +64 -18
- package/src/modules/sparkline.js +7 -3
- package/src/modules/status.js +13 -8
- package/src/plugins/proxy.js +10 -203
- package/src/script.js +0 -1
- package/scripts/cloudfront-ips.js +0 -56
package/src/modules/identity.js
CHANGED
|
@@ -1,15 +1,31 @@
|
|
|
1
|
-
const IPCIDR = require('ip-cidr');
|
|
1
|
+
const IPCIDR = require('ip-cidr').default;
|
|
2
2
|
|
|
3
3
|
const api = require('../app/api');
|
|
4
|
+
// Bot IP lists for identity verification
|
|
5
|
+
// Run `node scripts/fetch-openai-ips.js` to update OpenAI lists
|
|
6
|
+
// Run `node scripts/fetch-anthropic-ips.js` to update the Claude list
|
|
7
|
+
const amazonSearchBotIps = require('../data/amazon-searchbot-ips.json');
|
|
8
|
+
const amazonUserIps = require('../data/amazon-user-ips.json');
|
|
9
|
+
const amazonBotIps = require('../data/amazonbot-ips.json');
|
|
10
|
+
const chatgptUserIps = require('../data/chatgpt-user-ips.json');
|
|
11
|
+
const claudeBotIps = require('../data/claude-bot-ips.json');
|
|
12
|
+
const gptbotIps = require('../data/gptbot-ips.json');
|
|
13
|
+
const openaiSearchbotIps = require('../data/openai-searchbot-ips.json');
|
|
4
14
|
const { Aggregator } = require('../lib/aggregator');
|
|
5
15
|
const pipeline = require('../lib/pipeline');
|
|
6
16
|
|
|
17
|
+
// Anthropic publishes one list of ranges covering all Claude crawlers.
|
|
18
|
+
// Reverse DNS is not usable here: Claude crawlers run on shared cloud
|
|
19
|
+
// infrastructure, so their PTR records are not Anthropic-controlled.
|
|
20
|
+
const claudeBotCidrs = claudeBotIps.map((cidr) => new IPCIDR(cidr));
|
|
21
|
+
|
|
7
22
|
function augment(log) {
|
|
8
23
|
const family = log.getIn(['agent', 'family']);
|
|
9
24
|
const hostname = log.getIn(['address', 'hostname']);
|
|
10
25
|
const address =
|
|
11
26
|
log.getIn(['address', 'value']) || log.getIn(['request', 'address']);
|
|
12
27
|
const signature = log.getIn(['signature', 'id']);
|
|
28
|
+
const signatureAgent = log.getIn(['request', 'headers', 'signature-agent']);
|
|
13
29
|
|
|
14
30
|
switch (family) {
|
|
15
31
|
// Per hostname
|
|
@@ -69,6 +85,7 @@ function augment(log) {
|
|
|
69
85
|
return hostname && hostname.endsWith('.mojeek.com')
|
|
70
86
|
? log.set('identity', 'Mojeek')
|
|
71
87
|
: log;
|
|
88
|
+
case 'AwarioBot':
|
|
72
89
|
case 'BLEXBot':
|
|
73
90
|
return hostname && hostname.endsWith('.webmeup.com')
|
|
74
91
|
? log.set('identity', 'WebMeUp')
|
|
@@ -89,13 +106,11 @@ function augment(log) {
|
|
|
89
106
|
return hostname && hostname.endsWith('.naver.com')
|
|
90
107
|
? log.set('identity', 'Naver')
|
|
91
108
|
: log;
|
|
92
|
-
|
|
93
|
-
return hostname && hostname.endsWith('.fbsv.net')
|
|
94
|
-
? log.set('identity', 'Facebook')
|
|
95
|
-
: log;
|
|
109
|
+
|
|
96
110
|
case 'Stripe':
|
|
111
|
+
case 'Stripebot':
|
|
97
112
|
return hostname && hostname.endsWith('.stripe.com')
|
|
98
|
-
? log.set('identity',
|
|
113
|
+
? log.set('identity', 'Stripe')
|
|
99
114
|
: log;
|
|
100
115
|
case 'UptimeRobot':
|
|
101
116
|
return hostname && hostname.endsWith('.uptimerobot.com')
|
|
@@ -127,9 +142,17 @@ function augment(log) {
|
|
|
127
142
|
? log.set('identity', 'Sirportly')
|
|
128
143
|
: log;
|
|
129
144
|
case 'Bytespider':
|
|
130
|
-
return hostname &&
|
|
145
|
+
return hostname &&
|
|
146
|
+
(hostname.endsWith('.crawl.bytedance.com') ||
|
|
147
|
+
hostname.endsWith('.ap-southeast-1.compute.amazonaws.com'))
|
|
131
148
|
? log.set('identity', family)
|
|
132
149
|
: log;
|
|
150
|
+
case 'TikTokSpider':
|
|
151
|
+
return hostname &&
|
|
152
|
+
(hostname.endsWith('.crawl.bytedance.com') ||
|
|
153
|
+
hostname.endsWith('.ap-southeast-1.compute.amazonaws.com'))
|
|
154
|
+
? log.set('identity', 'TikTok')
|
|
155
|
+
: log;
|
|
133
156
|
case 'Mail.RU Bot':
|
|
134
157
|
case 'Mail.RU Bot Img':
|
|
135
158
|
case 'Mail.RU Bot Fast':
|
|
@@ -162,6 +185,11 @@ function augment(log) {
|
|
|
162
185
|
return hostname && hostname.endsWith('.babbar.eu')
|
|
163
186
|
? log.set('identity', 'Babbar')
|
|
164
187
|
: log;
|
|
188
|
+
case 'Reflectionbot':
|
|
189
|
+
// https://reflection.ai/bot
|
|
190
|
+
return hostname && hostname.endsWith('.reflection.ai')
|
|
191
|
+
? log.set('identity', 'Reflection')
|
|
192
|
+
: log;
|
|
165
193
|
case 'bnf.fr bot':
|
|
166
194
|
return hostname && hostname.endsWith('.bnf.fr')
|
|
167
195
|
? log.set('identity', 'BnF.fr')
|
|
@@ -182,6 +210,14 @@ function augment(log) {
|
|
|
182
210
|
return hostname && hostname.endsWith('.neevabot.com')
|
|
183
211
|
? log.set('identity', 'Neevabot')
|
|
184
212
|
: log;
|
|
213
|
+
case 'RootCrawl-Crawler':
|
|
214
|
+
return hostname && hostname.endsWith('.rootcrawl.org')
|
|
215
|
+
? log.set('identity', 'RootCrawl')
|
|
216
|
+
: log;
|
|
217
|
+
case 'bl.uk ldfc bot':
|
|
218
|
+
return hostname && hostname.endsWith('.bl.uk')
|
|
219
|
+
? log.set('identity', 'British Library')
|
|
220
|
+
: log;
|
|
185
221
|
case 'DataForSeoBot':
|
|
186
222
|
return hostname && hostname.endsWith('.dataforseo.com')
|
|
187
223
|
? log.set('identity', 'DataForSeo')
|
|
@@ -196,9 +232,40 @@ function augment(log) {
|
|
|
196
232
|
? log.set('identity', 'InfoTiger')
|
|
197
233
|
: log;
|
|
198
234
|
case 'Amazonbot':
|
|
199
|
-
return hostname && hostname.endsWith('.crawl.amazonbot.amazon')
|
|
235
|
+
return (hostname && hostname.endsWith('.crawl.amazonbot.amazon')) ||
|
|
236
|
+
amazonBotIps.some((cidr) => new IPCIDR(cidr).contains(address))
|
|
200
237
|
? log.set('identity', 'Amazonbot')
|
|
201
238
|
: log;
|
|
239
|
+
case 'Amzn-SearchBot':
|
|
240
|
+
return (hostname && hostname.endsWith('.crawl.amazonbot.amazon')) ||
|
|
241
|
+
amazonSearchBotIps.some((cidr) => new IPCIDR(cidr).contains(address))
|
|
242
|
+
? log.set('identity', 'Amazon SearchBot')
|
|
243
|
+
: log;
|
|
244
|
+
case 'Amzn-User':
|
|
245
|
+
return (hostname && hostname.endsWith('.crawl.amazonbot.amazon')) ||
|
|
246
|
+
amazonUserIps.some((cidr) => new IPCIDR(cidr).contains(address))
|
|
247
|
+
? log.set('identity', 'Amazon User')
|
|
248
|
+
: log;
|
|
249
|
+
case 'SERankingBacklinksBot':
|
|
250
|
+
return hostname && hostname.endsWith('.blex.seranking.com')
|
|
251
|
+
? log.set('identity', 'SE Ranking')
|
|
252
|
+
: log;
|
|
253
|
+
case 'SofyaBot':
|
|
254
|
+
return hostname && hostname.endsWith('.sofya.co')
|
|
255
|
+
? log.set('identity', 'Sofya')
|
|
256
|
+
: log;
|
|
257
|
+
case 'YouBot':
|
|
258
|
+
// https://docs.you.com/youbot
|
|
259
|
+
return hostname && hostname.endsWith('.search.you.com')
|
|
260
|
+
? log.set('identity', 'You.com')
|
|
261
|
+
: log;
|
|
262
|
+
case 'AIWebIndex':
|
|
263
|
+
case 'AIWebIndex-Agent':
|
|
264
|
+
// Lyrenth AI-readable web index, forward-confirmed rDNS under lyrenth.com
|
|
265
|
+
// https://lyrenth.com/bot
|
|
266
|
+
return hostname && hostname.endsWith('.lyrenth.com')
|
|
267
|
+
? log.set('identity', 'Lyrenth')
|
|
268
|
+
: log;
|
|
202
269
|
|
|
203
270
|
// Per hostname + CIDR
|
|
204
271
|
case 'Twitterbot':
|
|
@@ -211,6 +278,11 @@ function augment(log) {
|
|
|
211
278
|
(address && new IPCIDR('2a02:598::/32').contains(address))
|
|
212
279
|
? log.set('identity', 'Seznam')
|
|
213
280
|
: log;
|
|
281
|
+
case 'FacebookBot':
|
|
282
|
+
return (hostname && hostname.endsWith('.fbsv.net')) ||
|
|
283
|
+
(address && new IPCIDR('2a03:2880::/29').contains(address))
|
|
284
|
+
? log.set('identity', 'Facebook')
|
|
285
|
+
: log;
|
|
214
286
|
|
|
215
287
|
// Per CIDR
|
|
216
288
|
case 'github-camo':
|
|
@@ -221,6 +293,10 @@ function augment(log) {
|
|
|
221
293
|
return address && new IPCIDR('216.244.64.0/19').contains(address)
|
|
222
294
|
? log.set('identity', 'Moz')
|
|
223
295
|
: log;
|
|
296
|
+
case 'AliyunSecBot':
|
|
297
|
+
return address && new IPCIDR('8.217.0.0/16').contains(address)
|
|
298
|
+
? log.set('identity', family)
|
|
299
|
+
: log;
|
|
224
300
|
case '360Spider':
|
|
225
301
|
return address && new IPCIDR('42.236.10.0/24').contains(address)
|
|
226
302
|
? log.set('identity', family)
|
|
@@ -230,26 +306,33 @@ function augment(log) {
|
|
|
230
306
|
? log.set('identity', family)
|
|
231
307
|
: log;
|
|
232
308
|
case 'OAI-SearchBot':
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
new IPCIDR('172.203.190.128/28').contains(address) ||
|
|
237
|
-
new IPCIDR('51.8.102.0/24').contains(address))
|
|
309
|
+
return openaiSearchbotIps.some((cidr) =>
|
|
310
|
+
new IPCIDR(cidr).contains(address)
|
|
311
|
+
)
|
|
238
312
|
? log.set('identity', 'OpenAI SearchBot')
|
|
239
313
|
: log;
|
|
240
314
|
case 'GPTBot':
|
|
241
|
-
|
|
242
|
-
return address &&
|
|
243
|
-
(new IPCIDR('52.230.152.0/24').contains(address) ||
|
|
244
|
-
new IPCIDR('52.233.106.0/24').contains(address) ||
|
|
245
|
-
new IPCIDR('20.171.206.0/24').contains(address) ||
|
|
246
|
-
new IPCIDR('20.171.207.0/24').contains(address) ||
|
|
247
|
-
new IPCIDR('4.227.36.0/25').contains(address))
|
|
315
|
+
return gptbotIps.some((cidr) => new IPCIDR(cidr).contains(address))
|
|
248
316
|
? log.set('identity', 'OpenAI GPTBot')
|
|
249
317
|
: log;
|
|
318
|
+
case 'ChatGPT-User':
|
|
319
|
+
// https://openai.com/chatgpt-user.json
|
|
320
|
+
return chatgptUserIps.some((cidr) => new IPCIDR(cidr).contains(address))
|
|
321
|
+
? log.set('identity', 'ChatGPT')
|
|
322
|
+
: log;
|
|
323
|
+
case 'ClaudeBot':
|
|
324
|
+
case 'Claude-User':
|
|
325
|
+
case 'Claude-SearchBot':
|
|
326
|
+
case 'Claude-Web':
|
|
327
|
+
case 'anthropic-ai':
|
|
328
|
+
// https://claude.com/crawling/bots.json
|
|
329
|
+
return address && claudeBotCidrs.some((cidr) => cidr.contains(address))
|
|
330
|
+
? log.set('identity', 'Claude')
|
|
331
|
+
: log;
|
|
250
332
|
case 'meta-externalagent':
|
|
251
|
-
|
|
252
|
-
|
|
333
|
+
case 'meta-webindexer':
|
|
334
|
+
return address && new IPCIDR('2a03:2880::/29').contains(address)
|
|
335
|
+
? log.set('identity', 'Meta')
|
|
253
336
|
: log;
|
|
254
337
|
|
|
255
338
|
// EC2
|
|
@@ -288,16 +371,38 @@ function augment(log) {
|
|
|
288
371
|
hostname.endsWith('.eu-central-1.compute.amazonaws.com')
|
|
289
372
|
? log.set('identity', 'Wise')
|
|
290
373
|
: log;
|
|
291
|
-
case '
|
|
292
|
-
return hostname && hostname.endsWith('.
|
|
293
|
-
? log.set('identity', '
|
|
374
|
+
case 'PerplexityBot':
|
|
375
|
+
return hostname && hostname.endsWith('.compute-1.amazonaws.com')
|
|
376
|
+
? log.set('identity', 'Perplexity')
|
|
294
377
|
: log;
|
|
295
378
|
|
|
296
379
|
// GCE
|
|
380
|
+
case 'Aranet-SearchBot':
|
|
381
|
+
return hostname && hostname.endsWith('.googleusercontent.com')
|
|
382
|
+
? log.set('identity', 'Aranet')
|
|
383
|
+
: log;
|
|
384
|
+
case 'Discordbot':
|
|
385
|
+
return hostname && hostname.endsWith('.googleusercontent.com')
|
|
386
|
+
? log.set('identity', 'Discord')
|
|
387
|
+
: log;
|
|
297
388
|
case 'VelenPublicWebCrawler':
|
|
298
389
|
return hostname && hostname.endsWith('.googleusercontent.com')
|
|
299
390
|
? log.set('identity', 'Velen')
|
|
300
391
|
: log;
|
|
392
|
+
case 'SleepBot':
|
|
393
|
+
return hostname && hostname.endsWith('.googleusercontent.com')
|
|
394
|
+
? log.set('identity', 'SleepBot')
|
|
395
|
+
: log;
|
|
396
|
+
case 'DolfeEngineCrawler':
|
|
397
|
+
return hostname && hostname.endsWith('.googleusercontent.com')
|
|
398
|
+
? log.set('identity', 'Dolfe')
|
|
399
|
+
: log;
|
|
400
|
+
case 'ShapBot':
|
|
401
|
+
// Parallel Web Systems crawler, runs on GCE
|
|
402
|
+
// https://docs.parallel.ai/resources/crawler
|
|
403
|
+
return hostname && hostname.endsWith('.googleusercontent.com')
|
|
404
|
+
? log.set('identity', 'Parallel')
|
|
405
|
+
: log;
|
|
301
406
|
|
|
302
407
|
// Hetzner
|
|
303
408
|
case 'Ubermetrics':
|
|
@@ -312,6 +417,27 @@ function augment(log) {
|
|
|
312
417
|
return hostname && hostname.endsWith('.clients.your-server.de')
|
|
313
418
|
? log.set('identity', 'MegaIndex.ru')
|
|
314
419
|
: log;
|
|
420
|
+
case 'ev-crawler':
|
|
421
|
+
return hostname && hostname.endsWith('.headline.com')
|
|
422
|
+
? log.set('identity', 'Headline')
|
|
423
|
+
: log;
|
|
424
|
+
case 'SentryUptimeBot':
|
|
425
|
+
return hostname && hostname.endsWith('.googleusercontent.com')
|
|
426
|
+
? log.set('identity', 'Sentry')
|
|
427
|
+
: log;
|
|
428
|
+
case 'FlipboardProxy':
|
|
429
|
+
return hostname && hostname.endsWith('.flipboard.com')
|
|
430
|
+
? log.set('identity', 'Flipboard')
|
|
431
|
+
: log;
|
|
432
|
+
|
|
433
|
+
// Web Bot Auth
|
|
434
|
+
case 'ExaSearchBot':
|
|
435
|
+
// Exa search crawler. No published IP ranges or reverse DNS: requests
|
|
436
|
+
// are signed (RFC 9421) and carry a `Signature-Agent` header
|
|
437
|
+
// https://crawler.exa.ai/
|
|
438
|
+
return signatureAgent && signatureAgent.includes('https://crawler.exa.ai')
|
|
439
|
+
? log.set('identity', 'Exa')
|
|
440
|
+
: log;
|
|
315
441
|
}
|
|
316
442
|
|
|
317
443
|
// Hostname only
|
|
@@ -369,7 +495,9 @@ function start() {
|
|
|
369
495
|
|
|
370
496
|
aggregator.setIdentifier(identifier);
|
|
371
497
|
|
|
372
|
-
pipeline
|
|
498
|
+
pipeline
|
|
499
|
+
.getNode('main')
|
|
500
|
+
.map((log) => aggregator.processLog(log), 'aggregator');
|
|
373
501
|
|
|
374
502
|
api.registerAggregator('identities', aggregator);
|
|
375
503
|
}
|
package/src/modules/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const debug = require('debug');
|
|
2
2
|
|
|
3
3
|
const constants = require('../constants');
|
|
4
|
+
const pipeline = require('../lib/pipeline');
|
|
4
5
|
|
|
5
6
|
const debugModules = debug('hyperwatch:modules');
|
|
6
7
|
|
|
@@ -16,6 +17,8 @@ function get(module) {
|
|
|
16
17
|
return require('./dnsbl');
|
|
17
18
|
case 'geoip':
|
|
18
19
|
return require('./geoip');
|
|
20
|
+
case 'history':
|
|
21
|
+
return require('./history');
|
|
19
22
|
case 'hostname':
|
|
20
23
|
return require('./hostname');
|
|
21
24
|
case 'identity':
|
|
@@ -36,27 +39,35 @@ function get(module) {
|
|
|
36
39
|
}
|
|
37
40
|
}
|
|
38
41
|
|
|
39
|
-
function
|
|
42
|
+
function activeModulesWithKeys() {
|
|
40
43
|
return Object.keys(constants.modules)
|
|
41
44
|
.map((key) => Object.assign({ key }, constants.modules[key]))
|
|
42
45
|
.sort((a, b) => a.priority - b.priority)
|
|
43
46
|
.filter((m) => m.active === true)
|
|
44
|
-
.map((m) => get(m.key))
|
|
45
|
-
.filter((m) => m);
|
|
47
|
+
.map((m) => ({ key: m.key, module: get(m.key) }))
|
|
48
|
+
.filter((m) => m.module);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function activeModules() {
|
|
52
|
+
return activeModulesWithKeys().map((m) => m.module);
|
|
46
53
|
}
|
|
47
54
|
|
|
48
55
|
function init() {
|
|
49
|
-
for (const module of
|
|
56
|
+
for (const { key, module } of activeModulesWithKeys()) {
|
|
50
57
|
if (module && module.init) {
|
|
58
|
+
pipeline.currentModule = key;
|
|
51
59
|
module.init();
|
|
60
|
+
pipeline.currentModule = null;
|
|
52
61
|
}
|
|
53
62
|
}
|
|
54
63
|
}
|
|
55
64
|
|
|
56
65
|
function start() {
|
|
57
|
-
for (const module of
|
|
66
|
+
for (const { key, module } of activeModulesWithKeys()) {
|
|
58
67
|
if (module && module.start) {
|
|
68
|
+
pipeline.currentModule = key;
|
|
59
69
|
module.start();
|
|
70
|
+
pipeline.currentModule = null;
|
|
60
71
|
}
|
|
61
72
|
}
|
|
62
73
|
}
|
package/src/modules/language.js
CHANGED
package/src/modules/signature.js
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
|
-
const { is
|
|
1
|
+
const { is } = require('immutable');
|
|
2
2
|
|
|
3
3
|
const api = require('../app/api');
|
|
4
|
-
const { Aggregator } = require('../lib/aggregator');
|
|
4
|
+
const { Aggregator, lastSeen, statusCount } = require('../lib/aggregator');
|
|
5
5
|
const { Formatter } = require('../lib/formatter');
|
|
6
6
|
const pipeline = require('../lib/pipeline');
|
|
7
|
-
const {
|
|
7
|
+
const { touch, prune, countRecent } = require('../lib/recent-map');
|
|
8
|
+
const {
|
|
9
|
+
aggregateCount,
|
|
10
|
+
aggregateSum,
|
|
11
|
+
formatDuration,
|
|
12
|
+
md5,
|
|
13
|
+
} = require('../lib/util');
|
|
8
14
|
|
|
9
15
|
const { agentFormat } = require('./agent');
|
|
10
16
|
|
|
@@ -35,6 +41,9 @@ function normalisedIdentityHeader(headers) {
|
|
|
35
41
|
return obj;
|
|
36
42
|
}
|
|
37
43
|
|
|
44
|
+
const addressCount15m = (entry) => countRecent(entry.get('addresses'), 15 * 60);
|
|
45
|
+
const addressCount24h = (entry) => countRecent(entry.get('addresses'));
|
|
46
|
+
|
|
38
47
|
function computeSignature(headers) {
|
|
39
48
|
const string = Object.keys(headers)
|
|
40
49
|
.map((key) => [key, headers[key]].join(':'))
|
|
@@ -60,8 +69,10 @@ function init() {
|
|
|
60
69
|
pipeline.getNode('main').map(augment).registerNode('main');
|
|
61
70
|
}
|
|
62
71
|
|
|
72
|
+
let _aggregator;
|
|
73
|
+
|
|
63
74
|
function start() {
|
|
64
|
-
const aggregator = new Aggregator();
|
|
75
|
+
const aggregator = (_aggregator = new Aggregator());
|
|
65
76
|
|
|
66
77
|
aggregator.setIdentifier((log) => log.getIn(['signature', 'id']));
|
|
67
78
|
|
|
@@ -70,15 +81,24 @@ function start() {
|
|
|
70
81
|
signatureFormatter.setFormats([
|
|
71
82
|
['signature', (entry) => entry.getIn(['signature', 'id'])],
|
|
72
83
|
['identity', (entry) => entry.get('identity')],
|
|
73
|
-
['
|
|
84
|
+
['addressCount15m', addressCount15m],
|
|
85
|
+
['addressCount24h', addressCount24h],
|
|
74
86
|
[
|
|
75
87
|
'addresses',
|
|
76
88
|
(entry) =>
|
|
77
|
-
entry
|
|
78
|
-
.get('addresses')
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
89
|
+
entry.has('addresses')
|
|
90
|
+
? entry.get('addresses').keySeq().slice(0, 10).join('<br>')
|
|
91
|
+
: '',
|
|
92
|
+
],
|
|
93
|
+
[
|
|
94
|
+
'lastAddress',
|
|
95
|
+
(entry) => {
|
|
96
|
+
const addr = entry.get('lastAddress');
|
|
97
|
+
if (!addr) {
|
|
98
|
+
return '';
|
|
99
|
+
}
|
|
100
|
+
return addr.get('hostname') || addr.get('value') || '';
|
|
101
|
+
},
|
|
82
102
|
],
|
|
83
103
|
|
|
84
104
|
[
|
|
@@ -91,12 +111,24 @@ function start() {
|
|
|
91
111
|
},
|
|
92
112
|
],
|
|
93
113
|
|
|
94
|
-
['
|
|
95
|
-
['
|
|
114
|
+
['lastSeen', lastSeen],
|
|
115
|
+
['count15m', (entry) => aggregateCount(entry, 'per_minute')],
|
|
116
|
+
['count24h', (entry) => aggregateCount(entry, 'per_hour')],
|
|
117
|
+
|
|
118
|
+
['2xx15m', statusCount('2xx_per_minute')],
|
|
119
|
+
['2xx24h', statusCount('2xx_per_hour')],
|
|
120
|
+
['4xx15m', statusCount('4xx_per_minute')],
|
|
121
|
+
['4xx24h', statusCount('4xx_per_hour')],
|
|
122
|
+
|
|
123
|
+
[
|
|
124
|
+
'execTime15m',
|
|
125
|
+
(entry) => formatDuration(aggregateSum(entry, 'per_minute')),
|
|
126
|
+
],
|
|
127
|
+
['execTime24h', (entry) => formatDuration(aggregateSum(entry, 'per_hour'))],
|
|
96
128
|
]);
|
|
97
129
|
|
|
98
130
|
signatureFormatter.insertFormat('agent', agentFormat, {
|
|
99
|
-
before: '
|
|
131
|
+
before: 'count15m',
|
|
100
132
|
color: 'grey',
|
|
101
133
|
});
|
|
102
134
|
|
|
@@ -110,10 +142,12 @@ function start() {
|
|
|
110
142
|
}
|
|
111
143
|
|
|
112
144
|
const address = log.get('address');
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
145
|
+
entry = entry.set('lastAddress', address);
|
|
146
|
+
// Distinct IPs with last-seen time, pruned to 24h; backs both the 15m
|
|
147
|
+
// and 24h counts
|
|
148
|
+
const value = address && address.get('value');
|
|
149
|
+
if (value) {
|
|
150
|
+
entry = entry.update('addresses', (map) => touch(map, value));
|
|
117
151
|
}
|
|
118
152
|
|
|
119
153
|
return entry;
|
|
@@ -121,7 +155,16 @@ function start() {
|
|
|
121
155
|
|
|
122
156
|
aggregator.setEnricher(enricher);
|
|
123
157
|
|
|
124
|
-
|
|
158
|
+
aggregator.setEntryGc((entry) =>
|
|
159
|
+
entry.has('addresses') ? entry.update('addresses', prune) : entry
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
aggregator.sorters.addressCount15m = addressCount15m;
|
|
163
|
+
aggregator.sorters.addressCount24h = addressCount24h;
|
|
164
|
+
|
|
165
|
+
pipeline
|
|
166
|
+
.getNode('main')
|
|
167
|
+
.map((log) => aggregator.processLog(log), 'aggregator');
|
|
125
168
|
|
|
126
169
|
api.registerAggregator('signatures', aggregator);
|
|
127
170
|
}
|
|
@@ -129,4 +172,7 @@ function start() {
|
|
|
129
172
|
module.exports = {
|
|
130
173
|
init,
|
|
131
174
|
start,
|
|
175
|
+
get aggregator() {
|
|
176
|
+
return _aggregator;
|
|
177
|
+
},
|
|
132
178
|
};
|
package/src/modules/sparkline.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
const aggregator = require('../lib/aggregator');
|
|
2
2
|
|
|
3
|
-
const sparkline = (entry, key) => {
|
|
3
|
+
const sparkline = (entry, key, output) => {
|
|
4
|
+
if (output === 'text') {
|
|
5
|
+
return;
|
|
6
|
+
}
|
|
7
|
+
|
|
4
8
|
const id = entry.get('id');
|
|
5
9
|
|
|
6
10
|
const points = entry
|
|
@@ -22,8 +26,8 @@ sparkline ('${id}', ${JSON.stringify(points)}, '#797979', 14, 5);
|
|
|
22
26
|
};
|
|
23
27
|
|
|
24
28
|
function init() {
|
|
25
|
-
aggregator.defaultFormatter.insertFormat('activity', (entry) =>
|
|
26
|
-
sparkline(entry, 'per_minute')
|
|
29
|
+
aggregator.defaultFormatter.insertFormat('activity', (entry, output) =>
|
|
30
|
+
sparkline(entry, 'per_minute', output)
|
|
27
31
|
);
|
|
28
32
|
}
|
|
29
33
|
|
package/src/modules/status.js
CHANGED
|
@@ -3,30 +3,35 @@ const monitoring = require('../lib/monitoring');
|
|
|
3
3
|
const { formatTable } = require('../lib/util');
|
|
4
4
|
const stylesheet = require('../stylesheet');
|
|
5
5
|
|
|
6
|
-
const
|
|
6
|
+
const aggregateCount = (entry, path) =>
|
|
7
7
|
entry.hasIn(path) ? entry.getIn(path).reduce((p, c) => p + c, 0) : null;
|
|
8
8
|
|
|
9
9
|
function mapper(entry, format) {
|
|
10
10
|
return {
|
|
11
11
|
name: entry.get('name'),
|
|
12
12
|
type: entry.get('type'),
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
count15m:
|
|
14
|
+
aggregateCount(entry, ['speeds', 'processed', 'per_minute']) ||
|
|
15
|
+
aggregateCount(entry, ['speeds', 'accepted', 'per_minute']) ||
|
|
16
16
|
(format !== 'json' ? '' : null),
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
count24h:
|
|
18
|
+
aggregateCount(entry, ['speeds', 'processed', 'per_hour']) ||
|
|
19
|
+
aggregateCount(entry, ['speeds', 'accepted', 'per_hour']) ||
|
|
20
20
|
(format !== 'json' ? '' : null),
|
|
21
21
|
status: entry.get('status'),
|
|
22
22
|
};
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
function start() {
|
|
26
|
-
api.get('/status
|
|
26
|
+
api.get('/status{.:format}', (req, res) => {
|
|
27
27
|
const raw = req.query.raw ? true : false;
|
|
28
28
|
const format = req.params.format || (raw ? 'json' : null);
|
|
29
29
|
|
|
30
|
+
if (format && !['json', 'txt'].includes(format)) {
|
|
31
|
+
res.sendStatus(404);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
30
35
|
let rawData = monitoring.getAllComputed();
|
|
31
36
|
|
|
32
37
|
if (req.query.type) {
|