@hyperwatch/hyperwatch 4.2.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)
@@ -230,26 +277,26 @@ function augment(log) {
230
277
  ? log.set('identity', family)
231
278
  : log;
232
279
  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))
280
+ return openaiSearchbotIps.some((cidr) =>
281
+ new IPCIDR(cidr).contains(address)
282
+ )
238
283
  ? log.set('identity', 'OpenAI SearchBot')
239
284
  : log;
240
285
  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))
286
+ return gptbotIps.some((cidr) => new IPCIDR(cidr).contains(address))
248
287
  ? log.set('identity', 'OpenAI GPTBot')
249
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;
250
294
  case 'meta-externalagent':
251
- return address && new IPCIDR('2a03:2880:f800::/48').contains(address)
252
- ? log.set('identity', 'Meta External Agent')
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')
253
300
  : log;
254
301
 
255
302
  // EC2
@@ -292,12 +339,32 @@ function augment(log) {
292
339
  return hostname && hostname.endsWith('.us-east-2.compute.amazonaws.com')
293
340
  ? log.set('identity', 'Claude')
294
341
  : log;
342
+ case 'PerplexityBot':
343
+ return hostname && hostname.endsWith('.compute-1.amazonaws.com')
344
+ ? log.set('identity', 'Perplexity')
345
+ : log;
295
346
 
296
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;
297
356
  case 'VelenPublicWebCrawler':
298
357
  return hostname && hostname.endsWith('.googleusercontent.com')
299
358
  ? log.set('identity', 'Velen')
300
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;
301
368
 
302
369
  // Hetzner
303
370
  case 'Ubermetrics':
@@ -312,6 +379,18 @@ function augment(log) {
312
379
  return hostname && hostname.endsWith('.clients.your-server.de')
313
380
  ? log.set('identity', 'MegaIndex.ru')
314
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;
315
394
  }
316
395
 
317
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
  }