spider-watch 0.1.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.
- package/README.md +274 -0
- package/dist/config/defaults.d.ts +5 -0
- package/dist/config/defaults.d.ts.map +1 -0
- package/dist/config/defaults.js +70 -0
- package/dist/config/env-loader.d.ts +3 -0
- package/dist/config/env-loader.d.ts.map +1 -0
- package/dist/config/env-loader.js +29 -0
- package/dist/config/validate.d.ts +3 -0
- package/dist/config/validate.d.ts.map +1 -0
- package/dist/config/validate.js +19 -0
- package/dist/create-monitoring.d.ts +3 -0
- package/dist/create-monitoring.d.ts.map +1 -0
- package/dist/create-monitoring.js +73 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/middleware/auth-basic.d.ts +4 -0
- package/dist/middleware/auth-basic.d.ts.map +1 -0
- package/dist/middleware/auth-basic.js +34 -0
- package/dist/middleware/capture.d.ts +7 -0
- package/dist/middleware/capture.d.ts.map +1 -0
- package/dist/middleware/capture.js +68 -0
- package/dist/middleware/error.d.ts +4 -0
- package/dist/middleware/error.d.ts.map +1 -0
- package/dist/middleware/error.js +27 -0
- package/dist/repository/monitoring-repository.d.ts +4 -0
- package/dist/repository/monitoring-repository.d.ts.map +1 -0
- package/dist/repository/monitoring-repository.js +239 -0
- package/dist/repository/sqlite-db.d.ts +7 -0
- package/dist/repository/sqlite-db.d.ts.map +1 -0
- package/dist/repository/sqlite-db.js +91 -0
- package/dist/router/async-handler.d.ts +3 -0
- package/dist/router/async-handler.d.ts.map +1 -0
- package/dist/router/async-handler.js +5 -0
- package/dist/router/monitoring-router.d.ts +4 -0
- package/dist/router/monitoring-router.d.ts.map +1 -0
- package/dist/router/monitoring-router.js +109 -0
- package/dist/services/console-hook.d.ts +12 -0
- package/dist/services/console-hook.d.ts.map +1 -0
- package/dist/services/console-hook.js +61 -0
- package/dist/services/context.d.ts +7 -0
- package/dist/services/context.d.ts.map +1 -0
- package/dist/services/context.js +22 -0
- package/dist/services/http-client.d.ts +7 -0
- package/dist/services/http-client.d.ts.map +1 -0
- package/dist/services/http-client.js +56 -0
- package/dist/services/instrumentation.d.ts +8 -0
- package/dist/services/instrumentation.d.ts.map +1 -0
- package/dist/services/instrumentation.js +9 -0
- package/dist/services/recorder.d.ts +5 -0
- package/dist/services/recorder.d.ts.map +1 -0
- package/dist/services/recorder.js +23 -0
- package/dist/services/retention.d.ts +12 -0
- package/dist/services/retention.d.ts.map +1 -0
- package/dist/services/retention.js +36 -0
- package/dist/services/runtime.d.ts +3 -0
- package/dist/services/runtime.d.ts.map +1 -0
- package/dist/services/runtime.js +9 -0
- package/dist/types.d.ts +133 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/dist/ui/app.js +382 -0
- package/dist/ui/index.html +610 -0
- package/dist/utils/masking.d.ts +7 -0
- package/dist/utils/masking.d.ts.map +1 -0
- package/dist/utils/masking.js +79 -0
- package/package.json +71 -0
package/README.md
ADDED
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
# spider-watch
|
|
2
|
+
|
|
3
|
+
`spider-watch` is a reusable monitoring toolkit for Node.js Express apps, designed for **API-only backends** (no server-rendered views required).
|
|
4
|
+
|
|
5
|
+
It comes from a common production need: monitor incoming API requests, exceptions, outbound HTTP calls, and operational events in one place, with a built-in dashboard UI (labeled **Spider Watch**) and JSON APIs.
|
|
6
|
+
|
|
7
|
+
## What it provides
|
|
8
|
+
|
|
9
|
+
- Request capture (`REQUEST` events): method, path, status, duration, IP, headers/body (optional), response headers/body (optional)
|
|
10
|
+
- Exception capture (`EXCEPTION` events) through Express error middleware
|
|
11
|
+
- Outbound HTTP capture (`EXTERNAL_HTTP` events) via a monitored Axios instance
|
|
12
|
+
- Optional console capture (`LOG` events)
|
|
13
|
+
- Manual domain instrumentation (`DB_QUERY`, `JOB`, `NOTIFICATION`, `MAIL`, `CACHE_OP`)
|
|
14
|
+
- SQLite persistence with retention cleanup
|
|
15
|
+
- Built-in dashboard + API endpoints under a configurable route prefix
|
|
16
|
+
- Basic Auth protection for the monitoring surface
|
|
17
|
+
- Sensitive data masking for headers and payloads
|
|
18
|
+
|
|
19
|
+
## Requirements
|
|
20
|
+
|
|
21
|
+
- Node.js `>=18`
|
|
22
|
+
- Express `^4.18.0 || ^5.0.0` (peer dependency)
|
|
23
|
+
|
|
24
|
+
## Installation
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
pnpm add express-monitoring
|
|
28
|
+
# or: npm install express-monitoring
|
|
29
|
+
# or: yarn add express-monitoring
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Quick start (JavaScript)
|
|
33
|
+
|
|
34
|
+
```js
|
|
35
|
+
import express from "express";
|
|
36
|
+
import { createMonitoring, loadMonitoringConfigFromEnv } from "express-monitoring";
|
|
37
|
+
|
|
38
|
+
const app = express();
|
|
39
|
+
|
|
40
|
+
const monitoring = createMonitoring(loadMonitoringConfigFromEnv(process.env));
|
|
41
|
+
|
|
42
|
+
app.use(express.json());
|
|
43
|
+
|
|
44
|
+
// Required order:
|
|
45
|
+
app.use(monitoring.captureMiddleware);
|
|
46
|
+
app.use(monitoring.router);
|
|
47
|
+
app.use(monitoring.errorMiddleware);
|
|
48
|
+
|
|
49
|
+
monitoring.start();
|
|
50
|
+
|
|
51
|
+
const server = app.listen(3000, () => {
|
|
52
|
+
console.log("API listening on :3000");
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
process.on("SIGTERM", () => {
|
|
56
|
+
monitoring.stop();
|
|
57
|
+
server.close(() => process.exit(0));
|
|
58
|
+
});
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Core integration rules
|
|
62
|
+
|
|
63
|
+
1. Use `captureMiddleware` before your monitoring router and error middleware.
|
|
64
|
+
2. Keep `errorMiddleware` after your application routes/middlewares so thrown errors are captured.
|
|
65
|
+
3. Call `monitoring.start()` during bootstrap and `monitoring.stop()` during shutdown.
|
|
66
|
+
4. If monitoring is enabled and auth is enabled, credentials are required.
|
|
67
|
+
|
|
68
|
+
## Configuration
|
|
69
|
+
|
|
70
|
+
You can configure through environment variables or pass options directly.
|
|
71
|
+
|
|
72
|
+
### Defaults
|
|
73
|
+
|
|
74
|
+
```js
|
|
75
|
+
{
|
|
76
|
+
enabled: false,
|
|
77
|
+
routePrefix: "/monitoring",
|
|
78
|
+
dbPath: "./data/monitoring.sqlite",
|
|
79
|
+
retentionDays: 30,
|
|
80
|
+
captureLogs: false,
|
|
81
|
+
captureBodies: true,
|
|
82
|
+
sampleRate: 1,
|
|
83
|
+
auth: {
|
|
84
|
+
type: "basic",
|
|
85
|
+
user: "",
|
|
86
|
+
pass: "",
|
|
87
|
+
realm: "Monitoring",
|
|
88
|
+
enabled: true
|
|
89
|
+
},
|
|
90
|
+
masking: {
|
|
91
|
+
enabled: true,
|
|
92
|
+
sensitiveKeys: [
|
|
93
|
+
"password", "token", "authorization", "api_key", "access_token", "refresh_token",
|
|
94
|
+
"credentials", "cvv", "card_number", "ssn", "private_key"
|
|
95
|
+
],
|
|
96
|
+
maxBodySize: 8192
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Programmatic config example
|
|
102
|
+
|
|
103
|
+
```js
|
|
104
|
+
import { createMonitoring } from "express-monitoring";
|
|
105
|
+
|
|
106
|
+
const monitoring = createMonitoring({
|
|
107
|
+
enabled: true,
|
|
108
|
+
routePrefix: "/ops/monitoring",
|
|
109
|
+
dbPath: "./var/monitoring.sqlite",
|
|
110
|
+
retentionDays: 14,
|
|
111
|
+
captureLogs: true,
|
|
112
|
+
captureBodies: true,
|
|
113
|
+
sampleRate: 1,
|
|
114
|
+
auth: {
|
|
115
|
+
type: "basic",
|
|
116
|
+
enabled: true,
|
|
117
|
+
user: "admin",
|
|
118
|
+
pass: "change-me",
|
|
119
|
+
realm: "Spider Watch",
|
|
120
|
+
},
|
|
121
|
+
masking: {
|
|
122
|
+
enabled: true,
|
|
123
|
+
sensitiveKeys: ["authorization", "password", "token", "api_key"],
|
|
124
|
+
maxBodySize: 4096,
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Environment config example
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
MONITORING_ENABLED=true
|
|
133
|
+
MONITORING_ROUTE_PREFIX=/monitoring
|
|
134
|
+
MONITORING_DB_PATH=./data/monitoring.sqlite
|
|
135
|
+
MONITORING_RETENTION_DAYS=30
|
|
136
|
+
MONITORING_CAPTURE_LOGS=true
|
|
137
|
+
MONITORING_CAPTURE_BODIES=true
|
|
138
|
+
MONITORING_SAMPLE_RATE=1
|
|
139
|
+
|
|
140
|
+
MONITORING_AUTH_ENABLED=true
|
|
141
|
+
MONITORING_AUTH_USER=admin
|
|
142
|
+
MONITORING_AUTH_PASS=change-me
|
|
143
|
+
MONITORING_AUTH_REALM="Spider Watch"
|
|
144
|
+
|
|
145
|
+
MONITORING_MASKING_ENABLED=true
|
|
146
|
+
MONITORING_MAX_BODY_SIZE=8192
|
|
147
|
+
MONITORING_SENSITIVE_KEYS=authorization,password,token,api_key
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
```js
|
|
151
|
+
import { createMonitoring, loadMonitoringConfigFromEnv } from "express-monitoring";
|
|
152
|
+
|
|
153
|
+
const monitoring = createMonitoring(loadMonitoringConfigFromEnv(process.env));
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### Validation rules
|
|
157
|
+
|
|
158
|
+
- `sampleRate` must be between `0` and `1`
|
|
159
|
+
- `routePrefix` must start with `/`
|
|
160
|
+
- `retentionDays` and `masking.maxBodySize` must be positive integers
|
|
161
|
+
- If `enabled=true` and `auth.enabled=true`, `auth.user` and `auth.pass` are required
|
|
162
|
+
|
|
163
|
+
## Monitoring surface
|
|
164
|
+
|
|
165
|
+
Assuming `routePrefix=/monitoring`:
|
|
166
|
+
|
|
167
|
+
- `GET /monitoring` → Spider Watch dashboard UI
|
|
168
|
+
- `GET /monitoring/app.js` → dashboard script
|
|
169
|
+
- `GET /monitoring/api/events` → paginated event list with filters
|
|
170
|
+
- `GET /monitoring/api/events/:id` → event detail (request/exceptions/external calls when available)
|
|
171
|
+
- `DELETE /monitoring/api/events` → delete all data (requires JSON body `{"scope":"all"}`)
|
|
172
|
+
- `GET /monitoring/api/requests` → request-only view
|
|
173
|
+
- `GET /monitoring/api/requests/:id` → request detail with linked external calls/exceptions
|
|
174
|
+
|
|
175
|
+
If monitoring is disabled, requests under the route prefix return HTTP `503`.
|
|
176
|
+
|
|
177
|
+
## Event model (high-level)
|
|
178
|
+
|
|
179
|
+
- `REQUEST`: captured by `captureMiddleware`
|
|
180
|
+
- `EXCEPTION`: captured by `errorMiddleware`
|
|
181
|
+
- `EXTERNAL_HTTP`: captured by monitored Axios
|
|
182
|
+
- `LOG`: captured when `captureLogs=true`
|
|
183
|
+
- `SCHEDULED_TASK`: retention cleanup events
|
|
184
|
+
- Instrumentation events: `DB_QUERY`, `JOB`, `NOTIFICATION`, `MAIL`, `CACHE_OP`
|
|
185
|
+
|
|
186
|
+
Events are linked through a correlation ID generated per captured request.
|
|
187
|
+
|
|
188
|
+
## Outbound HTTP monitoring (Axios)
|
|
189
|
+
|
|
190
|
+
Use the monitored client returned by `createMonitoredAxios()`:
|
|
191
|
+
|
|
192
|
+
```js
|
|
193
|
+
const http = monitoring.createMonitoredAxios();
|
|
194
|
+
|
|
195
|
+
await http.get("https://api.example.com/v1/users", {
|
|
196
|
+
headers: { Authorization: "Bearer super-secret-token" },
|
|
197
|
+
});
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
When monitoring is enabled, outbound requests are stored as `EXTERNAL_HTTP` with method, URL, status, duration, masked headers/body, and optional error summary.
|
|
201
|
+
|
|
202
|
+
## Manual instrumentation
|
|
203
|
+
|
|
204
|
+
Use `monitoring.instrumentation` to emit custom operational events:
|
|
205
|
+
|
|
206
|
+
```js
|
|
207
|
+
monitoring.instrumentation.recordDbQuery("SELECT users by tenant", { tenantId: "t-123" });
|
|
208
|
+
monitoring.instrumentation.recordJobRun("daily-report", { tookMs: 482 });
|
|
209
|
+
monitoring.instrumentation.recordNotification("Slack alert sent", { channel: "#ops" });
|
|
210
|
+
monitoring.instrumentation.recordMail("Welcome email", { userId: "u-42" });
|
|
211
|
+
monitoring.instrumentation.recordCacheOp("users:list cache hit", { key: "users:tenant:t-123" });
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
## Security and sensitive data
|
|
215
|
+
|
|
216
|
+
- Protect monitoring routes with Basic Auth in production.
|
|
217
|
+
- Keep credentials out of source code (prefer environment variables/secrets managers).
|
|
218
|
+
- Masking is enabled by default for known sensitive keys.
|
|
219
|
+
- Payload strings are truncated to `masking.maxBodySize`.
|
|
220
|
+
- Binary bodies are stored as `[binary data]`.
|
|
221
|
+
|
|
222
|
+
## Sampling, storage, and retention
|
|
223
|
+
|
|
224
|
+
- `sampleRate` controls request capture probability (`1` = all, `0.1` = ~10%).
|
|
225
|
+
- Data is stored in SQLite at `dbPath`.
|
|
226
|
+
- Retention runs immediately on `start()` and then hourly.
|
|
227
|
+
- Old events are deleted according to `retentionDays`.
|
|
228
|
+
|
|
229
|
+
## Troubleshooting
|
|
230
|
+
|
|
231
|
+
### `auth.user and auth.pass are required when auth is enabled`
|
|
232
|
+
|
|
233
|
+
Set both credentials, or disable auth explicitly:
|
|
234
|
+
|
|
235
|
+
```bash
|
|
236
|
+
MONITORING_AUTH_ENABLED=false
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
### Dashboard returns 503
|
|
240
|
+
|
|
241
|
+
Monitoring is disabled. Set:
|
|
242
|
+
|
|
243
|
+
```bash
|
|
244
|
+
MONITORING_ENABLED=true
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
### No outbound HTTP events
|
|
248
|
+
|
|
249
|
+
Use `monitoring.createMonitoredAxios()` for calls you want tracked.
|
|
250
|
+
|
|
251
|
+
### Missing request payloads
|
|
252
|
+
|
|
253
|
+
Ensure `captureBodies=true`. If disabled, bodies and captured headers are intentionally omitted.
|
|
254
|
+
|
|
255
|
+
## Public API
|
|
256
|
+
|
|
257
|
+
```js
|
|
258
|
+
import { createMonitoring, loadMonitoringConfigFromEnv } from "express-monitoring";
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
- `createMonitoring(options?)` → monitoring instance
|
|
262
|
+
- `loadMonitoringConfigFromEnv(env?)` → normalized config object
|
|
263
|
+
|
|
264
|
+
Returned monitoring instance:
|
|
265
|
+
|
|
266
|
+
- `captureMiddleware`
|
|
267
|
+
- `router`
|
|
268
|
+
- `errorMiddleware`
|
|
269
|
+
- `start()`
|
|
270
|
+
- `stop()`
|
|
271
|
+
- `recordEvent(eventInput)`
|
|
272
|
+
- `instrumentation`
|
|
273
|
+
- `createMonitoredAxios()`
|
|
274
|
+
- `config`
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { MonitoringOptions } from "../types.js";
|
|
2
|
+
export declare const DEFAULT_SENSITIVE_KEYS: readonly ["password", "passwd", "pass", "secret", "token", "authorization", "auth", "apikey", "api_key", "accesstoken", "access_token", "refreshtoken", "refresh_token", "credential", "credentials", "pin", "cvv", "cvc", "cardnumber", "card_number", "ssn", "private_key", "privatekey"];
|
|
3
|
+
export declare const DEFAULT_OPTIONS: MonitoringOptions;
|
|
4
|
+
export declare function normalizeOptions(partial?: Partial<MonitoringOptions>): MonitoringOptions;
|
|
5
|
+
//# sourceMappingURL=defaults.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"defaults.d.ts","sourceRoot":"","sources":["../../src/config/defaults.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAErD,eAAO,MAAM,sBAAsB,6RAwBzB,CAAC;AAEX,eAAO,MAAM,eAAe,EAAE,iBAoB7B,CAAC;AASF,wBAAgB,gBAAgB,CAAC,OAAO,GAAE,OAAO,CAAC,iBAAiB,CAAM,GAAG,iBAAiB,CAkB5F"}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
export const DEFAULT_SENSITIVE_KEYS = [
|
|
2
|
+
"password",
|
|
3
|
+
"passwd",
|
|
4
|
+
"pass",
|
|
5
|
+
"secret",
|
|
6
|
+
"token",
|
|
7
|
+
"authorization",
|
|
8
|
+
"auth",
|
|
9
|
+
"apikey",
|
|
10
|
+
"api_key",
|
|
11
|
+
"accesstoken",
|
|
12
|
+
"access_token",
|
|
13
|
+
"refreshtoken",
|
|
14
|
+
"refresh_token",
|
|
15
|
+
"credential",
|
|
16
|
+
"credentials",
|
|
17
|
+
"pin",
|
|
18
|
+
"cvv",
|
|
19
|
+
"cvc",
|
|
20
|
+
"cardnumber",
|
|
21
|
+
"card_number",
|
|
22
|
+
"ssn",
|
|
23
|
+
"private_key",
|
|
24
|
+
"privatekey",
|
|
25
|
+
];
|
|
26
|
+
export const DEFAULT_OPTIONS = {
|
|
27
|
+
enabled: false,
|
|
28
|
+
routePrefix: "/monitoring",
|
|
29
|
+
dbPath: "./data/monitoring.sqlite",
|
|
30
|
+
retentionDays: 30,
|
|
31
|
+
captureLogs: false,
|
|
32
|
+
captureBodies: true,
|
|
33
|
+
sampleRate: 1,
|
|
34
|
+
auth: {
|
|
35
|
+
type: "basic",
|
|
36
|
+
user: "",
|
|
37
|
+
pass: "",
|
|
38
|
+
realm: "Monitoring",
|
|
39
|
+
enabled: true,
|
|
40
|
+
},
|
|
41
|
+
masking: {
|
|
42
|
+
enabled: true,
|
|
43
|
+
sensitiveKeys: [...DEFAULT_SENSITIVE_KEYS],
|
|
44
|
+
maxBodySize: 8192,
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
function normalizeRoutePrefix(value) {
|
|
48
|
+
const trimmed = value.trim();
|
|
49
|
+
if (!trimmed)
|
|
50
|
+
return "/monitoring";
|
|
51
|
+
const withLeadingSlash = trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
|
|
52
|
+
return withLeadingSlash.replace(/\/+$/, "") || "/monitoring";
|
|
53
|
+
}
|
|
54
|
+
export function normalizeOptions(partial = {}) {
|
|
55
|
+
const auth = {
|
|
56
|
+
...DEFAULT_OPTIONS.auth,
|
|
57
|
+
...(partial.auth ?? {}),
|
|
58
|
+
};
|
|
59
|
+
const masking = {
|
|
60
|
+
...DEFAULT_OPTIONS.masking,
|
|
61
|
+
...(partial.masking ?? {}),
|
|
62
|
+
};
|
|
63
|
+
return {
|
|
64
|
+
...DEFAULT_OPTIONS,
|
|
65
|
+
...partial,
|
|
66
|
+
routePrefix: normalizeRoutePrefix(partial.routePrefix ?? DEFAULT_OPTIONS.routePrefix),
|
|
67
|
+
auth,
|
|
68
|
+
masking,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"env-loader.d.ts","sourceRoot":"","sources":["../../src/config/env-loader.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAErD,wBAAgB,2BAA2B,CACzC,GAAG,GAAE,MAAM,CAAC,UAAwB,GACnC,iBAAiB,CA6BnB"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { normalizeOptions } from "./defaults.js";
|
|
2
|
+
export function loadMonitoringConfigFromEnv(env = process.env) {
|
|
3
|
+
const masking = {
|
|
4
|
+
enabled: env.MONITORING_MASKING_ENABLED !== "false",
|
|
5
|
+
maxBodySize: parseInt(env.MONITORING_MAX_BODY_SIZE ?? "8192", 10),
|
|
6
|
+
};
|
|
7
|
+
if (env.MONITORING_SENSITIVE_KEYS) {
|
|
8
|
+
masking.sensitiveKeys = env.MONITORING_SENSITIVE_KEYS.split(",")
|
|
9
|
+
.map((v) => v.trim())
|
|
10
|
+
.filter(Boolean);
|
|
11
|
+
}
|
|
12
|
+
return normalizeOptions({
|
|
13
|
+
enabled: env.MONITORING_ENABLED === "true",
|
|
14
|
+
routePrefix: env.MONITORING_ROUTE_PREFIX ?? "/monitoring",
|
|
15
|
+
dbPath: env.MONITORING_DB_PATH ?? "./data/monitoring.sqlite",
|
|
16
|
+
retentionDays: parseInt(env.MONITORING_RETENTION_DAYS ?? "30", 10),
|
|
17
|
+
captureLogs: env.MONITORING_CAPTURE_LOGS === "true",
|
|
18
|
+
captureBodies: env.MONITORING_CAPTURE_BODIES !== "false",
|
|
19
|
+
sampleRate: parseFloat(env.MONITORING_SAMPLE_RATE ?? "1"),
|
|
20
|
+
auth: {
|
|
21
|
+
type: "basic",
|
|
22
|
+
user: env.MONITORING_AUTH_USER ?? "",
|
|
23
|
+
pass: env.MONITORING_AUTH_PASS ?? "",
|
|
24
|
+
realm: env.MONITORING_AUTH_REALM ?? "Monitoring",
|
|
25
|
+
enabled: env.MONITORING_AUTH_ENABLED !== "false",
|
|
26
|
+
},
|
|
27
|
+
masking,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../../src/config/validate.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAErD,wBAAgB,eAAe,CAAC,OAAO,EAAE,iBAAiB,GAAG,IAAI,CAsBhE"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export function validateOptions(options) {
|
|
2
|
+
if (!Number.isFinite(options.sampleRate) || options.sampleRate < 0 || options.sampleRate > 1) {
|
|
3
|
+
throw new Error("[monitoring] sampleRate must be a number between 0 and 1");
|
|
4
|
+
}
|
|
5
|
+
if (!options.routePrefix || !options.routePrefix.startsWith("/")) {
|
|
6
|
+
throw new Error("[monitoring] routePrefix must start with '/'");
|
|
7
|
+
}
|
|
8
|
+
if (!Number.isInteger(options.retentionDays) || options.retentionDays <= 0) {
|
|
9
|
+
throw new Error("[monitoring] retentionDays must be a positive integer");
|
|
10
|
+
}
|
|
11
|
+
if (!Number.isInteger(options.masking.maxBodySize) || options.masking.maxBodySize <= 0) {
|
|
12
|
+
throw new Error("[monitoring] masking.maxBodySize must be a positive integer");
|
|
13
|
+
}
|
|
14
|
+
if (options.enabled && options.auth.enabled) {
|
|
15
|
+
if (!options.auth.user || !options.auth.pass) {
|
|
16
|
+
throw new Error("[monitoring] auth.user and auth.pass are required when auth is enabled");
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"create-monitoring.d.ts","sourceRoot":"","sources":["../src/create-monitoring.ts"],"names":[],"mappings":"AAoBA,OAAO,KAAK,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAGxE,wBAAgB,gBAAgB,CAAC,OAAO,GAAE,OAAO,CAAC,iBAAiB,CAAM,GAAG,kBAAkB,CAiE7F"}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { normalizeOptions } from "./config/defaults.js";
|
|
4
|
+
import { validateOptions } from "./config/validate.js";
|
|
5
|
+
import { createBasicAuthMiddleware } from "./middleware/auth-basic.js";
|
|
6
|
+
import { createCaptureMiddleware } from "./middleware/capture.js";
|
|
7
|
+
import { createErrorMiddleware } from "./middleware/error.js";
|
|
8
|
+
import { createMonitoringRepository } from "./repository/monitoring-repository.js";
|
|
9
|
+
import { createSqliteDb } from "./repository/sqlite-db.js";
|
|
10
|
+
import { createMonitoringRouter } from "./router/monitoring-router.js";
|
|
11
|
+
import { createConsoleHookService } from "./services/console-hook.js";
|
|
12
|
+
import { getCorrelationId } from "./services/context.js";
|
|
13
|
+
import { createMonitoredAxios } from "./services/http-client.js";
|
|
14
|
+
import { createInstrumentation } from "./services/instrumentation.js";
|
|
15
|
+
import { createRecorder } from "./services/recorder.js";
|
|
16
|
+
import { createRetentionService } from "./services/retention.js";
|
|
17
|
+
import { createRuntimeState } from "./services/runtime.js";
|
|
18
|
+
import { createMaskingUtils } from "./utils/masking.js";
|
|
19
|
+
export function createMonitoring(options = {}) {
|
|
20
|
+
const config = normalizeOptions(options);
|
|
21
|
+
validateOptions(config);
|
|
22
|
+
const dbAdapter = createSqliteDb(config.dbPath);
|
|
23
|
+
const repository = createMonitoringRepository(dbAdapter);
|
|
24
|
+
const runtime = createRuntimeState();
|
|
25
|
+
const masking = createMaskingUtils(config.masking);
|
|
26
|
+
const authMiddleware = createBasicAuthMiddleware(config);
|
|
27
|
+
const recorder = createRecorder(repository, getCorrelationId);
|
|
28
|
+
const retention = createRetentionService(config, repository, recorder.recordEvent);
|
|
29
|
+
const consoleHook = createConsoleHookService(config, recorder.recordEvent);
|
|
30
|
+
const instrumentation = createInstrumentation(recorder.recordEvent);
|
|
31
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
32
|
+
const uiDir = path.join(__dirname, "ui");
|
|
33
|
+
const captureMiddleware = createCaptureMiddleware(config, repository, masking);
|
|
34
|
+
const errorMiddleware = createErrorMiddleware(config, repository);
|
|
35
|
+
const router = createMonitoringRouter(config, repository, authMiddleware, uiDir);
|
|
36
|
+
function start() {
|
|
37
|
+
if (runtime.status === "running")
|
|
38
|
+
return;
|
|
39
|
+
runtime.status = "running";
|
|
40
|
+
runtime.startedAt = new Date().toISOString();
|
|
41
|
+
if (!config.enabled)
|
|
42
|
+
return;
|
|
43
|
+
dbAdapter.getDb();
|
|
44
|
+
runtime.dbConnected = true;
|
|
45
|
+
consoleHook.install();
|
|
46
|
+
runtime.consoleHookActive = consoleHook.isInstalled();
|
|
47
|
+
retention.start();
|
|
48
|
+
runtime.retentionJobActive = retention.isActive();
|
|
49
|
+
}
|
|
50
|
+
function stop() {
|
|
51
|
+
if (runtime.status === "stopped" || runtime.status === "stopping")
|
|
52
|
+
return;
|
|
53
|
+
runtime.status = "stopping";
|
|
54
|
+
retention.stop();
|
|
55
|
+
runtime.retentionJobActive = false;
|
|
56
|
+
consoleHook.uninstall();
|
|
57
|
+
runtime.consoleHookActive = false;
|
|
58
|
+
dbAdapter.closeDb();
|
|
59
|
+
runtime.dbConnected = false;
|
|
60
|
+
runtime.status = "stopped";
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
captureMiddleware,
|
|
64
|
+
errorMiddleware,
|
|
65
|
+
router,
|
|
66
|
+
start,
|
|
67
|
+
stop,
|
|
68
|
+
recordEvent: recorder.recordEvent,
|
|
69
|
+
instrumentation,
|
|
70
|
+
createMonitoredAxios: () => createMonitoredAxios(config, repository, masking, getCorrelationId),
|
|
71
|
+
config,
|
|
72
|
+
};
|
|
73
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { createMonitoring } from "./create-monitoring.js";
|
|
2
|
+
export { loadMonitoringConfigFromEnv } from "./config/env-loader.js";
|
|
3
|
+
export type { MonitoringAuthOptions, MonitoringEvent, MonitoringEventInput, MonitoringInstance, MonitoringMaskingOptions, MonitoringOptions, MonitoringRuntime, } from "./types.js";
|
|
4
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC1D,OAAO,EAAE,2BAA2B,EAAE,MAAM,wBAAwB,CAAC;AAErE,YAAY,EACV,qBAAqB,EACrB,eAAe,EACf,oBAAoB,EACpB,kBAAkB,EAClB,wBAAwB,EACxB,iBAAiB,EACjB,iBAAiB,GAClB,MAAM,YAAY,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth-basic.d.ts","sourceRoot":"","sources":["../../src/middleware/auth-basic.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAE9C,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAQrD,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,iBAAiB,GAAG,cAAc,CA8BnF"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { createHash, timingSafeEqual } from "node:crypto";
|
|
2
|
+
function safeCompare(a, b) {
|
|
3
|
+
const ha = createHash("sha256").update(a).digest();
|
|
4
|
+
const hb = createHash("sha256").update(b).digest();
|
|
5
|
+
return timingSafeEqual(ha, hb);
|
|
6
|
+
}
|
|
7
|
+
export function createBasicAuthMiddleware(config) {
|
|
8
|
+
return (req, res, next) => {
|
|
9
|
+
if (!config.auth.enabled)
|
|
10
|
+
return next();
|
|
11
|
+
const authHeader = req.headers.authorization ?? "";
|
|
12
|
+
if (!authHeader.startsWith("Basic ")) {
|
|
13
|
+
res.set("WWW-Authenticate", `Basic realm="${config.auth.realm}"`);
|
|
14
|
+
return res.status(401).json({ error: "Authentication required" });
|
|
15
|
+
}
|
|
16
|
+
try {
|
|
17
|
+
const decoded = Buffer.from(authHeader.slice(6), "base64").toString("utf8");
|
|
18
|
+
const colonIndex = decoded.indexOf(":");
|
|
19
|
+
const user = decoded.substring(0, colonIndex);
|
|
20
|
+
const pass = decoded.substring(colonIndex + 1);
|
|
21
|
+
const validUser = safeCompare(user, config.auth.user);
|
|
22
|
+
const validPass = safeCompare(pass, config.auth.pass);
|
|
23
|
+
if (!validUser || !validPass) {
|
|
24
|
+
res.set("WWW-Authenticate", `Basic realm="${config.auth.realm}"`);
|
|
25
|
+
return res.status(401).json({ error: "Invalid credentials" });
|
|
26
|
+
}
|
|
27
|
+
return next();
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
res.set("WWW-Authenticate", `Basic realm="${config.auth.realm}"`);
|
|
31
|
+
return res.status(401).json({ error: "Invalid credentials" });
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { RequestHandler } from "express";
|
|
2
|
+
import type { MonitoringOptions, MonitoringRepository } from "../types.js";
|
|
3
|
+
export declare function createCaptureMiddleware(config: MonitoringOptions, repository: MonitoringRepository, masking: {
|
|
4
|
+
maskBody: (body: unknown) => string | null;
|
|
5
|
+
maskHeaders: (headers: unknown) => Record<string, unknown>;
|
|
6
|
+
}): RequestHandler;
|
|
7
|
+
//# sourceMappingURL=capture.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"capture.d.ts","sourceRoot":"","sources":["../../src/middleware/capture.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAG9C,OAAO,KAAK,EAAE,iBAAiB,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAE3E,wBAAgB,uBAAuB,CACrC,MAAM,EAAE,iBAAiB,EACzB,UAAU,EAAE,oBAAoB,EAChC,OAAO,EAAE;IACP,QAAQ,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,MAAM,GAAG,IAAI,CAAC;IAC3C,WAAW,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC5D,GACA,cAAc,CA+EhB"}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { createContext, runWithContext } from "../services/context.js";
|
|
2
|
+
export function createCaptureMiddleware(config, repository, masking) {
|
|
3
|
+
return (req, res, next) => {
|
|
4
|
+
if (!config.enabled)
|
|
5
|
+
return next();
|
|
6
|
+
if (Math.random() > config.sampleRate)
|
|
7
|
+
return next();
|
|
8
|
+
if (req.path.startsWith(config.routePrefix)) {
|
|
9
|
+
return next();
|
|
10
|
+
}
|
|
11
|
+
const context = createContext();
|
|
12
|
+
const startTime = Date.now();
|
|
13
|
+
const responseChunks = [];
|
|
14
|
+
const originalWrite = res.write.bind(res);
|
|
15
|
+
const originalEnd = res.end.bind(res);
|
|
16
|
+
res.write = ((chunk, ...args) => {
|
|
17
|
+
if (chunk) {
|
|
18
|
+
responseChunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)));
|
|
19
|
+
}
|
|
20
|
+
return originalWrite(chunk, ...args);
|
|
21
|
+
});
|
|
22
|
+
res.end = ((chunk, ...args) => {
|
|
23
|
+
if (chunk) {
|
|
24
|
+
responseChunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)));
|
|
25
|
+
}
|
|
26
|
+
return originalEnd(chunk, ...args);
|
|
27
|
+
});
|
|
28
|
+
runWithContext(context, () => {
|
|
29
|
+
res.on("finish", () => {
|
|
30
|
+
try {
|
|
31
|
+
const durationMs = Date.now() - startTime;
|
|
32
|
+
const rawResponseBody = responseChunks.length
|
|
33
|
+
? Buffer.concat(responseChunks).toString("utf8")
|
|
34
|
+
: null;
|
|
35
|
+
const requestBody = config.captureBodies ? masking.maskBody(req.body) : null;
|
|
36
|
+
const responseBody = config.captureBodies ? masking.maskBody(rawResponseBody) : null;
|
|
37
|
+
const ipAddress = req.headers["x-forwarded-for"]?.toString().split(",")[0]?.trim() ??
|
|
38
|
+
req.socket.remoteAddress ??
|
|
39
|
+
null;
|
|
40
|
+
const summary = `${req.method} ${req.path} ${res.statusCode}`;
|
|
41
|
+
const severity = res.statusCode >= 500 ? "ERROR" : res.statusCode >= 400 ? "WARN" : "INFO";
|
|
42
|
+
const eventId = repository.insertEvent({
|
|
43
|
+
eventType: "REQUEST",
|
|
44
|
+
severity,
|
|
45
|
+
correlationId: context.correlationId,
|
|
46
|
+
summary,
|
|
47
|
+
data: { requestId: context.requestId },
|
|
48
|
+
});
|
|
49
|
+
repository.insertRequest(eventId, {
|
|
50
|
+
method: req.method,
|
|
51
|
+
path: req.path,
|
|
52
|
+
statusCode: res.statusCode,
|
|
53
|
+
durationMs,
|
|
54
|
+
ipAddress,
|
|
55
|
+
requestHeaders: config.captureBodies ? masking.maskHeaders(req.headers) : {},
|
|
56
|
+
requestBody,
|
|
57
|
+
responseHeaders: config.captureBodies ? masking.maskHeaders(res.getHeaders()) : {},
|
|
58
|
+
responseBody,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
catch (err) {
|
|
62
|
+
console.error("[monitoring] Failed to capture request:", err instanceof Error ? err.message : String(err));
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
next();
|
|
66
|
+
});
|
|
67
|
+
};
|
|
68
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { ErrorRequestHandler } from "express";
|
|
2
|
+
import type { MonitoringOptions, MonitoringRepository } from "../types.js";
|
|
3
|
+
export declare function createErrorMiddleware(config: MonitoringOptions, repository: MonitoringRepository): ErrorRequestHandler;
|
|
4
|
+
//# sourceMappingURL=error.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"error.d.ts","sourceRoot":"","sources":["../../src/middleware/error.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,SAAS,CAAC;AAGnD,OAAO,KAAK,EAAE,iBAAiB,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAE3E,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,iBAAiB,EACzB,UAAU,EAAE,oBAAoB,GAC/B,mBAAmB,CA6BrB"}
|