@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.
- package/README.md +1 -1
- package/config/apache_syslog_example.js +29 -0
- package/config/default.js +1 -1
- package/config/example.js +1 -1
- package/config/express_websocket_example.js +28 -0
- package/package.json +38 -33
- 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 +21 -6
- package/src/app/websocket.js +28 -2
- package/src/constants.js +9 -0
- package/src/data/amazon-searchbot-ips.json +514 -0
- package/src/data/amazon-user-ips.json +1025 -0
- package/src/data/amazonbot-ips.json +520 -0
- package/src/data/chatgpt-user-ips.json +228 -0
- package/src/data/cloudflare-ips.json +24 -0
- package/src/data/cloudfront-ips.json +231 -0
- package/src/data/gptbot-ips.json +23 -0
- package/src/data/openai-searchbot-ips.json +37 -0
- package/src/index.js +24 -3
- package/src/input/websocket.js +33 -17
- package/src/lib/aggregator.js +68 -12
- package/src/lib/index.js +2 -0
- package/src/lib/persistence.js +72 -0
- package/src/lib/speed.js +43 -3
- package/src/lib/util.js +17 -1
- package/src/modules/agent.js +1 -1
- package/src/modules/history.js +86 -0
- package/src/modules/identity.js +101 -22
- package/src/modules/index.js +2 -0
- package/src/modules/language.js +1 -1
- package/src/modules/signature.js +15 -4
- package/src/modules/status.js +7 -7
- package/src/plugins/proxy.js +10 -203
- package/src/script.js +0 -1
- package/scripts/cloudfront-ips.js +0 -56
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@ Hyperwatch is built on a real-time stream processor handling logs from inputs of
|
|
|
12
12
|
|
|
13
13
|
## Install
|
|
14
14
|
|
|
15
|
-
Make sure you have Node.js version >=
|
|
15
|
+
Make sure you have Node.js version >= 20.
|
|
16
16
|
|
|
17
17
|
We recommend using [nvm](https://github.com/creationix/nvm): `nvm install && nvm use`.
|
|
18
18
|
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
module.exports = function (hyperwatch) {
|
|
2
|
+
const { pipeline, input, format, logger } = hyperwatch;
|
|
3
|
+
|
|
4
|
+
/* Init Hyperwatch first (will load modules) */
|
|
5
|
+
|
|
6
|
+
hyperwatch.init();
|
|
7
|
+
|
|
8
|
+
/* Input configuration */
|
|
9
|
+
/* =================== */
|
|
10
|
+
|
|
11
|
+
/* Syslog input */
|
|
12
|
+
/* ------------- */
|
|
13
|
+
|
|
14
|
+
const syslogApacheHyperwatchCombinedInput = input.syslog.create({
|
|
15
|
+
port: 1518,
|
|
16
|
+
parse: format.apache.parser({
|
|
17
|
+
format: format.apache.formats.hyperwatchCombined,
|
|
18
|
+
}),
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
pipeline.registerInput(syslogApacheHyperwatchCombinedInput);
|
|
22
|
+
|
|
23
|
+
/* Pipeline configuration */
|
|
24
|
+
/* ====================== */
|
|
25
|
+
|
|
26
|
+
pipeline
|
|
27
|
+
.getNode('main')
|
|
28
|
+
.map((log) => console.log(logger.defaultFormatter.format(log, 'console')));
|
|
29
|
+
};
|
package/config/default.js
CHANGED
|
@@ -76,7 +76,7 @@ if (require.main === module) {
|
|
|
76
76
|
try {
|
|
77
77
|
hyperwatch = require('../hyperwatch');
|
|
78
78
|
} catch (e) {
|
|
79
|
-
// eslint-disable-next-line
|
|
79
|
+
// eslint-disable-next-line n/no-missing-require
|
|
80
80
|
hyperwatch = require('@hyperwatch/hyperwatch');
|
|
81
81
|
}
|
|
82
82
|
|
package/config/example.js
CHANGED
|
@@ -165,7 +165,7 @@ if (require.main === module) {
|
|
|
165
165
|
try {
|
|
166
166
|
hyperwatch = require('../hyperwatch');
|
|
167
167
|
} catch (e) {
|
|
168
|
-
// eslint-disable-next-line
|
|
168
|
+
// eslint-disable-next-line n/no-missing-require
|
|
169
169
|
hyperwatch = require('@hyperwatch/hyperwatch');
|
|
170
170
|
}
|
|
171
171
|
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
module.exports = function (hyperwatch) {
|
|
2
|
+
const { pipeline, input, logger } = hyperwatch;
|
|
3
|
+
|
|
4
|
+
/* Init Hyperwatch first (will load modules) */
|
|
5
|
+
|
|
6
|
+
hyperwatch.init();
|
|
7
|
+
|
|
8
|
+
/* Input configuration */
|
|
9
|
+
/* =================== */
|
|
10
|
+
|
|
11
|
+
/* Syslog input */
|
|
12
|
+
/* ------------- */
|
|
13
|
+
|
|
14
|
+
const webSocketServerInput = input.websocket.create({
|
|
15
|
+
name: 'WebSocket server (JSON standard format)',
|
|
16
|
+
type: 'server',
|
|
17
|
+
path: '/input/log',
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
pipeline.registerInput(webSocketServerInput);
|
|
21
|
+
|
|
22
|
+
/* Pipeline configuration */
|
|
23
|
+
/* ====================== */
|
|
24
|
+
|
|
25
|
+
pipeline
|
|
26
|
+
.getNode('main')
|
|
27
|
+
.map((log) => console.log(logger.defaultFormatter.format(log, 'console')));
|
|
28
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hyperwatch/hyperwatch",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.3.1",
|
|
4
4
|
"description": "Open Source HTTP Traffic Manager",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "François Hodierne <francois@hodierne.net>",
|
|
@@ -10,16 +10,25 @@
|
|
|
10
10
|
],
|
|
11
11
|
"repository": {
|
|
12
12
|
"type": "git",
|
|
13
|
-
"url": "https://github.com/hyperwatch/hyperwatch.git"
|
|
13
|
+
"url": "git+https://github.com/hyperwatch/hyperwatch.git"
|
|
14
14
|
},
|
|
15
15
|
"main": "hyperwatch.js",
|
|
16
16
|
"bin": {
|
|
17
|
-
"hyperwatch": "
|
|
17
|
+
"hyperwatch": "bin/hyperwatch"
|
|
18
18
|
},
|
|
19
|
+
"files": [
|
|
20
|
+
"bin",
|
|
21
|
+
"config",
|
|
22
|
+
"docs",
|
|
23
|
+
"scripts",
|
|
24
|
+
"src",
|
|
25
|
+
"start.js"
|
|
26
|
+
],
|
|
19
27
|
"scripts": {
|
|
20
28
|
"depcheck": "depcheck",
|
|
21
|
-
"lint": "eslint
|
|
22
|
-
"lint:fix": "eslint
|
|
29
|
+
"lint": "eslint",
|
|
30
|
+
"lint:fix": "eslint --fix",
|
|
31
|
+
"prepare": "husky",
|
|
23
32
|
"prettier": "prettier \"**/*.@(js|json|md|yml)\"",
|
|
24
33
|
"prettier:check": "npm run prettier -- --list-different",
|
|
25
34
|
"prettier:write": "npm run prettier -- --write",
|
|
@@ -29,52 +38,48 @@
|
|
|
29
38
|
"dependencies": {
|
|
30
39
|
"@hyperwatch/useragent": "^3.9.3",
|
|
31
40
|
"accept-language-parser": "^1.5.0",
|
|
32
|
-
"ajv": "^8.
|
|
33
|
-
"ajv-formats": "^
|
|
34
|
-
"chalk": "^4.1.
|
|
41
|
+
"ajv": "^8.20.0",
|
|
42
|
+
"ajv-formats": "^3.0.1",
|
|
43
|
+
"chalk": "^4.1.2",
|
|
35
44
|
"country-code-emoji": "^2.3.0",
|
|
36
|
-
"
|
|
37
|
-
"
|
|
38
|
-
"
|
|
45
|
+
"csv-stringify": "^6.8.3",
|
|
46
|
+
"debug": "^4.4.3",
|
|
47
|
+
"dnsbl": "^4.0.3",
|
|
48
|
+
"express": "^4.22.2",
|
|
39
49
|
"express-ws": "^5.0.2",
|
|
40
|
-
"geoip-lite": "^1.4.
|
|
41
|
-
"immutable": "^
|
|
42
|
-
"ip-cidr": "^
|
|
43
|
-
"lodash": "^4.
|
|
44
|
-
"lru-cache": "^
|
|
45
|
-
"micro-strptime": "^0.
|
|
50
|
+
"geoip-lite": "^1.4.10",
|
|
51
|
+
"immutable": "^5.1.9",
|
|
52
|
+
"ip-cidr": "^4.0.2",
|
|
53
|
+
"lodash": "^4.18.1",
|
|
54
|
+
"lru-cache": "^11.5.2",
|
|
55
|
+
"micro-strptime": "^1.0.0",
|
|
46
56
|
"proxy-addr": "^2.0.7",
|
|
47
57
|
"rc": "^1.2.8",
|
|
48
58
|
"syslog-parse": "^1.3.1",
|
|
49
59
|
"tail": "^2.2.6",
|
|
50
|
-
"
|
|
51
|
-
"ws": "^8.14.2"
|
|
60
|
+
"ws": "^8.21.3"
|
|
52
61
|
},
|
|
53
62
|
"devDependencies": {
|
|
63
|
+
"@eslint/js": "^9.39.5",
|
|
54
64
|
"depcheck": "^1.4.7",
|
|
55
|
-
"eslint": "^
|
|
56
|
-
"eslint-plugin-import": "^2.
|
|
57
|
-
"eslint-plugin-
|
|
58
|
-
"
|
|
59
|
-
"
|
|
60
|
-
"
|
|
61
|
-
"
|
|
62
|
-
"prettier": "^3.
|
|
65
|
+
"eslint": "^9.39.5",
|
|
66
|
+
"eslint-plugin-import": "^2.32.0",
|
|
67
|
+
"eslint-plugin-n": "^17.24.0",
|
|
68
|
+
"globals": "^17.11.0",
|
|
69
|
+
"husky": "^9.1.7",
|
|
70
|
+
"lint-staged": "^16.4.0",
|
|
71
|
+
"mocha": "^11.8.0",
|
|
72
|
+
"prettier": "^3.9.6",
|
|
63
73
|
"prettier-package-json": "^2.8.0"
|
|
64
74
|
},
|
|
65
75
|
"engines": {
|
|
66
|
-
"node": ">=
|
|
76
|
+
"node": ">=20"
|
|
67
77
|
},
|
|
68
78
|
"depcheck": {
|
|
69
79
|
"ignores": [
|
|
70
80
|
"prettier-package-json"
|
|
71
81
|
]
|
|
72
82
|
},
|
|
73
|
-
"husky": {
|
|
74
|
-
"hooks": {
|
|
75
|
-
"pre-commit": "lint-staged"
|
|
76
|
-
}
|
|
77
|
-
},
|
|
78
83
|
"lint-staged": {
|
|
79
84
|
"*.{js,json,md,yml}": [
|
|
80
85
|
"prettier --write"
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
const dataDir = path.join(__dirname, '..', 'src', 'data');
|
|
5
|
+
|
|
6
|
+
async function fetchLines(url) {
|
|
7
|
+
const res = await fetch(url);
|
|
8
|
+
if (!res.ok) {
|
|
9
|
+
throw new Error(`Failed to fetch ${url}: ${res.status}`);
|
|
10
|
+
}
|
|
11
|
+
const text = await res.text();
|
|
12
|
+
return text.trim().split('\n').filter(Boolean);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async function main() {
|
|
16
|
+
const ipv4 = await fetchLines('https://www.cloudflare.com/ips-v4/');
|
|
17
|
+
const ipv6 = await fetchLines('https://www.cloudflare.com/ips-v6/');
|
|
18
|
+
const cidrs = [...ipv4, ...ipv6];
|
|
19
|
+
|
|
20
|
+
const filePath = path.join(dataDir, 'cloudflare-ips.json');
|
|
21
|
+
fs.writeFileSync(filePath, `${JSON.stringify(cidrs, null, 2)}\n`);
|
|
22
|
+
console.log(
|
|
23
|
+
`cloudflare-ips: ${cidrs.length} CIDRs (${ipv4.length} IPv4, ${ipv6.length} IPv6)`
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
main().catch(console.error);
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
const dataDir = path.join(__dirname, '..', 'src', 'data');
|
|
5
|
+
|
|
6
|
+
async function main() {
|
|
7
|
+
const response = await fetch(
|
|
8
|
+
'https://ip-ranges.amazonaws.com/ip-ranges.json'
|
|
9
|
+
);
|
|
10
|
+
const ipRanges = await response.json();
|
|
11
|
+
|
|
12
|
+
const ipv4 = ipRanges.prefixes
|
|
13
|
+
.filter((p) => p.service === 'CLOUDFRONT')
|
|
14
|
+
.map((p) => p.ip_prefix);
|
|
15
|
+
|
|
16
|
+
const ipv6 = ipRanges.ipv6_prefixes
|
|
17
|
+
.filter((p) => p.service === 'CLOUDFRONT')
|
|
18
|
+
.map((p) => p.ipv6_prefix);
|
|
19
|
+
|
|
20
|
+
const cidrs = [...ipv4, ...ipv6];
|
|
21
|
+
|
|
22
|
+
const filePath = path.join(dataDir, 'cloudfront-ips.json');
|
|
23
|
+
fs.writeFileSync(filePath, `${JSON.stringify(cidrs, null, 2)}\n`);
|
|
24
|
+
console.log(
|
|
25
|
+
`cloudfront-ips: ${cidrs.length} CIDRs (${ipv4.length} IPv4, ${ipv6.length} IPv6)`
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
main().catch(console.error);
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
const dataDir = path.join(__dirname, '..', 'src', 'data');
|
|
5
|
+
|
|
6
|
+
const sources = [
|
|
7
|
+
{ name: 'chatgpt-user-ips', url: 'https://openai.com/chatgpt-user.json' },
|
|
8
|
+
{ name: 'gptbot-ips', url: 'https://openai.com/gptbot.json' },
|
|
9
|
+
{ name: 'openai-searchbot-ips', url: 'https://openai.com/searchbot.json' },
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
async function fetchAndStore({ name, url }) {
|
|
13
|
+
const res = await fetch(url);
|
|
14
|
+
if (!res.ok) {
|
|
15
|
+
console.error(`Failed to fetch ${name}: ${res.status} ${res.statusText}`);
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
const data = await res.json();
|
|
19
|
+
const cidrs = data.prefixes.map((p) => {
|
|
20
|
+
const cidr = p.ipv4Prefix || p.ip_prefix;
|
|
21
|
+
return cidr.includes('/') ? cidr : `${cidr}/32`;
|
|
22
|
+
});
|
|
23
|
+
const filePath = path.join(dataDir, `${name}.json`);
|
|
24
|
+
fs.writeFileSync(filePath, `${JSON.stringify(cidrs, null, 2)}\n`);
|
|
25
|
+
console.log(`${name}: ${cidrs.length} CIDRs`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function main() {
|
|
29
|
+
for (const source of sources) {
|
|
30
|
+
await fetchAndStore(source);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
main().catch(console.error);
|
package/src/app/api.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
|
+
const crypto = require('crypto');
|
|
1
2
|
const fs = require('fs');
|
|
2
3
|
const path = require('path');
|
|
3
4
|
|
|
5
|
+
const { stringify } = require('csv-stringify/sync');
|
|
4
6
|
const express = require('express');
|
|
5
|
-
const uuid = require('uuid');
|
|
6
7
|
|
|
7
8
|
const monitoring = require('../lib/monitoring');
|
|
9
|
+
const persistence = require('../lib/persistence');
|
|
8
10
|
const { formatTable } = require('../lib/util');
|
|
9
11
|
const stylesheet = require('../stylesheet');
|
|
10
12
|
|
|
@@ -47,7 +49,7 @@ app.streamToHttp = (
|
|
|
47
49
|
updateMonitoringStatus();
|
|
48
50
|
|
|
49
51
|
app.get(endpoint, (req, res) => {
|
|
50
|
-
const requestId =
|
|
52
|
+
const requestId = crypto.randomUUID();
|
|
51
53
|
requests[requestId] = [req, res];
|
|
52
54
|
updateMonitoringStatus();
|
|
53
55
|
|
|
@@ -87,20 +89,33 @@ body { display: flex; flex-direction: column-reverse; }
|
|
|
87
89
|
};
|
|
88
90
|
|
|
89
91
|
app.registerAggregator = (name, aggregator) => {
|
|
90
|
-
|
|
92
|
+
persistence.register(name, aggregator);
|
|
93
|
+
app.get(`/${name}.:format(json|csv)?`, (req, res) => {
|
|
91
94
|
const raw = req.query.raw ? true : false;
|
|
92
95
|
const format = req.params.format || (raw ? 'json' : null);
|
|
93
96
|
const limit = req.query.limit || 100;
|
|
94
|
-
const sort = req.query.sort || '
|
|
97
|
+
const sort = req.query.sort || 'count15m';
|
|
95
98
|
|
|
96
99
|
const data = aggregator.getData({
|
|
97
100
|
sort,
|
|
98
101
|
limit,
|
|
99
|
-
format,
|
|
102
|
+
format: format === 'csv' ? 'json' : format,
|
|
100
103
|
raw,
|
|
101
104
|
});
|
|
102
105
|
|
|
103
|
-
if (format === '
|
|
106
|
+
if (format === 'csv') {
|
|
107
|
+
const rows = data.toJS();
|
|
108
|
+
const columns = Object.keys(rows[0] || {}).filter(
|
|
109
|
+
(k) => k !== 'activity'
|
|
110
|
+
);
|
|
111
|
+
const csv = stringify(rows, { header: true, columns });
|
|
112
|
+
res.setHeader('Content-Type', 'text/csv; charset=utf-8');
|
|
113
|
+
res.setHeader(
|
|
114
|
+
'Content-Disposition',
|
|
115
|
+
`attachment; filename="${name}.csv"`
|
|
116
|
+
);
|
|
117
|
+
res.send(csv);
|
|
118
|
+
} else if (format === 'json') {
|
|
104
119
|
res.send(data);
|
|
105
120
|
} else {
|
|
106
121
|
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
package/src/app/websocket.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
const crypto = require('crypto');
|
|
2
|
+
|
|
1
3
|
const express = require('express');
|
|
2
4
|
const expressWs = require('express-ws');
|
|
3
|
-
const uuid = require('uuid');
|
|
4
5
|
|
|
6
|
+
const constants = require('../constants');
|
|
5
7
|
const monitoring = require('../lib/monitoring');
|
|
6
8
|
|
|
7
9
|
const app = express();
|
|
@@ -36,18 +38,27 @@ app.streamToWebsocket = (
|
|
|
36
38
|
updateMonitoringStatus();
|
|
37
39
|
|
|
38
40
|
app.ws(endpoint, (client, req) => {
|
|
39
|
-
const clientId = req.query.clientId ||
|
|
41
|
+
const clientId = req.query.clientId || crypto.randomUUID();
|
|
40
42
|
if (clients[clientId]) {
|
|
41
43
|
console.log(`Client '${clientId}' is already connected. Terminating.`);
|
|
42
44
|
client.terminate();
|
|
43
45
|
return;
|
|
44
46
|
}
|
|
45
47
|
clients[clientId] = client;
|
|
48
|
+
client.isAlive = true;
|
|
46
49
|
updateMonitoringStatus();
|
|
50
|
+
client.on('pong', () => {
|
|
51
|
+
client.isAlive = true;
|
|
52
|
+
});
|
|
47
53
|
client.on('close', () => {
|
|
54
|
+
console.log(`Client '${clientId}' closed.`);
|
|
48
55
|
delete clients[clientId];
|
|
49
56
|
updateMonitoringStatus();
|
|
50
57
|
});
|
|
58
|
+
client.on('error', (error) => {
|
|
59
|
+
console.log(`Client '${clientId}' error.`);
|
|
60
|
+
console.log(error);
|
|
61
|
+
});
|
|
51
62
|
});
|
|
52
63
|
|
|
53
64
|
stream.map((log) => {
|
|
@@ -60,6 +71,21 @@ app.streamToWebsocket = (
|
|
|
60
71
|
}
|
|
61
72
|
});
|
|
62
73
|
});
|
|
74
|
+
|
|
75
|
+
// Heartbeat: detect and clean up stale connections
|
|
76
|
+
setInterval(() => {
|
|
77
|
+
Object.entries(clients).forEach(([clientId, client]) => {
|
|
78
|
+
if (!client.isAlive) {
|
|
79
|
+
console.log(`Client '${clientId}' stale. Terminating.`);
|
|
80
|
+
client.terminate();
|
|
81
|
+
delete clients[clientId];
|
|
82
|
+
updateMonitoringStatus();
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
client.isAlive = false;
|
|
86
|
+
client.ping();
|
|
87
|
+
});
|
|
88
|
+
}, constants.heartbeatInterval || 30000);
|
|
63
89
|
};
|
|
64
90
|
|
|
65
91
|
module.exports = app;
|
package/src/constants.js
CHANGED
|
@@ -47,11 +47,20 @@ const constants = {
|
|
|
47
47
|
active: false,
|
|
48
48
|
priority: 620,
|
|
49
49
|
},
|
|
50
|
+
history: {
|
|
51
|
+
active: false,
|
|
52
|
+
priority: 700,
|
|
53
|
+
},
|
|
50
54
|
sparkline: {
|
|
51
55
|
active: false,
|
|
52
56
|
priority: 800,
|
|
53
57
|
},
|
|
54
58
|
},
|
|
59
|
+
persistence: {
|
|
60
|
+
enabled: false,
|
|
61
|
+
path: null,
|
|
62
|
+
namespace: null,
|
|
63
|
+
},
|
|
55
64
|
};
|
|
56
65
|
|
|
57
66
|
module.exports = rc('hyperwatch', constants);
|