@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 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 >= 14.
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 node/no-missing-require
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 node/no-missing-require
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.2.0",
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": "./bin/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 '**/*.js'",
22
- "lint:fix": "eslint '**/*.js' --fix",
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.12.0",
33
- "ajv-formats": "^2.1.1",
34
- "chalk": "^4.1.0",
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
- "debug": "^4.3.4",
37
- "dnsbl": "^4.0.1",
38
- "express": "^4.18.2",
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.8",
41
- "immutable": "^4.3.4",
42
- "ip-cidr": "^3.1.0",
43
- "lodash": "^4.17.21",
44
- "lru-cache": "^10.0.1",
45
- "micro-strptime": "^0.2.3",
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
- "uuid": "^9.0.1",
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": "^8.53.0",
56
- "eslint-plugin-import": "^2.29.0",
57
- "eslint-plugin-node": "^11.1.0",
58
- "husky": "^4.3.5",
59
- "lint-staged": "^10.5.3",
60
- "mocha": "^10.2.0",
61
- "node-fetch": "^2.0.0",
62
- "prettier": "^3.3.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": ">=14.0"
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 = uuid.v4();
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
- app.get(`/${name}.:format(json)?`, (req, res) => {
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 || '15m';
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 === 'json') {
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');
@@ -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 || uuid.v4();
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);