@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
@@ -10,10 +10,10 @@ Let's start.
10
10
 
11
11
  On the same server where Apache is running, or on a server that is reachable by it, install the Hyperwatch processor.
12
12
 
13
- As a prerequirement, you'll need Node.js >= 7. Use nvm if you're in trouble.
13
+ As a prerequisite, you'll need Node.js >= 24. We recommend [nvm](https://github.com/nvm-sh/nvm).
14
14
 
15
15
  ```bash
16
- nvm install node
16
+ nvm install 24
17
17
  ```
18
18
 
19
19
  #### Install from npm
@@ -24,7 +24,7 @@ npm install -g @hyperwatch/hyperwatch
24
24
 
25
25
  #### Install from Git
26
26
 
27
- Alternatively, for developemnt purpose, you can use Git and clone the public repository:
27
+ Alternatively, for development purpose, you can use Git and clone the public repository:
28
28
 
29
29
  ```bash
30
30
  git clone https://github.com/hyperwatch/hyperwatch.git
@@ -34,13 +34,13 @@ npm install
34
34
 
35
35
  ### Configure Hyperwatch
36
36
 
37
- In our suggested configuration, Hyperwatch will be listening for access logs in the the `access_watch_combined` format on port `1518`.
37
+ In our suggested configuration, Hyperwatch will be listening for access logs in the `hyperwatch_combined` format on port `1518`.
38
38
 
39
- We always recommand using the `access_watch_combined` format, which is logging more detailed information and allows for a much better analysis than the regular `combined` format.
39
+ We always recommend using the `hyperwatch_combined` format, which is logging more detailed information and allows for a much better analysis than the regular `combined` format.
40
40
 
41
- To get more familiar, you can inspect default and example configurations in [`config/default.js`](<(../../config/default.js)>) and [`config/example.js`](../../config/example.js) file.
41
+ To get more familiar, you can inspect default and example configurations in [`config/default.js`](../../config/default.js) and [`config/example.js`](../../config/example.js) file.
42
42
 
43
- Now, you can create your own configuration in `apache_syslog_example.js`:
43
+ Now, you can create your own configuration in `apache_syslog_example.js` (a complete version is available in [`config/apache_syslog_example.js`](../../config/apache_syslog_example.js)):
44
44
 
45
45
  ```javascript
