@hyperwatch/hyperwatch 4.1.0 → 4.3.1

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.
@@ -1,8 +1,13 @@
1
- const { Map, Set, is } = require('immutable');
1
+ const { Map, Set, fromJS, is } = require('immutable');
2
2
 
3
3
  const { Formatter, address, identity } = require('../lib/formatter');
4
4
  const { Speed } = require('../lib/speed');
5
- const { aggregateSpeed, md5 } = require('../lib/util');
5
+ const {
6
+ aggregateCount,
7
+ aggregateSum,
8
+ formatDuration,
9
+ md5,
10
+ } = require('../lib/util');
6
11
 
7
12
  const defaultFormatter = new Formatter();
8
13
  defaultFormatter.setFormats([
@@ -10,8 +15,11 @@ defaultFormatter.setFormats([
10
15
 
11
16
  ['address', address],
12
17
 
13
- ['15m', (entry) => aggregateSpeed(entry, 'per_minute')],
14
- ['24h', (entry) => aggregateSpeed(entry, 'per_hour')],
18
+ ['count15m', (entry) => aggregateCount(entry, 'per_minute')],
19
+ ['count24h', (entry) => aggregateCount(entry, 'per_hour')],
20
+
21
+ ['execTime15m', (entry) => formatDuration(aggregateSum(entry, 'per_minute'))],
22
+ ['execTime24h', (entry) => formatDuration(aggregateSum(entry, 'per_hour'))],
15
23
  ]);
16
24
 
17
25
  const defaultEnricher = (entry, log) => {
@@ -42,9 +50,11 @@ const defaultIdentifier = (log) => {
42
50
  };
43
51
 
44
52
  const defaultSorters = {
45
- '15m': (entry) => aggregateSpeed(entry, 'per_minute'),
46
- '24h': (entry) => aggregateSpeed(entry, 'per_hour'),
53
+ count15m: (entry) => aggregateCount(entry, 'per_minute'),
54
+ count24h: (entry) => aggregateCount(entry, 'per_hour'),
47
55
  latest: (entry) => entry.getIn(['speed', 'per_minute']).latest,
56
+ execTime15m: (entry) => aggregateSum(entry, 'per_minute'),
57
+ execTime24h: (entry) => aggregateSum(entry, 'per_hour'),
48
58
  };
49
59
 
50
60
  class Aggregator {
@@ -56,7 +66,7 @@ class Aggregator {
56
66
  this.sorters = defaultSorters;
57
67
  this.gcSize = 1000;
58
68
 
59
- setInterval(() => this.gc(), 60 * 1000);
69
+ setInterval(() => this.gc(), 60 * 1000).unref();
60
70
  }
61
71
 
62
72
  setFormatter(fn) {
@@ -80,17 +90,29 @@ class Aggregator {
80
90
  processLog(log) {
81
91
  const identifier = this.identifier(log);
82
92
  const id = md5(identifier);
93
+ const rawExecTime = Number(log.get('executionTime'));
94
+ const executionTime = Number.isFinite(rawExecTime) ? rawExecTime : 0;
83
95
 
84
96
  if (!this.entries.has(id)) {
85
97
  this.entries = this.entries
86
98
  .setIn([id, 'id'], id)
87
99
  .setIn([id, 'identifier'], identifier)
88
- .setIn([id, 'speed', 'per_minute'], new Speed(60, 15).hit())
89
- .setIn([id, 'speed', 'per_hour'], new Speed(3600, 24).hit());
100
+ .setIn(
101
+ [id, 'speed', 'per_minute'],
102
+ new Speed(60, 15).hit(undefined, executionTime)
103
+ )
104
+ .setIn(
105
+ [id, 'speed', 'per_hour'],
106
+ new Speed(3600, 24).hit(undefined, executionTime)
107
+ );
90
108
  } else {
91
109
  this.entries = this.entries
92
- .updateIn([id, 'speed', 'per_minute'], (speed) => speed.hit())
93
- .updateIn([id, 'speed', 'per_hour'], (speed) => speed.hit());
110
+ .updateIn([id, 'speed', 'per_minute'], (speed) =>
111
+ speed.hit(undefined, executionTime)
112
+ )
113
+ .updateIn([id, 'speed', 'per_hour'], (speed) =>
114
+ speed.hit(undefined, executionTime)
115
+ );
94
116
  }
95
117
 
96
118
  this.entries = this.entries.updateIn([id], (entry) =>
@@ -106,7 +128,7 @@ class Aggregator {
106
128
 
107
129
  getData({ raw, sort, format, limit }) {
108
130
  if (!sort || !this.sorters[sort]) {
109
- sort = '15m';
131
+ sort = 'count15m';
110
132
  }
111
133
 
112
134
  const rawData = this.entries
@@ -142,6 +164,40 @@ class Aggregator {
142
164
 
143
165
  this.entries = this.entries.filter((value, key) => keepList.has(key));
144
166
  }
167
+
168
+ dump() {
169
+ return this.entries
170
+ .map((entry) => {
171
+ const plain = entry.toJS();
172
+ plain.speed = {
173
+ per_minute: entry.getIn(['speed', 'per_minute']).toJSON(),
174
+ per_hour: entry.getIn(['speed', 'per_hour']).toJSON(),
175
+ };
176
+ return plain;
177
+ })
178
+ .valueSeq()
179
+ .toArray();
180
+ }
181
+
182
+ load(data) {
183
+ for (const item of data) {
184
+ const { speed, ...rest } = item;
185
+ // fromJS deep-converts everything to Immutable structures.
186
+ // Signature headers must stay as a plain object (used with Object.entries),
187
+ // and addresses must be a Set, not a List — fix both after conversion.
188
+ let entry = fromJS(rest);
189
+ if (entry.hasIn(['signature', 'headers'])) {
190
+ entry = entry.setIn(['signature', 'headers'], rest.signature.headers);
191
+ }
192
+ if (entry.has('addresses')) {
193
+ entry = entry.update('addresses', (list) => Set(list));
194
+ }
195
+ entry = entry
196
+ .setIn(['speed', 'per_minute'], Speed.fromJSON(speed.per_minute))
197
+ .setIn(['speed', 'per_hour'], Speed.fromJSON(speed.per_hour));
198
+ this.entries = this.entries.set(rest.id, entry);
199
+ }
200
+ }
145
201
  }
146
202
 
147
203
  module.exports = { Aggregator, defaultFormatter, defaultEnricher };
package/src/lib/index.js CHANGED
@@ -2,6 +2,7 @@ const aggregator = require('./aggregator');
2
2
  const cache = require('./cache');
3
3
  const formatter = require('./formatter');
4
4
  const logger = require('./logger');
5
+ const persistence = require('./persistence');
5
6
  const pipeline = require('./pipeline');
6
7
  const util = require('./util');
7
8
 
@@ -10,6 +11,7 @@ module.exports = {
10
11
  cache,
11
12
  formatter,
12
13
  logger,
14
+ persistence,
13
15
  pipeline,
14
16
  util,
15
17
  };
@@ -0,0 +1,72 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ const debug = require('debug');
5
+
6
+ const debugPersistence = debug('hyperwatch:persistence');
7
+
8
+ const aggregators = Object.create(null);
9
+
10
+ const SAFE_NAME = /^[A-Za-z0-9._-]+$/;
11
+
12
+ function register(name, aggregator) {
13
+ if (!SAFE_NAME.test(name)) {
14
+ throw new Error(`Invalid aggregator name for persistence: "${name}"`);
15
+ }
16
+ aggregators[name] = aggregator;
17
+ }
18
+
19
+ function dump(dir) {
20
+ if (!fs.existsSync(dir)) {
21
+ fs.mkdirSync(dir, { recursive: true });
22
+ }
23
+ for (const [name, aggregator] of Object.entries(aggregators)) {
24
+ const data = aggregator.dump();
25
+ const target = path.join(dir, `${name}.json`);
26
+ const tmp = path.join(dir, `${name}.json.tmp`);
27
+ fs.writeFileSync(tmp, JSON.stringify(data));
28
+ fs.renameSync(tmp, target);
29
+ }
30
+ debugPersistence(
31
+ `Dumped ${Object.keys(aggregators).length} aggregator(s) to ${dir}`
32
+ );
33
+ }
34
+
35
+ function load(dir) {
36
+ if (!fs.existsSync(dir)) {
37
+ return;
38
+ }
39
+ let files;
40
+ try {
41
+ files = fs.readdirSync(dir).filter((f) => f.endsWith('.json'));
42
+ } catch (err) {
43
+ debugPersistence(
44
+ `Cannot read persistence directory ${dir}: ${err.message}`
45
+ );
46
+ return;
47
+ }
48
+ let loaded = 0;
49
+ for (const file of files) {
50
+ const name = path.basename(file, '.json');
51
+ if (aggregators[name]) {
52
+ try {
53
+ const data = JSON.parse(fs.readFileSync(path.join(dir, file), 'utf8'));
54
+ if (!Array.isArray(data)) {
55
+ debugPersistence(
56
+ `Skipping ${file}: expected array, got ${typeof data}`
57
+ );
58
+ continue;
59
+ }
60
+ aggregators[name].load(data);
61
+ loaded += data.length;
62
+ } catch (err) {
63
+ debugPersistence(`Skipping ${file}: ${err.message}`);
64
+ }
65
+ }
66
+ }
67
+ if (loaded) {
68
+ debugPersistence(`Loaded ${loaded} entries from ${dir}`);
69
+ }
70
+ }
71
+
72
+ module.exports = { register, dump, load };
package/src/lib/speed.js CHANGED
@@ -10,6 +10,7 @@ class Speed {
10
10
  this.windowSize = windowSize;
11
11
  this.size = size;
12
12
  this.counters = Map();
13
+ this.sums = Map();
13
14
  this.started = null;
14
15
  this.latest = null;
15
16
  }
@@ -18,23 +19,26 @@ class Speed {
18
19
  gc() {
19
20
  const cutoff = now() - this.size * this.windowSize;
20
21
  this.counters = this.counters.filter((c, t) => parseInt(t) > cutoff);
22
+ this.sums = this.sums.filter((c, t) => parseInt(t) > cutoff);
21
23
  }
22
24
 
23
- hit(time = now()) {
25
+ hit(time = now(), value) {
24
26
  this.started = !this.started ? time : Math.min(this.started, time);
25
27
  this.latest = !this.latest ? time : Math.max(this.latest, time);
26
28
  const idx = time - (time % this.windowSize);
27
29
  this.counters = this.counters.update(`${idx}`, 0, (n) => n + 1);
30
+ if (value !== undefined) {
31
+ this.sums = this.sums.update(`${idx}`, 0, (n) => n + value);
32
+ }
28
33
  this.gc();
29
34
  return this;
30
35
  }
31
36
 
32
- compute() {
37
+ compute(time = now()) {
33
38
  this.gc();
34
39
  if (!this.started) {
35
40
  return List();
36
41
  }
37
- const time = now();
38
42
  return Range(0, this.size)
39
43
  .map((n) => {
40
44
  let t = time - n * this.windowSize;
@@ -45,6 +49,42 @@ class Speed {
45
49
  })
46
50
  .filter((v) => v !== undefined);
47
51
  }
52
+
53
+ computeSum(time = now()) {
54
+ this.gc();
55
+ if (!this.started) {
56
+ return List();
57
+ }
58
+ return Range(0, this.size)
59
+ .map((n) => {
60
+ let t = time - n * this.windowSize;
61
+ t = t - (t % this.windowSize);
62
+ if (t >= this.started - (this.started % this.windowSize)) {
63
+ return this.sums.get(`${t}`, 0);
64
+ }
65
+ })
66
+ .filter((v) => v !== undefined);
67
+ }
68
+
69
+ toJSON() {
70
+ return {
71
+ windowSize: this.windowSize,
72
+ size: this.size,
73
+ counters: this.counters.toObject(),
74
+ sums: this.sums.toObject(),
75
+ started: this.started,
76
+ latest: this.latest,
77
+ };
78
+ }
79
+
80
+ static fromJSON(data) {
81
+ const speed = new Speed(data.windowSize, data.size);
82
+ speed.counters = Map(data.counters);
83
+ speed.sums = Map(data.sums);
84
+ speed.started = data.started;
85
+ speed.latest = data.latest;
86
+ return speed;
87
+ }
48
88
  }
49
89
 
50
90
  module.exports = {
package/src/lib/util.js CHANGED
@@ -34,12 +34,28 @@ exports.createLog = (req, res) => {
34
34
  });
35
35
  };
36
36
 
37
- exports.aggregateSpeed = (entry, key) =>
37
+ exports.aggregateCount = (entry, key) =>
38
38
  entry
39
39
  .getIn(['speed', key])
40
40
  .compute()
41
41
  .reduce((p, c) => p + c, 0);
42
42
 
43
+ exports.aggregateSum = (entry, key) =>
44
+ entry
45
+ .getIn(['speed', key])
46
+ .computeSum()
47
+ .reduce((p, c) => p + c, 0);
48
+
49
+ exports.formatDuration = (ms) => {
50
+ const totalSeconds = ms / 1000;
51
+ const minutes = Math.floor(totalSeconds / 60);
52
+ const seconds = Math.round(totalSeconds % 60);
53
+ if (minutes > 0) {
54
+ return seconds > 0 ? `${minutes}m${seconds}s` : `${minutes}m`;
55
+ }
56
+ return `${totalSeconds.toFixed(1)}s`;
57
+ };
58
+
43
59
  exports.formatTable = (data) => {
44
60
  if (!data || data.length === 0) {
45
61
  return '';
@@ -70,7 +70,7 @@ function init() {
70
70
  pipeline.getNode('main').map(augment).registerNode('main');
71
71
 
72
72
  aggregator.defaultFormatter.insertFormat('agent', agentFormat, {
73
- before: '15m',
73
+ before: 'count15m',
74
74
  color: 'grey',
75
75
  });
76
76
  aggregator.defaultFormatter.insertFormat('os', osFormat, {
@@ -0,0 +1,86 @@
1
+ const { api } = require('../app');
2
+ const constants = require('../constants');
3
+ const pipeline = require('../lib/pipeline');
4
+
5
+ class CircularBuffer {
6
+ constructor(capacity = 1000) {
7
+ this.capacity = capacity;
8
+ this.buffer = new Array(capacity);
9
+ this.pointer = 0;
10
+ this.size = 0;
11
+ }
12
+
13
+ push(item) {
14
+ this.buffer[this.pointer] = item;
15
+ this.pointer = (this.pointer + 1) % this.capacity;
16
+ if (this.size < this.capacity) {
17
+ this.size++;
18
+ }
19
+ }
20
+
21
+ toArray() {
22
+ if (this.size < this.capacity) {
23
+ return this.buffer.slice(0, this.size).reverse();
24
+ }
25
+ return [
26
+ ...this.buffer.slice(this.pointer),
27
+ ...this.buffer.slice(0, this.pointer),
28
+ ].reverse();
29
+ }
30
+ }
31
+
32
+ function start() {
33
+ const capacity =
34
+ (constants.modules.history && constants.modules.history.capacity) || 1000;
35
+ const buffers = {};
36
+
37
+ function registerNodeHistory(name, node) {
38
+ const buffer = new CircularBuffer(capacity);
39
+ buffers[name] = buffer;
40
+
41
+ node.map((log) => {
42
+ buffer.push(log);
43
+ return log;
44
+ });
45
+
46
+ api.get(`/history/${name}.json`, (req, res) => {
47
+ const { identity, signature, address } = req.query;
48
+ const limit = parseInt(req.query.limit, 10) || 100;
49
+
50
+ let logs = buffer.toArray();
51
+
52
+ if (identity) {
53
+ logs = logs.filter((log) => log.get('identity') === identity);
54
+ }
55
+ if (signature) {
56
+ logs = logs.filter(
57
+ (log) => log.getIn(['signature', 'id']) === signature
58
+ );
59
+ }
60
+ if (address) {
61
+ logs = logs.filter(
62
+ (log) => log.getIn(['address', 'value']) === address
63
+ );
64
+ }
65
+
66
+ logs = logs.slice(0, limit);
67
+
68
+ res.json(logs);
69
+ });
70
+ }
71
+
72
+ for (const [name, node] of Object.entries(pipeline.nodes)) {
73
+ registerNodeHistory(name, node);
74
+ }
75
+
76
+ // Auto-register future nodes
77
+ const originalRegisterNode = pipeline.registerNode.bind(pipeline);
78
+ pipeline.registerNode = function (name, node) {
79
+ originalRegisterNode(name, node);
80
+ if (!buffers[name]) {
81
+ registerNodeHistory(name, node);
82
+ }
83
+ };
84
+ }
85
+
86
+ module.exports = { start };
@@ -1,6 +1,14 @@
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
+ const amazonSearchBotIps = require('../data/amazon-searchbot-ips.json');
7
+ const amazonUserIps = require('../data/amazon-user-ips.json');
8
+ const amazonBotIps = require('../data/amazonbot-ips.json');
9
+ const chatgptUserIps = require('../data/chatgpt-user-ips.json');
10
+ const gptbotIps = require('../data/gptbot-ips.json');
11
+ const openaiSearchbotIps = require('../data/openai-searchbot-ips.json');
4
12
  const { Aggregator } = require('../lib/aggregator');
5
13
  const pipeline = require('../lib/pipeline');
6
14
 
@@ -69,6 +77,7 @@ function augment(log) {
69
77
  return hostname && hostname.endsWith('.mojeek.com')
70
78
  ? log.set('identity', 'Mojeek')
71
79
  : log;
80
+ case 'AwarioBot':
72
81
  case 'BLEXBot':
73
82
  return hostname && hostname.endsWith('.webmeup.com')
74
83
  ? log.set('identity', 'WebMeUp')
@@ -89,13 +98,11 @@ function augment(log) {
89
98
  return hostname && hostname.endsWith('.naver.com')
90
99
  ? log.set('identity', 'Naver')
91
100
  : log;
92
- case 'FacebookBot':
93
- return hostname && hostname.endsWith('.fbsv.net')
94
- ? log.set('identity', 'Facebook')
95
- : log;
101
+
96
102
  case 'Stripe':
103
+ case 'Stripebot':
97
104
  return hostname && hostname.endsWith('.stripe.com')
98
- ? log.set('identity', family)
105
+ ? log.set('identity', 'Stripe')
99
106
  : log;
100
107
  case 'UptimeRobot':
101
108
  return hostname && hostname.endsWith('.uptimerobot.com')
@@ -127,9 +134,17 @@ function augment(log) {
127
134
  ? log.set('identity', 'Sirportly')
128
135
  : log;
129
136
  case 'Bytespider':
130
- return hostname && hostname.endsWith('.crawl.bytedance.com')
137
+ return hostname &&
138
+ (hostname.endsWith('.crawl.bytedance.com') ||
139
+ hostname.endsWith('.ap-southeast-1.compute.amazonaws.com'))
131
140
  ? log.set('identity', family)
132
141
  : log;
142
+ case 'TikTokSpider':
143
+ return hostname &&
144
+ (hostname.endsWith('.crawl.bytedance.com') ||
145
+ hostname.endsWith('.ap-southeast-1.compute.amazonaws.com'))
146
+ ? log.set('identity', 'TikTok')
147
+ : log;
133
148
  case 'Mail.RU Bot':
134
149
  case 'Mail.RU Bot Img':
135
150
  case 'Mail.RU Bot Fast':
@@ -182,6 +197,14 @@ function augment(log) {
182
197
  return hostname && hostname.endsWith('.neevabot.com')
183
198
  ? log.set('identity', 'Neevabot')
184
199
  : log;
200
+ case 'RootCrawl-Crawler':
201
+ return hostname && hostname.endsWith('.rootcrawl.org')
202
+ ? log.set('identity', 'RootCrawl')
203
+ : log;
204
+ case 'bl.uk ldfc bot':
205
+ return hostname && hostname.endsWith('.bl.uk')
206
+ ? log.set('identity', 'British Library')
207
+ : log;
185
208
  case 'DataForSeoBot':
186
209
  return hostname && hostname.endsWith('.dataforseo.com')
187
210
  ? log.set('identity', 'DataForSeo')
@@ -196,9 +219,24 @@ function augment(log) {
196
219
  ? log.set('identity', 'InfoTiger')
197
220
  : log;
198
221
  case 'Amazonbot':
199
- return hostname && hostname.endsWith('.crawl.amazonbot.amazon')
222
+ return (hostname && hostname.endsWith('.crawl.amazonbot.amazon')) ||
223
+ amazonBotIps.some((cidr) => new IPCIDR(cidr).contains(address))
200
224
  ? log.set('identity', 'Amazonbot')
201
225
  : log;
226
+ case 'Amzn-SearchBot':
227
+ return (hostname && hostname.endsWith('.crawl.amazonbot.amazon')) ||
228
+ amazonSearchBotIps.some((cidr) => new IPCIDR(cidr).contains(address))
229
+ ? log.set('identity', 'Amazon SearchBot')
230
+ : log;
231
+ case 'Amzn-User':
232
+ return (hostname && hostname.endsWith('.crawl.amazonbot.amazon')) ||
233
+ amazonUserIps.some((cidr) => new IPCIDR(cidr).contains(address))
234
+ ? log.set('identity', 'Amazon User')
235
+ : log;
236
+ case 'SERankingBacklinksBot':
237
+ return hostname && hostname.endsWith('.blex.seranking.com')
238
+ ? log.set('identity', 'SE Ranking')
239
+ : log;
202
240
 
203
241
  // Per hostname + CIDR
204
242
  case 'Twitterbot':
@@ -211,6 +249,11 @@ function augment(log) {
211
249
  (address && new IPCIDR('2a02:598::/32').contains(address))
212
250
  ? log.set('identity', 'Seznam')
213
251
  : log;
252
+ case 'FacebookBot':
253
+ return (hostname && hostname.endsWith('.fbsv.net')) ||
254
+ (address && new IPCIDR('2a03:2880::/29').contains(address))
255
+ ? log.set('identity', 'Facebook')
256
+ : log;
214
257
 
215
258
  // Per CIDR
216
259
  case 'github-camo':
@@ -221,6 +264,10 @@ function augment(log) {
221
264
  return address && new IPCIDR('216.244.64.0/19').contains(address)
222
265
  ? log.set('identity', 'Moz')
223
266
  : log;
267
+ case 'AliyunSecBot':
268
+ return address && new IPCIDR('8.217.0.0/16').contains(address)
269
+ ? log.set('identity', family)
270
+ : log;
224
271
  case '360Spider':
225
272
  return address && new IPCIDR('42.236.10.0/24').contains(address)
226
273
  ? log.set('identity', family)
@@ -229,6 +276,28 @@ function augment(log) {
229
276
  return address && new IPCIDR('203.133.160.0/19').contains(address)
230
277
  ? log.set('identity', family)
231
278
  : log;
279
+ case 'OAI-SearchBot':
280
+ return openaiSearchbotIps.some((cidr) =>
281
+ new IPCIDR(cidr).contains(address)
282
+ )
283
+ ? log.set('identity', 'OpenAI SearchBot')
284
+ : log;
285
+ case 'GPTBot':
286
+ return gptbotIps.some((cidr) => new IPCIDR(cidr).contains(address))
287
+ ? log.set('identity', 'OpenAI GPTBot')
288
+ : log;
289
+ case 'ChatGPT-User':
290
+ // https://openai.com/chatgpt-user.json
291
+ return chatgptUserIps.some((cidr) => new IPCIDR(cidr).contains(address))
292
+ ? log.set('identity', 'ChatGPT')
293
+ : log;
294
+ case 'meta-externalagent':
295
+ case 'meta-webindexer':
296
+ return address &&
297
+ (new IPCIDR('2a03:2880::/29').contains(address) ||
298
+ new IPCIDR('2a06:98c0:3600::/48').contains(address))
299
+ ? log.set('identity', 'Meta')
300
+ : log;
232
301
 
233
302
  // EC2
234
303
  case 'Raven':
@@ -270,12 +339,32 @@ function augment(log) {
270
339
  return hostname && hostname.endsWith('.us-east-2.compute.amazonaws.com')
271
340
  ? log.set('identity', 'Claude')
272
341
  : log;
342
+ case 'PerplexityBot':
343
+ return hostname && hostname.endsWith('.compute-1.amazonaws.com')
344
+ ? log.set('identity', 'Perplexity')
345
+ : log;
273
346
 
274
347
  // GCE
348
+ case 'Aranet-SearchBot':
349
+ return hostname && hostname.endsWith('.googleusercontent.com')
350
+ ? log.set('identity', 'Aranet')
351
+ : log;
352
+ case 'Discordbot':
353
+ return hostname && hostname.endsWith('.googleusercontent.com')
354
+ ? log.set('identity', 'Discord')
355
+ : log;
275
356
  case 'VelenPublicWebCrawler':
276
357
  return hostname && hostname.endsWith('.googleusercontent.com')
277
358
  ? log.set('identity', 'Velen')
278
359
  : log;
360
+ case 'SleepBot':
361
+ return hostname && hostname.endsWith('.googleusercontent.com')
362
+ ? log.set('identity', 'SleepBot')
363
+ : log;
364
+ case 'DolfeEngineCrawler':
365
+ return hostname && hostname.endsWith('.googleusercontent.com')
366
+ ? log.set('identity', 'Dolfe')
367
+ : log;
279
368
 
280
369
  // Hetzner
281
370
  case 'Ubermetrics':
@@ -290,6 +379,18 @@ function augment(log) {
290
379
  return hostname && hostname.endsWith('.clients.your-server.de')
291
380
  ? log.set('identity', 'MegaIndex.ru')
292
381
  : log;
382
+ case 'ev-crawler':
383
+ return hostname && hostname.endsWith('.headline.com')
384
+ ? log.set('identity', 'Headline')
385
+ : log;
386
+ case 'SentryUptimeBot':
387
+ return hostname && hostname.endsWith('.googleusercontent.com')
388
+ ? log.set('identity', 'Sentry')
389
+ : log;
390
+ case 'FlipboardProxy':
391
+ return hostname && hostname.endsWith('.flipboard.com')
392
+ ? log.set('identity', 'Flipboard')
393
+ : log;
293
394
  }
294
395
 
295
396
  // Hostname only
@@ -16,6 +16,8 @@ function get(module) {
16
16
  return require('./dnsbl');
17
17
  case 'geoip':
18
18
  return require('./geoip');
19
+ case 'history':
20
+ return require('./history');
19
21
  case 'hostname':
20
22
  return require('./hostname');
21
23
  case 'identity':
@@ -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
  }
@@ -4,7 +4,12 @@ const api = require('../app/api');
4
4
  const { Aggregator } = 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 {
8
+ aggregateCount,
9
+ aggregateSum,
10
+ formatDuration,
11
+ md5,
12
+ } = require('../lib/util');
8
13
 
9
14
  const { agentFormat } = require('./agent');
10
15
 
@@ -91,12 +96,18 @@ function start() {
91
96
  },
92
97
  ],
93
98
 
94
- ['15m', (entry) => aggregateSpeed(entry, 'per_minute')],
95
- ['24h', (entry) => aggregateSpeed(entry, 'per_hour')],
99
+ ['count15m', (entry) => aggregateCount(entry, 'per_minute')],
100
+ ['count24h', (entry) => aggregateCount(entry, 'per_hour')],
101
+
102
+ [
103
+ 'execTime15m',
104
+ (entry) => formatDuration(aggregateSum(entry, 'per_minute')),
105
+ ],
106
+ ['execTime24h', (entry) => formatDuration(aggregateSum(entry, 'per_hour'))],
96
107
  ]);
97
108
 
98
109
  signatureFormatter.insertFormat('agent', agentFormat, {
99
- before: '15m',
110
+ before: 'count15m',
100
111
  color: 'grey',
101
112
  });
102
113