@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.
Files changed (57) hide show
  1. package/README.md +9 -4
  2. package/config/apache_syslog_example.js +29 -0
  3. package/config/default.js +2 -2
  4. package/config/example.js +5 -5
  5. package/config/express_websocket_example.js +28 -0
  6. package/config/websocket_client_example.js +52 -0
  7. package/docs/configuration.md +67 -6
  8. package/docs/express-embedding.md +137 -0
  9. package/docs/input.md +50 -24
  10. package/docs/tutorials/apache_input.md +16 -14
  11. package/docs/tutorials/express_input.md +10 -8
  12. package/package.json +42 -35
  13. package/scripts/fetch-anthropic-ips.js +60 -0
  14. package/scripts/fetch-cloudflare-ips.js +27 -0
  15. package/scripts/fetch-cloudfront-ips.js +29 -0
  16. package/scripts/fetch-openai-ips.js +34 -0
  17. package/src/app/api.js +149 -8
  18. package/src/app/index.js +8 -4
  19. package/src/app/mount.js +115 -0
  20. package/src/app/websocket.js +39 -10
  21. package/src/app/ws-server.js +123 -0
  22. package/src/constants.js +17 -5
  23. package/src/data/amazon-searchbot-ips.json +818 -0
  24. package/src/data/amazon-user-ips.json +1025 -0
  25. package/src/data/amazonbot-ips.json +1294 -0
  26. package/src/data/chatgpt-user-ips.json +231 -0
  27. package/src/data/claude-bot-ips.json +28 -0
  28. package/src/data/cloudflare-ips.json +24 -0
  29. package/src/data/cloudfront-ips.json +245 -0
  30. package/src/data/gptbot-ips.json +20 -0
  31. package/src/data/openai-searchbot-ips.json +41 -0
  32. package/src/index.js +24 -3
  33. package/src/input/http.js +4 -0
  34. package/src/input/syslog.js +5 -1
  35. package/src/input/websocket.js +35 -19
  36. package/src/lib/aggregator.js +169 -20
  37. package/src/lib/formatter.js +10 -1
  38. package/src/lib/index.js +2 -0
  39. package/src/lib/log-buffer.js +45 -0
  40. package/src/lib/persistence.js +82 -0
  41. package/src/lib/pipeline.js +122 -11
  42. package/src/lib/recent-map.js +23 -0
  43. package/src/lib/speed.js +43 -3
  44. package/src/lib/util.js +17 -1
  45. package/src/modules/address.js +59 -2
  46. package/src/modules/agent.js +1 -1
  47. package/src/modules/dnsbl.js +11 -1
  48. package/src/modules/history.js +62 -0
  49. package/src/modules/identity.js +154 -26
  50. package/src/modules/index.js +16 -5
  51. package/src/modules/language.js +1 -1
  52. package/src/modules/signature.js +64 -18
  53. package/src/modules/sparkline.js +7 -3
  54. package/src/modules/status.js +13 -8
  55. package/src/plugins/proxy.js +10 -203
  56. package/src/script.js +0 -1
  57. package/scripts/cloudfront-ips.js +0 -56
@@ -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
- case 'FacebookBot':
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', family)
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 && hostname.endsWith('.crawl.bytedance.com')
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
- // https://openai.com/searchbot.json
234
- return address &&
235
- (new IPCIDR('20.42.10.176/28').contains(address) ||
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
- // https://openai.com/gptbot.json
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
- return address && new IPCIDR('2a03:2880:f800::/48').contains(address)
252
- ? log.set('identity', 'Meta External Agent')
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 'ClaudeBot':
292
- return hostname && hostname.endsWith('.us-east-2.compute.amazonaws.com')
293
- ? log.set('identity', 'Claude')
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.getNode('main').map((log) => aggregator.processLog(log));
498
+ pipeline
499
+ .getNode('main')
500
+ .map((log) => aggregator.processLog(log), 'aggregator');
373
501
 
374
502
  api.registerAggregator('identities', aggregator);
375
503
  }
@@ -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 activeModules() {
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 activeModules()) {
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 activeModules()) {
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
  }
@@ -33,7 +33,7 @@ function init() {
33
33
  pipeline.getNode('main').map(augment).registerNode('main');
34
34
 
35
35
  aggregator.defaultFormatter.insertFormat('language', language, {
36
- before: '15m',
36
+ before: 'count15m',
37
37
  color: 'grey',
38
38
  });
39
39
  }
@@ -1,10 +1,16 @@
1
- const { is, Set } = require('immutable');
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 { aggregateSpeed, md5 } = require('../lib/util');
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
- ['addressCount', (entry) => entry.get('addresses').size],
84
+ ['addressCount15m', addressCount15m],
85
+ ['addressCount24h', addressCount24h],
74
86
  [
75
87
  'addresses',
76
88
  (entry) =>
77
- entry
78
- .get('addresses')
79
- .map((address) => address.get('value'))
80
- .slice(0, 10)
81
- .join('<br>'),
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
- ['15m', (entry) => aggregateSpeed(entry, 'per_minute')],
95
- ['24h', (entry) => aggregateSpeed(entry, 'per_hour')],
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: '15m',
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
- if (!entry.has('addresses')) {
114
- entry = entry.set('addresses', new Set([address]));
115
- } else if (!entry.get('addresses').has(address)) {
116
- entry = entry.update('addresses', (set) => set.add(address));
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
- pipeline.getNode('main').map((log) => aggregator.processLog(log));
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
  };
@@ -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
 
@@ -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 aggregateSpeed = (entry, path) =>
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
- '15m':
14
- aggregateSpeed(entry, ['speeds', 'processed', 'per_minute']) ||
15
- aggregateSpeed(entry, ['speeds', 'accepted', 'per_minute']) ||
13
+ count15m:
14
+ aggregateCount(entry, ['speeds', 'processed', 'per_minute']) ||
15
+ aggregateCount(entry, ['speeds', 'accepted', 'per_minute']) ||
16
16
  (format !== 'json' ? '' : null),
17
- '24h':
18
- aggregateSpeed(entry, ['speeds', 'processed', 'per_hour']) ||
19
- aggregateSpeed(entry, ['speeds', 'accepted', 'per_hour']) ||
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(.:format(txt|json))?', (req, res) => {
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) {