46
46
  module.exports = function (hyperwatch) {
@@ -61,7 +61,7 @@ module.exports = function (hyperwatch) {
61
61
 
62
62
  ### Configure Apache
63
63
 
64
- First, if you're following our recommendation and opted for the `hyperwatch_combined` format, you need to define it in the Apache configuration. This will not replace the standard log format, just create an additional one.
64
+ First, if you're following our recommendation and opted for the `hyperwatch_combined` format, you need to define it in the Apache configuration. This will not replace the standard log format, just create an additional one.
65
65
 
66
66
  ```
67
67
  LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-agent}i\" \"%{Accept}i\" \"%{Accept-Charset}i\" \"%{Accept-Encoding}i\" \"%{Accept-Language}i\" \"%{Connection}i\" \"%{Dnt}i\" \"%{From}i\" \"%{Host}i\"" hyperwatch_combined
@@ -79,13 +79,13 @@ Note: This is known to be working on _Ubuntu 16.04, 18.04_ with _logger 2.27.1,
79
79
 
80
80
  In this example, there are 3 important things:
81
81
 
82
- 1. If Hyperwatch is running on the same server, we can use `localhost` as IP address.
83
- If it's on a different server, replace `localhost` by the proper private or public IP address.
84
- 2. We configured Hyperwatch to listen for syslog messages in the `hyperwatch_combined` format on port `1518`.
82
+ 1. If Hyperwatch is running on the same server, we can use `localhost` as IP address.
83
+ If it's on a different server, replace `localhost` by the proper private or public IP address.
84
+ 2. We configured Hyperwatch to listen for syslog messages in the `hyperwatch_combined` format on port `1518`.
85
85
  We're properly passing that port in the configuration
86
- 3. Finally, we're asking Apache to use the `hyperwatch_combined` log format we previously configured.
86
+ 3. Finally, we're asking Apache to use the `hyperwatch_combined` log format we previously configured.
87
87
 
88
- Don't forget to reload Aapche with the updated configuration. On Ubuntu, it would be:
88
+ Don't forget to reload Apache with the updated configuration. On Ubuntu, it would be:
89
89
 
90
90
  ```bash
91
91
  service apache2 reload
@@ -101,4 +101,6 @@ hyperwatch apache_syslog_example.js
101
101
 
102
102
  ### Browse the interface
103
103
 
104
- Now, you can point your browser to the IP/port where Hyperwatch is running. If you see data flowing, congrats you made it!
104
+ Now, you can point your browser to the `/status` page on the IP/port where Hyperwatch is running (e.g. `http://localhost:3000/status`). If you see traffic going through your input, congrats you made it!
105
+
106
+ To watch the logs live at `/logs/main` and explore aggregations such as `/addresses` or `/identities`, activate the corresponding modules. See [Global Configuration](../configuration.md#modules).
@@ -10,10 +10,10 @@ Let's start!
10
10
 
11
11
  On the same server where the Node/Express application is running, or on a server that is reachable by it, install the Hyperwatch processor.
12
12
 
13
- As a prerequirement, you'll need Node.js &gt;= 7. Use nvm if you're in trouble.
13
+ As a prerequisite, you'll need Node.js >= 24. We recommend [nvm](https://github.com/nvm-sh/nvm).
14
14
 
15
15
  ```bash
16
- nvm install node
16
+ nvm install 24
17
17
  ```
18
18
 
19
19
  #### Install from npm
@@ -24,7 +24,7 @@ npm install -g @hyperwatch/hyperwatch
24
24
 
25
25
  #### Install from Git
26
26
 
27
- Alternatively, for developement purpose, you can use Git and clone the public repository:
27
+ Alternatively, for development purpose, you can use Git and clone the public repository:
28
28
 
29
29
  ```bash
30
30
  git clone https://github.com/hyperwatch/hyperwatch.git
@@ -38,7 +38,7 @@ In our suggested configuration, Hyperwatch will be listening for access logs usi
38
38
 
39
39
  All communications between your Node/Express application and Hyperwatch will be happening in clear, please only use that setup on your internal network. If on the public internet, we're advising to use the Websocket Secure protocol (wss) which is straightforward but out of the scope of this tutorial.
40
40
 
41
- Now, you can create your own configuration in `express_websocket_example.js`:
41
+ Now, you can create your own configuration in `express_websocket_example.js` (a complete version is available in [`config/express_websocket_example.js`](../../config/express_websocket_example.js)):
42
42
 
43
43
  ```javascript
44
44
  module.exports = function (hyperwatch) {
@@ -77,10 +77,10 @@ app.use(hyperwatchExpressLogger('websocket', 'ws://localhost:3000/input/log'));
77
77
 
78
78
  In this example, there are 3 important things:
79
79
 
80
- 1. If Hyperwatch is running on the same server, we can use `localhost` as IP address.
81
- If it's on a different server, replace `localhost` by the proper private or public IP address.
80
+ 1. If Hyperwatch is running on the same server, we can use `localhost` as IP address.
81
+ If it's on a different server, replace `localhost` by the proper private or public IP address.
82
82
  2. Replace the port (here `3000`) by the relevant one, it should be the main port where Hyperwatch is running.
83
- 3. Finally, the path `/input/log` should match the one configured on Hyperwatch side, If you're following this tutorial from start to begin, nothing to change!
83
+ 3. Finally, the path `/input/log` should match the one configured on Hyperwatch side, If you're following this tutorial from start to end, nothing to change!
84
84
 
85
85
  Now, that you added and configured the Hyperwatch middleware, you can deploy and restart your application.
86
86
 
@@ -96,4 +96,6 @@ hyperwatch express_websocket_example.js
96
96
 
97
97
  ### Browse the interface
98
98
 
99
- Now, you can point your browser to the IP/port where Hyperwatch is running. If you see data flowing, congrats you made it!
99
+ Now, you can point your browser to the `/status` page on the IP/port where Hyperwatch is running (e.g. `http://localhost:3000/status`). If you see traffic going through your input, congrats you made it!
100
+
101
+ To watch the logs live at `/logs/main` and explore aggregations such as `/addresses` or `/identities`, activate the corresponding modules. See [Global Configuration](../configuration.md#modules).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyperwatch/hyperwatch",
3
- "version": "4.2.0",
3
+ "version": "5.0.0",
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,50 @@
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": "^6.0.0",
35
44
  "country-code-emoji": "^2.3.0",
36
- "debug": "^4.3.4",
37
- "dnsbl": "^4.0.1",
38
- "express": "^4.18.2",
39
- "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",
45
+ "csv-stringify": "^6.8.3",
46
+ "debug": "^4.4.3",
47
+ "dnsbl": "^5.1.1",
48
+ "express": "^5.2.1",
49
+ "geoip-lite": "^2.0.3",
50
+ "immutable": "^5.1.9",
51
+ "ip-cidr": "^4.0.2",
52
+ "lodash": "^4.18.1",
53
+ "lru-cache": "^11.5.2",
54
+ "micro-strptime": "^1.0.0",
46
55
  "proxy-addr": "^2.0.7",
47
56
  "rc": "^1.2.8",
48
- "syslog-parse": "^1.3.1",
57
+ "syslog-parse": "^2.0.0",
49
58
  "tail": "^2.2.6",
50
- "uuid": "^9.0.1",
51
- "ws": "^8.14.2"
59
+ "ws": "^8.21.3"
52
60
  },
53
61
  "devDependencies": {
62
+ "@eslint/js": "^10.0.1",
54
63
  "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",
64
+ "eslint": "^10.9.1",
65
+ "eslint-plugin-import-x": "^4.17.1",
66
+ "eslint-plugin-n": "^18.3.0",
67
+ "globals": "^17.11.0",
68
+ "husky": "^9.1.7",
69
+ "lint-staged": "^17.4.1",
70
+ "mocha": "^12.0.0",
71
+ "prettier": "^3.9.6",
63
72
  "prettier-package-json": "^2.8.0"
64
73
  },
65
74
  "engines": {
66
- "node": ">=14.0"
75
+ "node": ">=24"
76
+ },
77
+ "allowScripts": {
78
+ "unrs-resolver": false
67
79
  },
68
80
  "depcheck": {
69
81
  "ignores": [
70
82
  "prettier-package-json"
71
83
  ]
72
84
  },
73
- "husky": {
74
- "hooks": {
75
- "pre-commit": "lint-staged"
76
- }
77
- },
78
85
  "lint-staged": {
79
86
  "*.{js,json,md,yml}": [
80
87
  "prettier --write"
@@ -0,0 +1,60 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ const dataDir = path.join(__dirname, '..', 'src', 'data');
5
+
6
+ // Anthropic publishes a single list covering all Claude crawlers
7
+ // (ClaudeBot, Claude-User, Claude-SearchBot).
8
+ const sources = [
9
+ { name: 'claude-bot-ips', url: 'https://claude.com/crawling/bots.json' },
10
+ ];
11
+
12
+ function extractCidrs(data) {
13
+ const prefixes = Array.isArray(data) ? data : data.prefixes;
14
+ if (!Array.isArray(prefixes)) {
15
+ throw new Error('unexpected payload: no prefixes array');
16
+ }
17
+ return prefixes
18
+ .map((prefix) => {
19
+ if (typeof prefix === 'string') {
20
+ return prefix;
21
+ }
22
+ return prefix.ipv4Prefix || prefix.ipv6Prefix || prefix.ip_prefix;
23
+ })
24
+ .filter(Boolean)
25
+ .map((cidr) => {
26
+ if (cidr.includes('/')) {
27
+ return cidr;
28
+ }
29
+ return cidr.includes(':') ? `${cidr}/128` : `${cidr}/32`;
30
+ });
31
+ }
32
+
33
+ async function fetchAndStore({ name, url }) {
34
+ const res = await fetch(url);
35
+ if (!res.ok) {
36
+ console.error(`Failed to fetch ${name}: ${res.status} ${res.statusText}`);
37
+ process.exitCode = 1;
38
+ return;
39
+ }
40
+ const cidrs = extractCidrs(await res.json());
41
+ if (cidrs.length === 0) {
42
+ console.error(`Failed to fetch ${name}: empty list, keeping current file`);
43
+ process.exitCode = 1;
44
+ return;
45
+ }
46
+ const filePath = path.join(dataDir, `${name}.json`);
47
+ fs.writeFileSync(filePath, `${JSON.stringify(cidrs, null, 2)}\n`);
48
+ console.log(`${name}: ${cidrs.length} CIDRs`);
49
+ }
50
+
51
+ async function main() {
52
+ for (const source of sources) {
53
+ await fetchAndStore(source);
54
+ }
55
+ }
56
+
57
+ main().catch((error) => {
58
+ console.error(error);
59
+ process.exitCode = 1;
60
+ });
@@ -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,19 +1,137 @@
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');
10
+ const pipeline = require('../lib/pipeline');
8
11
  const { formatTable } = require('../lib/util');
9
12
  const stylesheet = require('../stylesheet');
10
13
 
14
+ const wsServer = require('./ws-server');
15
+
11
16
  const script = fs.readFileSync(path.join(__dirname, '..', 'script.js'));
12
17
 
13
18
  const app = express();
14
19
 
20
+ // WebSocket upgrades dispatched by mount() when Hyperwatch is embedded
21
+ app.use(wsServer.middleware);
22
+
15
23
  app.use(express.json());
16
24
 
25
+ function renderHtmlTree(node) {
26
+ const label = [];
27
+ if (node.name) {
28
+ label.push(`<strong>${node.name}</strong>`);
29
+ }
30
+ if (node.op) {
31
+ label.push(`<span class="op">[${node.op}]</span>`);
32
+ }
33
+ if (node.module) {
34
+ label.push(`<span class="module">(${node.module})</span>`);
35
+ }
36
+ if (node.fnName) {
37
+ label.push(`<span class="fn">${node.fnName}</span>`);
38
+ }
39
+ if (node.label) {
40
+ label.push(`<span class="label">${node.label}</span>`);
41
+ }
42
+
43
+ let html = `<li>${label.join(' ')}`;
44
+ if (node.children && node.children.length > 0) {
45
+ html += '<ul>';
46
+ for (const child of node.children) {
47
+ html += renderHtmlTree(child);
48
+ }
49
+ html += '</ul>';
50
+ }
51
+ html += '</li>';
52
+ return html;
53
+ }
54
+
55
+ function renderHtmlInputs(inputs) {
56
+ if (!inputs || inputs.length === 0) {
57
+ return '';
58
+ }
59
+ let html = '<ul>';
60
+ for (const input of inputs) {
61
+ html += `<li><strong>${input.name}</strong> <span class="op">[input]</span>`;
62
+ if (input.status) {
63
+ html += ` <span class="module">(${input.status})</span>`;
64
+ }
65
+ html += ` accepted: ${input.accepted}, rejected: ${input.rejected}`;
66
+ if (input.tree) {
67
+ html += `<ul>${renderHtmlTree(input.tree)}</ul>`;
68
+ }
69
+ html += '</li>';
70
+ }
71
+ html += '</ul>';
72
+ return html;
73
+ }
74
+
75
+ app.get('/nodes{.:format}', (req, res) => {
76
+ const nodes = Object.keys(pipeline.nodes);
77
+ const format = req.params.format;
78
+ const view = req.query.view;
79
+
80
+ if (format && !['csv', 'json'].includes(format)) {
81
+ res.sendStatus(404);
82
+ return;
83
+ }
84
+
85
+ if (format === 'csv') {
86
+ const csv = stringify(
87
+ nodes.map((name) => ({ name })),
88
+ {
89
+ header: true,
90
+ columns: ['name'],
91
+ }
92
+ );
93
+ res.setHeader('Content-Type', 'text/csv; charset=utf-8');
94
+ res.setHeader('Content-Disposition', 'attachment; filename="nodes.csv"');
95
+ res.send(csv);
96
+ } else if (format === 'json') {
97
+ if (view === 'tree') {
98
+ res.json(pipeline.getTree());
99
+ } else {
100
+ res.json(nodes);
101
+ }
102
+ } else {
103
+ res.setHeader('Content-Type', 'text/html; charset=utf-8');
104
+ if (view === 'tree') {
105
+ const tree = pipeline.getTree();
106
+ res.send(
107
+ `<!DOCTYPE html>
108
+ <html>
109
+ <head>
110
+ <style>${stylesheet}
111
+ .op { color: #666; }
112
+ .module { color: #0a0; }
113
+ .fn { color: #00a; }
114
+ .label { color: #a50; }
115
+ ul { list-style: none; padding-left: 1.5em; }
116
+ </style>
117
+ </head>
118
+ <body>${renderHtmlInputs(tree.inputs)}<ul>${renderHtmlTree(tree)}</ul></body>
119
+ </html>`
120
+ );
121
+ } else {
122
+ res.send(
123
+ `<!DOCTYPE html>
124
+ <html>
125
+ <head>
126
+ <style>${stylesheet}</style>
127
+ </head>
128
+ <body>${formatTable(nodes.map((name) => ({ name })))}</body>
129
+ </html>`
130
+ );
131
+ }
132
+ }
133
+ });
134
+
17
135
  app.streamToHttp = (
18
136
  endpoint,
19
137
  stream,
@@ -47,7 +165,7 @@ app.streamToHttp = (
47
165
  updateMonitoringStatus();
48
166
 
49
167
  app.get(endpoint, (req, res) => {
50
- const requestId = uuid.v4();
168
+ const requestId = crypto.randomUUID();
51
169
  requests[requestId] = [req, res];
52
170
  updateMonitoringStatus();
53
171
 
@@ -83,24 +201,42 @@ body { display: flex; flex-direction: column-reverse; }
83
201
  res.write(`<div>${line}</div>\n`);
84
202
  }
85
203
  });
86
- });
204
+ }, `http:${endpoint}`);
87
205
  };
88
206
 
89
207
  app.registerAggregator = (name, aggregator) => {
90
- app.get(`/${name}.:format(json)?`, (req, res) => {
208
+ persistence.register(name, aggregator);
209
+ app.get(`/${name}{.:format}`, (req, res) => {
91
210
  const raw = req.query.raw ? true : false;
92
211
  const format = req.params.format || (raw ? 'json' : null);
93
212
  const limit = req.query.limit || 100;
94
- const sort = req.query.sort || '15m';
213
+ const sort = req.query.sort || 'count15m';
214
+
215
+ if (format && !['csv', 'json'].includes(format)) {
216
+ res.sendStatus(404);
217
+ return;
218
+ }
95
219
 
96
220
  const data = aggregator.getData({
97
221
  sort,
98
222
  limit,
99
- format,
223
+ format: format === 'csv' ? 'json' : format,
100
224
  raw,
101
225
  });
102
226
 
103
- if (format === 'json') {
227
+ if (format === 'csv') {
228
+ const rows = data.toJS();
229
+ const columns = Object.keys(rows[0] || {}).filter(
230
+ (k) => k !== 'activity'
231
+ );
232
+ const csv = stringify(rows, { header: true, columns });
233
+ res.setHeader('Content-Type', 'text/csv; charset=utf-8');
234
+ res.setHeader(
235
+ 'Content-Disposition',
236
+ `attachment; filename="${name}.csv"`
237
+ );
238
+ res.send(csv);
239
+ } else if (format === 'json') {
104
240
  res.send(data);
105
241
  } else {
106
242
  res.setHeader('Content-Type', 'text/html; charset=utf-8');
@@ -117,7 +253,12 @@ app.registerAggregator = (name, aggregator) => {
117
253
  }
118
254
  });
119
255
 
120
- app.get(`/${name}/:identifier.:format(json)?`, (req, res) => {
256
+ app.delete(`/${name}`, (req, res) => {
257
+ aggregator.reset();
258
+ res.send({ success: true });
259
+ });
260
+
261
+ app.get(`/${name}/:identifier{.json}`, (req, res) => {
121
262
  const entry = aggregator.get(req.params.identifier);
122
263
  if (!entry) {
123
264
  res.status(404).send('Not Found');
package/src/app/index.js CHANGED
@@ -1,18 +1,22 @@
1
1
  const http = require('http');
2
2
 
3
3
  const express = require('express');
4
- const expressWs = require('express-ws');
5
4
 
6
5
  const constants = require('../constants');
7
6
 
8
7
  const api = require('./api');
8
+ const mount = require('./mount');
9
9
  const websocket = require('./websocket');
10
+ const wsServer = require('./ws-server');
10
11
 
11
12
  const app = express();
12
13
  const httpServer = http.createServer(app);
13
- expressWs(app, httpServer);
14
14
 
15
- app.use(api, websocket);
15
+ httpServer.on('upgrade', (request, socket, head) => {
16
+ wsServer.handleUpgrade(request, socket, head);
17
+ });
18
+
19
+ app.use(api);
16
20
 
17
21
  function start() {
18
22
  const port = process.env.PORT || constants.port;
@@ -25,4 +29,4 @@ function stop() {
25
29
  httpServer.close();
26
30
  }
27
31
 
28
- module.exports = { api, start, stop, websocket };
32
+ module.exports = { api, mount, start, stop, websocket };