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
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { getCorrelationId } from "../services/context.js";
|
|
2
|
+
export function createErrorMiddleware(config, repository) {
|
|
3
|
+
return (err, req, _res, next) => {
|
|
4
|
+
if (config.enabled) {
|
|
5
|
+
try {
|
|
6
|
+
const correlationId = getCorrelationId();
|
|
7
|
+
const eventId = repository.insertEvent({
|
|
8
|
+
eventType: "EXCEPTION",
|
|
9
|
+
severity: "ERROR",
|
|
10
|
+
correlationId,
|
|
11
|
+
summary: `${err.name || "Error"}: ${err.message || String(err)}`,
|
|
12
|
+
data: { path: req.path, method: req.method },
|
|
13
|
+
});
|
|
14
|
+
repository.insertException(eventId, {
|
|
15
|
+
correlationId,
|
|
16
|
+
name: err.name || "Error",
|
|
17
|
+
message: err.message || String(err),
|
|
18
|
+
stackTrace: err.stack || null,
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
catch (captureErr) {
|
|
22
|
+
console.error("[monitoring] Failed to capture exception:", captureErr instanceof Error ? captureErr.message : String(captureErr));
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
next(err);
|
|
26
|
+
};
|
|
27
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"monitoring-repository.d.ts","sourceRoot":"","sources":["../../src/repository/monitoring-repository.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAMV,oBAAoB,EAErB,MAAM,aAAa,CAAC;AACrB,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AA0EtD,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,eAAe,GAAG,oBAAoB,CA6PzF"}
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
function serialize(value) {
|
|
3
|
+
if (value === null || value === undefined)
|
|
4
|
+
return null;
|
|
5
|
+
if (typeof value === "string")
|
|
6
|
+
return value;
|
|
7
|
+
return JSON.stringify(value);
|
|
8
|
+
}
|
|
9
|
+
function deserialize(value) {
|
|
10
|
+
if (value === null || value === undefined)
|
|
11
|
+
return null;
|
|
12
|
+
try {
|
|
13
|
+
return JSON.parse(value);
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return value;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function rowToEvent(row) {
|
|
20
|
+
if (!row)
|
|
21
|
+
return null;
|
|
22
|
+
return {
|
|
23
|
+
id: String(row.id),
|
|
24
|
+
eventType: String(row.event_type),
|
|
25
|
+
timestamp: String(row.timestamp),
|
|
26
|
+
severity: String(row.severity),
|
|
27
|
+
correlationId: row.correlation_id ?? null,
|
|
28
|
+
parentId: row.parent_id ?? null,
|
|
29
|
+
summary: row.summary ?? null,
|
|
30
|
+
data: deserialize(row.data),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
function rowToRequest(row) {
|
|
34
|
+
if (!row)
|
|
35
|
+
return null;
|
|
36
|
+
return {
|
|
37
|
+
eventId: String(row.event_id),
|
|
38
|
+
method: row.method ?? null,
|
|
39
|
+
path: row.path ?? null,
|
|
40
|
+
statusCode: row.status_code ?? null,
|
|
41
|
+
durationMs: row.duration_ms ?? null,
|
|
42
|
+
ipAddress: row.ip_address ?? null,
|
|
43
|
+
requestHeaders: deserialize(row.request_headers),
|
|
44
|
+
requestBody: row.request_body ?? null,
|
|
45
|
+
responseHeaders: deserialize(row.response_headers),
|
|
46
|
+
responseBody: row.response_body ?? null,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
function rowToExternalHttp(row) {
|
|
50
|
+
if (!row)
|
|
51
|
+
return null;
|
|
52
|
+
return {
|
|
53
|
+
eventId: String(row.event_id),
|
|
54
|
+
correlationId: row.correlation_id ?? null,
|
|
55
|
+
method: row.method ?? null,
|
|
56
|
+
url: row.url ?? null,
|
|
57
|
+
statusCode: row.status_code ?? null,
|
|
58
|
+
durationMs: row.duration_ms ?? null,
|
|
59
|
+
requestHeaders: deserialize(row.request_headers),
|
|
60
|
+
requestBody: row.request_body ?? null,
|
|
61
|
+
responseHeaders: deserialize(row.response_headers),
|
|
62
|
+
responseBody: row.response_body ?? null,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
function rowToException(row) {
|
|
66
|
+
if (!row)
|
|
67
|
+
return null;
|
|
68
|
+
return {
|
|
69
|
+
eventId: String(row.event_id),
|
|
70
|
+
correlationId: row.correlation_id ?? null,
|
|
71
|
+
name: row.name ?? null,
|
|
72
|
+
message: row.message ?? null,
|
|
73
|
+
stackTrace: row.stack_trace ?? null,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
export function createMonitoringRepository(adapter) {
|
|
77
|
+
function insertEvent({ eventType, severity = "INFO", correlationId, parentId, summary, data, }) {
|
|
78
|
+
const db = adapter.getDb();
|
|
79
|
+
const id = randomUUID();
|
|
80
|
+
const timestamp = new Date().toISOString();
|
|
81
|
+
db.prepare(`INSERT INTO monitoring_events (id, event_type, timestamp, severity, correlation_id, parent_id, summary, data)
|
|
82
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`).run(id, eventType, timestamp, severity, correlationId ?? null, parentId ?? null, summary ?? null, serialize(data));
|
|
83
|
+
return id;
|
|
84
|
+
}
|
|
85
|
+
function getEventById(id) {
|
|
86
|
+
const db = adapter.getDb();
|
|
87
|
+
const row = db.prepare("SELECT * FROM monitoring_events WHERE id = ?").get(id);
|
|
88
|
+
return rowToEvent(row);
|
|
89
|
+
}
|
|
90
|
+
function listEvents({ from, to, status, type, method, q, page = 1, pageSize = 50, } = {}) {
|
|
91
|
+
const db = adapter.getDb();
|
|
92
|
+
const conditions = [];
|
|
93
|
+
const params = [];
|
|
94
|
+
if (typeof from === "string" && from) {
|
|
95
|
+
conditions.push("e.timestamp >= ?");
|
|
96
|
+
params.push(from);
|
|
97
|
+
}
|
|
98
|
+
if (typeof to === "string" && to) {
|
|
99
|
+
conditions.push("e.timestamp <= ?");
|
|
100
|
+
params.push(to);
|
|
101
|
+
}
|
|
102
|
+
if (typeof type === "string" && type) {
|
|
103
|
+
conditions.push("e.event_type = ?");
|
|
104
|
+
params.push(type.toUpperCase());
|
|
105
|
+
}
|
|
106
|
+
if (typeof method === "string" && method) {
|
|
107
|
+
conditions.push("r.method = ?");
|
|
108
|
+
params.push(method.toUpperCase());
|
|
109
|
+
}
|
|
110
|
+
if (typeof status === "string" && status) {
|
|
111
|
+
if (status === "success") {
|
|
112
|
+
conditions.push("(r.status_code < 400 OR r.status_code IS NULL)");
|
|
113
|
+
}
|
|
114
|
+
else if (status === "error") {
|
|
115
|
+
conditions.push("(r.status_code >= 400 OR e.severity = 'ERROR')");
|
|
116
|
+
}
|
|
117
|
+
else if (!Number.isNaN(Number(status))) {
|
|
118
|
+
conditions.push("r.status_code = ?");
|
|
119
|
+
params.push(Number(status));
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
if (typeof q === "string" && q) {
|
|
123
|
+
conditions.push("(e.correlation_id LIKE ? OR e.summary LIKE ? OR r.path LIKE ? OR r.ip_address LIKE ?)");
|
|
124
|
+
const like = `%${q}%`;
|
|
125
|
+
params.push(like, like, like, like);
|
|
126
|
+
}
|
|
127
|
+
const where = conditions.length ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
128
|
+
const safePage = Math.max(1, Number(page) || 1);
|
|
129
|
+
const safePageSize = Math.min(200, Math.max(1, Number(pageSize) || 50));
|
|
130
|
+
const offset = (safePage - 1) * safePageSize;
|
|
131
|
+
const countRow = db
|
|
132
|
+
.prepare(`SELECT COUNT(*) as cnt
|
|
133
|
+
FROM monitoring_events e
|
|
134
|
+
LEFT JOIN request_records r ON r.event_id = e.id
|
|
135
|
+
${where}`)
|
|
136
|
+
.get(...params);
|
|
137
|
+
const rows = db
|
|
138
|
+
.prepare(`SELECT e.*, r.method, r.path, r.status_code, r.duration_ms, r.ip_address
|
|
139
|
+
FROM monitoring_events e
|
|
140
|
+
LEFT JOIN request_records r ON r.event_id = e.id
|
|
141
|
+
${where}
|
|
142
|
+
ORDER BY e.timestamp DESC
|
|
143
|
+
LIMIT ? OFFSET ?`)
|
|
144
|
+
.all(...params, safePageSize, offset);
|
|
145
|
+
return {
|
|
146
|
+
items: rows.map((row) => ({
|
|
147
|
+
id: String(row.id),
|
|
148
|
+
eventType: String(row.event_type),
|
|
149
|
+
timestamp: String(row.timestamp),
|
|
150
|
+
severity: String(row.severity),
|
|
151
|
+
correlationId: row.correlation_id ?? null,
|
|
152
|
+
summary: row.summary ?? null,
|
|
153
|
+
method: row.method ?? null,
|
|
154
|
+
path: row.path ?? null,
|
|
155
|
+
statusCode: row.status_code ?? null,
|
|
156
|
+
durationMs: row.duration_ms ?? null,
|
|
157
|
+
ipAddress: row.ip_address ?? null,
|
|
158
|
+
})),
|
|
159
|
+
page: safePage,
|
|
160
|
+
pageSize: safePageSize,
|
|
161
|
+
total: countRow?.cnt ?? 0,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
function insertRequest(eventId, record) {
|
|
165
|
+
const db = adapter.getDb();
|
|
166
|
+
db.prepare(`INSERT INTO request_records (event_id, method, path, status_code, duration_ms, ip_address, request_headers, request_body, response_headers, response_body)
|
|
167
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(eventId, record.method ?? null, record.path ?? null, record.statusCode ?? null, record.durationMs ?? null, record.ipAddress ?? null, serialize(record.requestHeaders), record.requestBody ?? null, serialize(record.responseHeaders), record.responseBody ?? null);
|
|
168
|
+
}
|
|
169
|
+
function getRequestByEventId(eventId) {
|
|
170
|
+
const db = adapter.getDb();
|
|
171
|
+
const row = db.prepare("SELECT * FROM request_records WHERE event_id = ?").get(eventId);
|
|
172
|
+
return rowToRequest(row);
|
|
173
|
+
}
|
|
174
|
+
function insertExternalHttp(eventId, record) {
|
|
175
|
+
const db = adapter.getDb();
|
|
176
|
+
db.prepare(`INSERT INTO external_http_records (event_id, correlation_id, method, url, status_code, duration_ms, request_headers, request_body, response_headers, response_body)
|
|
177
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(eventId, record.correlationId ?? null, record.method ?? null, record.url ?? null, record.statusCode ?? null, record.durationMs ?? null, serialize(record.requestHeaders), record.requestBody ?? null, serialize(record.responseHeaders), record.responseBody ?? null);
|
|
178
|
+
}
|
|
179
|
+
function getExternalHttpsByCorrelationId(correlationId) {
|
|
180
|
+
const db = adapter.getDb();
|
|
181
|
+
const rows = db
|
|
182
|
+
.prepare("SELECT * FROM external_http_records WHERE correlation_id = ? ORDER BY rowid ASC")
|
|
183
|
+
.all(correlationId);
|
|
184
|
+
return rows
|
|
185
|
+
.map((row) => rowToExternalHttp(row))
|
|
186
|
+
.filter((row) => Boolean(row));
|
|
187
|
+
}
|
|
188
|
+
function insertException(eventId, record) {
|
|
189
|
+
const db = adapter.getDb();
|
|
190
|
+
db.prepare(`INSERT INTO exception_records (event_id, correlation_id, name, message, stack_trace)
|
|
191
|
+
VALUES (?, ?, ?, ?, ?)`).run(eventId, record.correlationId ?? null, record.name ?? null, record.message ?? null, record.stackTrace ?? null);
|
|
192
|
+
}
|
|
193
|
+
function getExceptionsByCorrelationId(correlationId) {
|
|
194
|
+
const db = adapter.getDb();
|
|
195
|
+
const rows = db
|
|
196
|
+
.prepare("SELECT * FROM exception_records WHERE correlation_id = ? ORDER BY rowid ASC")
|
|
197
|
+
.all(correlationId);
|
|
198
|
+
return rows
|
|
199
|
+
.map((row) => rowToException(row))
|
|
200
|
+
.filter((row) => Boolean(row));
|
|
201
|
+
}
|
|
202
|
+
function deleteOldEvents(olderThanDays) {
|
|
203
|
+
const db = adapter.getDb();
|
|
204
|
+
const cutoff = new Date(Date.now() - olderThanDays * 24 * 60 * 60 * 1000).toISOString();
|
|
205
|
+
const result = db.prepare("DELETE FROM monitoring_events WHERE timestamp < ?").run(cutoff);
|
|
206
|
+
return result.changes;
|
|
207
|
+
}
|
|
208
|
+
function countEvents() {
|
|
209
|
+
const db = adapter.getDb();
|
|
210
|
+
const row = db.prepare("SELECT COUNT(*) as cnt FROM monitoring_events").get();
|
|
211
|
+
return row?.cnt ?? 0;
|
|
212
|
+
}
|
|
213
|
+
function deleteAllEvents() {
|
|
214
|
+
const db = adapter.getDb();
|
|
215
|
+
const tx = db.transaction(() => {
|
|
216
|
+
const deleteResult = db.prepare("DELETE FROM monitoring_events").run();
|
|
217
|
+
const remainingRow = db.prepare("SELECT COUNT(*) as cnt FROM monitoring_events").get();
|
|
218
|
+
return {
|
|
219
|
+
deleted: deleteResult.changes ?? 0,
|
|
220
|
+
remaining: remainingRow?.cnt ?? 0,
|
|
221
|
+
};
|
|
222
|
+
});
|
|
223
|
+
return tx();
|
|
224
|
+
}
|
|
225
|
+
return {
|
|
226
|
+
insertEvent,
|
|
227
|
+
getEventById,
|
|
228
|
+
listEvents,
|
|
229
|
+
insertRequest,
|
|
230
|
+
getRequestByEventId,
|
|
231
|
+
insertExternalHttp,
|
|
232
|
+
getExternalHttpsByCorrelationId,
|
|
233
|
+
insertException,
|
|
234
|
+
getExceptionsByCorrelationId,
|
|
235
|
+
deleteOldEvents,
|
|
236
|
+
countEvents,
|
|
237
|
+
deleteAllEvents,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sqlite-db.d.ts","sourceRoot":"","sources":["../../src/repository/sqlite-db.ts"],"names":[],"mappings":"AAGA,OAAO,QAAQ,MAAM,gBAAgB,CAAC;AAEtC,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,MAAM,QAAQ,CAAC,QAAQ,CAAC;IAC/B,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB;AAED,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,eAAe,CA2B9D"}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import Database from "better-sqlite3";
|
|
4
|
+
export function createSqliteDb(dbPath) {
|
|
5
|
+
let db = null;
|
|
6
|
+
function getDb() {
|
|
7
|
+
if (!db) {
|
|
8
|
+
const resolvedPath = path.resolve(dbPath);
|
|
9
|
+
const dir = path.dirname(resolvedPath);
|
|
10
|
+
if (!fs.existsSync(dir)) {
|
|
11
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
12
|
+
}
|
|
13
|
+
db = new Database(resolvedPath);
|
|
14
|
+
db.pragma("journal_mode = WAL");
|
|
15
|
+
db.pragma("foreign_keys = ON");
|
|
16
|
+
initSchema(db);
|
|
17
|
+
}
|
|
18
|
+
return db;
|
|
19
|
+
}
|
|
20
|
+
function closeDb() {
|
|
21
|
+
if (db) {
|
|
22
|
+
db.close();
|
|
23
|
+
db = null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return { getDb, closeDb };
|
|
27
|
+
}
|
|
28
|
+
function initSchema(db) {
|
|
29
|
+
db.exec(`
|
|
30
|
+
CREATE TABLE IF NOT EXISTS monitoring_events (
|
|
31
|
+
id TEXT PRIMARY KEY,
|
|
32
|
+
event_type TEXT NOT NULL,
|
|
33
|
+
timestamp TEXT NOT NULL,
|
|
34
|
+
severity TEXT NOT NULL DEFAULT 'INFO',
|
|
35
|
+
correlation_id TEXT,
|
|
36
|
+
parent_id TEXT,
|
|
37
|
+
summary TEXT,
|
|
38
|
+
data TEXT
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
CREATE INDEX IF NOT EXISTS idx_events_timestamp ON monitoring_events(timestamp);
|
|
42
|
+
CREATE INDEX IF NOT EXISTS idx_events_correlation ON monitoring_events(correlation_id);
|
|
43
|
+
CREATE INDEX IF NOT EXISTS idx_events_type ON monitoring_events(event_type);
|
|
44
|
+
CREATE INDEX IF NOT EXISTS idx_events_severity ON monitoring_events(severity);
|
|
45
|
+
|
|
46
|
+
CREATE TABLE IF NOT EXISTS request_records (
|
|
47
|
+
event_id TEXT PRIMARY KEY,
|
|
48
|
+
method TEXT,
|
|
49
|
+
path TEXT,
|
|
50
|
+
status_code INTEGER,
|
|
51
|
+
duration_ms INTEGER,
|
|
52
|
+
ip_address TEXT,
|
|
53
|
+
request_headers TEXT,
|
|
54
|
+
request_body TEXT,
|
|
55
|
+
response_headers TEXT,
|
|
56
|
+
response_body TEXT,
|
|
57
|
+
FOREIGN KEY (event_id) REFERENCES monitoring_events(id) ON DELETE CASCADE
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
CREATE INDEX IF NOT EXISTS idx_req_path ON request_records(path);
|
|
61
|
+
CREATE INDEX IF NOT EXISTS idx_req_status ON request_records(status_code);
|
|
62
|
+
CREATE INDEX IF NOT EXISTS idx_req_method ON request_records(method);
|
|
63
|
+
|
|
64
|
+
CREATE TABLE IF NOT EXISTS external_http_records (
|
|
65
|
+
event_id TEXT PRIMARY KEY,
|
|
66
|
+
correlation_id TEXT,
|
|
67
|
+
method TEXT,
|
|
68
|
+
url TEXT,
|
|
69
|
+
status_code INTEGER,
|
|
70
|
+
duration_ms INTEGER,
|
|
71
|
+
request_headers TEXT,
|
|
72
|
+
request_body TEXT,
|
|
73
|
+
response_headers TEXT,
|
|
74
|
+
response_body TEXT,
|
|
75
|
+
FOREIGN KEY (event_id) REFERENCES monitoring_events(id) ON DELETE CASCADE
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
CREATE INDEX IF NOT EXISTS idx_ext_correlation ON external_http_records(correlation_id);
|
|
79
|
+
|
|
80
|
+
CREATE TABLE IF NOT EXISTS exception_records (
|
|
81
|
+
event_id TEXT PRIMARY KEY,
|
|
82
|
+
correlation_id TEXT,
|
|
83
|
+
name TEXT,
|
|
84
|
+
message TEXT,
|
|
85
|
+
stack_trace TEXT,
|
|
86
|
+
FOREIGN KEY (event_id) REFERENCES monitoring_events(id) ON DELETE CASCADE
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
CREATE INDEX IF NOT EXISTS idx_exc_correlation ON exception_records(correlation_id);
|
|
90
|
+
`);
|
|
91
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"async-handler.d.ts","sourceRoot":"","sources":["../../src/router/async-handler.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,OAAO,EAAE,cAAc,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAE/E,wBAAgB,YAAY,CAC1B,OAAO,EAAE,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,EAAE,IAAI,EAAE,YAAY,KAAK,OAAO,CAAC,OAAO,CAAC,GAAG,OAAO,GACvF,cAAc,CAIhB"}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { type RequestHandler, type Router } from "express";
|
|
2
|
+
import type { MonitoringOptions, MonitoringRepository } from "../types.js";
|
|
3
|
+
export declare function createMonitoringRouter(config: MonitoringOptions, repository: MonitoringRepository, authMiddleware: RequestHandler, uiDir: string): Router;
|
|
4
|
+
//# sourceMappingURL=monitoring-router.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"monitoring-router.d.ts","sourceRoot":"","sources":["../../src/router/monitoring-router.ts"],"names":[],"mappings":"AAGA,OAAgB,EAAE,KAAK,cAAc,EAAE,KAAK,MAAM,EAAE,MAAM,SAAS,CAAC;AAGpE,OAAO,KAAK,EAAE,iBAAiB,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAc3E,wBAAgB,sBAAsB,CACpC,MAAM,EAAE,iBAAiB,EACzB,UAAU,EAAE,oBAAoB,EAChC,cAAc,EAAE,cAAc,EAC9B,KAAK,EAAE,MAAM,GACZ,MAAM,CAoIR"}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import express from "express";
|
|
4
|
+
import { asyncHandler } from "./async-handler.js";
|
|
5
|
+
function parseDeleteScope(body) {
|
|
6
|
+
if (!body || typeof body !== "object") {
|
|
7
|
+
return { valid: false, error: "Request body is required" };
|
|
8
|
+
}
|
|
9
|
+
if (body.scope !== "all") {
|
|
10
|
+
return { valid: false, error: 'Invalid scope. Expected {"scope":"all"}' };
|
|
11
|
+
}
|
|
12
|
+
return { valid: true };
|
|
13
|
+
}
|
|
14
|
+
export function createMonitoringRouter(config, repository, authMiddleware, uiDir) {
|
|
15
|
+
const router = express.Router();
|
|
16
|
+
const prefix = config.routePrefix;
|
|
17
|
+
if (!config.enabled) {
|
|
18
|
+
router.use(prefix, (_req, res) => {
|
|
19
|
+
res.status(503).json({ error: "Monitoring is disabled" });
|
|
20
|
+
});
|
|
21
|
+
return router;
|
|
22
|
+
}
|
|
23
|
+
router.use(prefix, authMiddleware);
|
|
24
|
+
router.get(prefix, asyncHandler(async (_req, res) => {
|
|
25
|
+
const indexPath = path.join(uiDir, "index.html");
|
|
26
|
+
const html = await fs.promises.readFile(indexPath, "utf8");
|
|
27
|
+
const withPrefix = html.replaceAll("__MONITORING_BASE_PATH__", prefix);
|
|
28
|
+
res.type("html").send(withPrefix);
|
|
29
|
+
}));
|
|
30
|
+
router.get(`${prefix}/app.js`, asyncHandler(async (_req, res) => {
|
|
31
|
+
const js = await fs.promises.readFile(path.join(uiDir, "app.js"), "utf8");
|
|
32
|
+
res.type("js").send(js);
|
|
33
|
+
}));
|
|
34
|
+
router.get(`${prefix}/api/events`, asyncHandler(async (req, res) => {
|
|
35
|
+
const { from, to, status, type, method, q } = req.query;
|
|
36
|
+
const page = Math.max(1, parseInt(String(req.query.page ?? "1"), 10));
|
|
37
|
+
const pageSize = Math.min(200, Math.max(1, parseInt(String(req.query.pageSize ?? "50"), 10)));
|
|
38
|
+
const result = repository.listEvents({ from, to, status, type, method, q, page, pageSize });
|
|
39
|
+
res.json(result);
|
|
40
|
+
}));
|
|
41
|
+
router.get(`${prefix}/api/events/:id`, asyncHandler(async (req, res) => {
|
|
42
|
+
const eventId = String(req.params.id ?? "");
|
|
43
|
+
const event = repository.getEventById(eventId);
|
|
44
|
+
if (!event) {
|
|
45
|
+
res.status(404).json({ error: "Event not found" });
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const response = { event };
|
|
49
|
+
if (event.eventType === "REQUEST") {
|
|
50
|
+
response.request = repository.getRequestByEventId(event.id);
|
|
51
|
+
if (event.correlationId) {
|
|
52
|
+
response.externalHttpCalls = repository.getExternalHttpsByCorrelationId(event.correlationId);
|
|
53
|
+
response.exceptions = repository.getExceptionsByCorrelationId(event.correlationId);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
else if (event.eventType === "EXCEPTION") {
|
|
57
|
+
const exceptions = repository.getExceptionsByCorrelationId(event.correlationId || event.id);
|
|
58
|
+
response.exception = exceptions[0] ?? null;
|
|
59
|
+
}
|
|
60
|
+
else if (event.eventType === "EXTERNAL_HTTP") {
|
|
61
|
+
const rows = repository.getExternalHttpsByCorrelationId(event.correlationId || event.id);
|
|
62
|
+
response.externalHttp = rows.find((row) => row.eventId === event.id) ?? null;
|
|
63
|
+
}
|
|
64
|
+
res.json(response);
|
|
65
|
+
}));
|
|
66
|
+
router.delete(`${prefix}/api/events`, asyncHandler(async (req, res) => {
|
|
67
|
+
const validation = parseDeleteScope(req.body);
|
|
68
|
+
if (!validation.valid) {
|
|
69
|
+
res.status(400).json({ error: validation.error });
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const { deleted, remaining } = repository.deleteAllEvents();
|
|
73
|
+
const message = deleted === 0 ? "No monitoring data to delete." : `Deleted ${deleted} monitoring event(s).`;
|
|
74
|
+
res.json({ success: true, deleted, remaining, message });
|
|
75
|
+
}));
|
|
76
|
+
router.get(`${prefix}/api/requests`, asyncHandler(async (req, res) => {
|
|
77
|
+
const { from, to, status, method, q } = req.query;
|
|
78
|
+
const page = Math.max(1, parseInt(String(req.query.page ?? "1"), 10));
|
|
79
|
+
const pageSize = Math.min(200, Math.max(1, parseInt(String(req.query.pageSize ?? "50"), 10)));
|
|
80
|
+
const result = repository.listEvents({
|
|
81
|
+
from,
|
|
82
|
+
to,
|
|
83
|
+
status,
|
|
84
|
+
type: "REQUEST",
|
|
85
|
+
method,
|
|
86
|
+
q,
|
|
87
|
+
page,
|
|
88
|
+
pageSize,
|
|
89
|
+
});
|
|
90
|
+
res.json(result);
|
|
91
|
+
}));
|
|
92
|
+
router.get(`${prefix}/api/requests/:id`, asyncHandler(async (req, res) => {
|
|
93
|
+
const eventId = String(req.params.id ?? "");
|
|
94
|
+
const event = repository.getEventById(eventId);
|
|
95
|
+
if (!event || event.eventType !== "REQUEST") {
|
|
96
|
+
res.status(404).json({ error: "Request not found" });
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
const request = repository.getRequestByEventId(event.id);
|
|
100
|
+
const externalHttpCalls = event.correlationId
|
|
101
|
+
? repository.getExternalHttpsByCorrelationId(event.correlationId)
|
|
102
|
+
: [];
|
|
103
|
+
const exceptions = event.correlationId
|
|
104
|
+
? repository.getExceptionsByCorrelationId(event.correlationId)
|
|
105
|
+
: [];
|
|
106
|
+
res.json({ request: { ...event, ...request }, externalHttpCalls, exceptions });
|
|
107
|
+
}));
|
|
108
|
+
return router;
|
|
109
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { MonitoringOptions } from "../types.js";
|
|
2
|
+
export declare function createConsoleHookService(config: MonitoringOptions, recordEvent: (event: {
|
|
3
|
+
eventType: string;
|
|
4
|
+
severity?: "INFO" | "WARN" | "ERROR";
|
|
5
|
+
summary: string;
|
|
6
|
+
data?: unknown;
|
|
7
|
+
}) => string | null): {
|
|
8
|
+
install: () => void;
|
|
9
|
+
uninstall: () => void;
|
|
10
|
+
isInstalled: () => boolean;
|
|
11
|
+
};
|
|
12
|
+
//# sourceMappingURL=console-hook.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"console-hook.d.ts","sourceRoot":"","sources":["../../src/services/console-hook.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAIrD,wBAAgB,wBAAwB,CACtC,MAAM,EAAE,iBAAiB,EACzB,WAAW,EAAE,CAAC,KAAK,EAAE;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC;IACrC,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB,KAAK,MAAM,GAAG,IAAI;mBAMC,IAAI;qBA0CF,IAAI;uBAcF,OAAO;EAKhC"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
export function createConsoleHookService(config, recordEvent) {
|
|
2
|
+
let installed = false;
|
|
3
|
+
const levels = ["log", "info", "warn", "error", "debug"];
|
|
4
|
+
const originals = new Map();
|
|
5
|
+
function install() {
|
|
6
|
+
if (installed || !config.captureLogs)
|
|
7
|
+
return;
|
|
8
|
+
installed = true;
|
|
9
|
+
for (const level of levels) {
|
|
10
|
+
const original = console[level].bind(console);
|
|
11
|
+
originals.set(level, original);
|
|
12
|
+
console[level] = (...args) => {
|
|
13
|
+
original(...args);
|
|
14
|
+
try {
|
|
15
|
+
const severity = level === "error" ? "ERROR" : level === "warn" ? "WARN" : "INFO";
|
|
16
|
+
const summary = args
|
|
17
|
+
.map((value) => {
|
|
18
|
+
if (typeof value === "string")
|
|
19
|
+
return value;
|
|
20
|
+
try {
|
|
21
|
+
return JSON.stringify(value);
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return String(value);
|
|
25
|
+
}
|
|
26
|
+
})
|
|
27
|
+
.join(" ")
|
|
28
|
+
.substring(0, 500);
|
|
29
|
+
recordEvent({
|
|
30
|
+
eventType: "LOG",
|
|
31
|
+
severity,
|
|
32
|
+
summary,
|
|
33
|
+
data: {
|
|
34
|
+
level,
|
|
35
|
+
args: args.map((value) => String(value)),
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
catch (err) {
|
|
40
|
+
console.error("[monitoring] Failed to capture console log:", err instanceof Error ? err.message : String(err));
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function uninstall() {
|
|
46
|
+
if (!installed)
|
|
47
|
+
return;
|
|
48
|
+
for (const level of levels) {
|
|
49
|
+
const original = originals.get(level);
|
|
50
|
+
if (original) {
|
|
51
|
+
console[level] = original;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
originals.clear();
|
|
55
|
+
installed = false;
|
|
56
|
+
}
|
|
57
|
+
function isInstalled() {
|
|
58
|
+
return installed;
|
|
59
|
+
}
|
|
60
|
+
return { install, uninstall, isInstalled };
|
|
61
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { MonitoringContext } from "../types.js";
|
|
2
|
+
export declare function runWithContext(context: MonitoringContext, fn: () => void): void;
|
|
3
|
+
export declare function getContext(): MonitoringContext | null;
|
|
4
|
+
export declare function getCorrelationId(): string | null;
|
|
5
|
+
export declare function getRequestId(): string | null;
|
|
6
|
+
export declare function createContext(overrides?: Partial<MonitoringContext>): MonitoringContext;
|
|
7
|
+
//# sourceMappingURL=context.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../../src/services/context.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAIrD,wBAAgB,cAAc,CAAC,OAAO,EAAE,iBAAiB,EAAE,EAAE,EAAE,MAAM,IAAI,GAAG,IAAI,CAE/E;AAED,wBAAgB,UAAU,IAAI,iBAAiB,GAAG,IAAI,CAErD;AAED,wBAAgB,gBAAgB,IAAI,MAAM,GAAG,IAAI,CAEhD;AAED,wBAAgB,YAAY,IAAI,MAAM,GAAG,IAAI,CAE5C;AAED,wBAAgB,aAAa,CAAC,SAAS,GAAE,OAAO,CAAC,iBAAiB,CAAM,GAAG,iBAAiB,CAM3F"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
const storage = new AsyncLocalStorage();
|
|
4
|
+
export function runWithContext(context, fn) {
|
|
5
|
+
storage.run(context, fn);
|
|
6
|
+
}
|
|
7
|
+
export function getContext() {
|
|
8
|
+
return storage.getStore() ?? null;
|
|
9
|
+
}
|
|
10
|
+
export function getCorrelationId() {
|
|
11
|
+
return storage.getStore()?.correlationId ?? null;
|
|
12
|
+
}
|
|
13
|
+
export function getRequestId() {
|
|
14
|
+
return storage.getStore()?.requestId ?? null;
|
|
15
|
+
}
|
|
16
|
+
export function createContext(overrides = {}) {
|
|
17
|
+
return {
|
|
18
|
+
correlationId: randomUUID(),
|
|
19
|
+
requestId: randomUUID(),
|
|
20
|
+
...overrides,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type AxiosInstance } from "axios";
|
|
2
|
+
import type { MonitoringOptions, MonitoringRepository } from "../types.js";
|
|
3
|
+
export declare function createMonitoredAxios(config: MonitoringOptions, repository: MonitoringRepository, masking: {
|
|
4
|
+
maskBody: (body: unknown) => string | null;
|
|
5
|
+
maskHeaders: (headers: unknown) => Record<string, unknown>;
|
|
6
|
+
}, getCorrelationId: () => string | null): AxiosInstance;
|
|
7
|
+
//# sourceMappingURL=http-client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"http-client.d.ts","sourceRoot":"","sources":["../../src/services/http-client.ts"],"names":[],"mappings":"AAAA,OAAc,EACZ,KAAK,aAAa,EAGnB,MAAM,OAAO,CAAC;AAEf,OAAO,KAAK,EAAE,iBAAiB,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAO3E,wBAAgB,oBAAoB,CAClC,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,EACD,gBAAgB,EAAE,MAAM,MAAM,GAAG,IAAI,GACpC,aAAa,CA0Cf"}
|