@gravito/spectrum 3.0.0 → 3.0.2
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 +100 -48
- package/README.zh-TW.md +163 -0
- package/dist/index.cjs +320 -20
- package/dist/index.d.cts +343 -2
- package/dist/index.d.ts +343 -2
- package/dist/index.js +320 -20
- package/package.json +12 -10
package/dist/index.js
CHANGED
|
@@ -1,46 +1,113 @@
|
|
|
1
|
+
// src/SpectrumOrbit.ts
|
|
2
|
+
import { getCsrfToken } from "@gravito/core";
|
|
3
|
+
|
|
1
4
|
// src/storage/MemoryStorage.ts
|
|
2
5
|
var MemoryStorage = class {
|
|
3
6
|
requests = [];
|
|
4
7
|
logs = [];
|
|
5
8
|
queries = [];
|
|
6
9
|
maxItems = 1e3;
|
|
10
|
+
/**
|
|
11
|
+
* Initializes the memory storage.
|
|
12
|
+
*
|
|
13
|
+
* As memory storage does not require external resources, this is a no-op.
|
|
14
|
+
*
|
|
15
|
+
* @returns Resolves immediately
|
|
16
|
+
*/
|
|
7
17
|
async init() {
|
|
8
18
|
}
|
|
19
|
+
/**
|
|
20
|
+
* Temporarily stores a captured HTTP request in memory.
|
|
21
|
+
*
|
|
22
|
+
* @param req - The request snapshot to store
|
|
23
|
+
*/
|
|
9
24
|
async storeRequest(req) {
|
|
10
25
|
this.requests.unshift(req);
|
|
11
26
|
this.trim(this.requests);
|
|
12
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* Temporarily stores a captured log entry in memory.
|
|
30
|
+
*
|
|
31
|
+
* @param log - The log entry to store
|
|
32
|
+
*/
|
|
13
33
|
async storeLog(log) {
|
|
14
34
|
this.logs.unshift(log);
|
|
15
35
|
this.trim(this.logs);
|
|
16
36
|
}
|
|
37
|
+
/**
|
|
38
|
+
* Temporarily stores a captured database query in memory.
|
|
39
|
+
*
|
|
40
|
+
* @param query - The query information to store
|
|
41
|
+
*/
|
|
17
42
|
async storeQuery(query) {
|
|
18
43
|
this.queries.unshift(query);
|
|
19
44
|
this.trim(this.queries);
|
|
20
45
|
}
|
|
46
|
+
/**
|
|
47
|
+
* Retrieves a list of recent HTTP requests from memory.
|
|
48
|
+
*
|
|
49
|
+
* @param limit - Maximum number of requests to return
|
|
50
|
+
* @param offset - Number of requests to skip for pagination
|
|
51
|
+
* @returns A promise resolving to an array of captured requests
|
|
52
|
+
*/
|
|
21
53
|
async getRequests(limit = 100, offset = 0) {
|
|
22
54
|
return this.requests.slice(offset, offset + limit);
|
|
23
55
|
}
|
|
56
|
+
/**
|
|
57
|
+
* Finds a specific HTTP request by its unique identifier.
|
|
58
|
+
*
|
|
59
|
+
* @param id - The unique ID of the request to find
|
|
60
|
+
* @returns The found request or null if not present
|
|
61
|
+
*/
|
|
24
62
|
async getRequest(id) {
|
|
25
63
|
return this.requests.find((r) => r.id === id) || null;
|
|
26
64
|
}
|
|
65
|
+
/**
|
|
66
|
+
* Retrieves a list of recent application logs from memory.
|
|
67
|
+
*
|
|
68
|
+
* @param limit - Maximum number of logs to return
|
|
69
|
+
* @param offset - Number of logs to skip for pagination
|
|
70
|
+
* @returns A promise resolving to an array of captured logs
|
|
71
|
+
*/
|
|
27
72
|
async getLogs(limit = 100, offset = 0) {
|
|
28
73
|
return this.logs.slice(offset, offset + limit);
|
|
29
74
|
}
|
|
75
|
+
/**
|
|
76
|
+
* Retrieves a list of recent database queries from memory.
|
|
77
|
+
*
|
|
78
|
+
* @param limit - Maximum number of queries to return
|
|
79
|
+
* @param offset - Number of queries to skip for pagination
|
|
80
|
+
* @returns A promise resolving to an array of captured queries
|
|
81
|
+
*/
|
|
30
82
|
async getQueries(limit = 100, offset = 0) {
|
|
31
83
|
return this.queries.slice(offset, offset + limit);
|
|
32
84
|
}
|
|
85
|
+
/**
|
|
86
|
+
* Wipes all captured telemetry data from memory.
|
|
87
|
+
*
|
|
88
|
+
* @returns Resolves when all collections are emptied
|
|
89
|
+
*/
|
|
33
90
|
async clear() {
|
|
34
91
|
this.requests = [];
|
|
35
92
|
this.logs = [];
|
|
36
93
|
this.queries = [];
|
|
37
94
|
}
|
|
95
|
+
/**
|
|
96
|
+
* Sets a new capacity limit and prunes existing data to fit.
|
|
97
|
+
*
|
|
98
|
+
* @param maxItems - The new maximum number of items to retain per category
|
|
99
|
+
*/
|
|
38
100
|
async prune(maxItems) {
|
|
39
101
|
this.maxItems = maxItems;
|
|
40
102
|
this.trim(this.requests);
|
|
41
103
|
this.trim(this.logs);
|
|
42
104
|
this.trim(this.queries);
|
|
43
105
|
}
|
|
106
|
+
/**
|
|
107
|
+
* Truncates an array to ensure it does not exceed the maximum allowed capacity.
|
|
108
|
+
*
|
|
109
|
+
* @param arr - The target array to truncate
|
|
110
|
+
*/
|
|
44
111
|
trim(arr) {
|
|
45
112
|
if (arr.length > this.maxItems) {
|
|
46
113
|
arr.splice(this.maxItems);
|
|
@@ -50,13 +117,30 @@ var MemoryStorage = class {
|
|
|
50
117
|
|
|
51
118
|
// src/SpectrumOrbit.ts
|
|
52
119
|
var SpectrumOrbit = class _SpectrumOrbit {
|
|
120
|
+
/**
|
|
121
|
+
* Global instance of SpectrumOrbit.
|
|
122
|
+
*
|
|
123
|
+
* Used for internal access and singleton patterns within the framework.
|
|
124
|
+
*/
|
|
53
125
|
static instance;
|
|
54
126
|
name = "spectrum";
|
|
55
127
|
config;
|
|
56
128
|
// Event listeners for SSE
|
|
57
129
|
listeners = /* @__PURE__ */ new Set();
|
|
130
|
+
currentRequestId = null;
|
|
58
131
|
warnedSecurity = false;
|
|
59
132
|
deps;
|
|
133
|
+
/**
|
|
134
|
+
* Initializes a new instance of SpectrumOrbit.
|
|
135
|
+
*
|
|
136
|
+
* @param config - Configuration options for behavior and storage
|
|
137
|
+
* @param deps - Internal dependencies for integration with other orbits
|
|
138
|
+
*
|
|
139
|
+
* @example
|
|
140
|
+
* ```typescript
|
|
141
|
+
* const spectrum = new SpectrumOrbit({ path: '/debug' });
|
|
142
|
+
* ```
|
|
143
|
+
*/
|
|
60
144
|
constructor(config = {}, deps = {}) {
|
|
61
145
|
this.config = {
|
|
62
146
|
path: config.path || "/gravito/spectrum",
|
|
@@ -69,29 +153,60 @@ var SpectrumOrbit = class _SpectrumOrbit {
|
|
|
69
153
|
this.deps = deps;
|
|
70
154
|
_SpectrumOrbit.instance = this;
|
|
71
155
|
}
|
|
156
|
+
/**
|
|
157
|
+
* Checks if the current operation should be captured based on sample rate.
|
|
158
|
+
*
|
|
159
|
+
* @returns True if capture is allowed
|
|
160
|
+
*/
|
|
72
161
|
shouldCapture() {
|
|
73
162
|
if (this.config.sampleRate >= 1) {
|
|
74
163
|
return true;
|
|
75
164
|
}
|
|
76
165
|
return Math.random() < this.config.sampleRate;
|
|
77
166
|
}
|
|
167
|
+
/**
|
|
168
|
+
* Broadcasts telemetry data to all connected SSE clients.
|
|
169
|
+
*
|
|
170
|
+
* @param type - The category of the data
|
|
171
|
+
* @param data - The telemetry payload
|
|
172
|
+
*/
|
|
78
173
|
broadcast(type, data) {
|
|
79
174
|
const payload = JSON.stringify({ type, data });
|
|
80
175
|
for (const listener of this.listeners) {
|
|
81
176
|
listener(payload);
|
|
82
177
|
}
|
|
83
178
|
}
|
|
179
|
+
/**
|
|
180
|
+
* Installs the Spectrum orbit into the Gravito core.
|
|
181
|
+
*
|
|
182
|
+
* Sets up collection listeners, initializes storage, and registers API/UI routes.
|
|
183
|
+
*
|
|
184
|
+
* @param core - The planet core instance
|
|
185
|
+
* @returns Resolves when installation is complete
|
|
186
|
+
* @throws {Error} If storage initialization fails
|
|
187
|
+
*
|
|
188
|
+
* @example
|
|
189
|
+
* ```typescript
|
|
190
|
+
* await spectrum.install(core);
|
|
191
|
+
* ```
|
|
192
|
+
*/
|
|
84
193
|
async install(core) {
|
|
85
194
|
if (!this.config.enabled) {
|
|
86
195
|
return;
|
|
87
196
|
}
|
|
88
197
|
await this.config.storage.init();
|
|
198
|
+
await this.config.storage.prune(this.config.maxItems);
|
|
89
199
|
this.setupHttpCollection(core);
|
|
90
200
|
this.setupLogCollection(core);
|
|
91
201
|
this.setupDatabaseCollection(core);
|
|
92
202
|
this.registerRoutes(core);
|
|
93
203
|
core.logger.info(`[Spectrum] Debug Dashboard initialized at ${this.config.path}`);
|
|
94
204
|
}
|
|
205
|
+
/**
|
|
206
|
+
* Configures collection of database queries from Atlas orbit.
|
|
207
|
+
*
|
|
208
|
+
* @param core - The planet core instance
|
|
209
|
+
*/
|
|
95
210
|
setupDatabaseCollection(core) {
|
|
96
211
|
const attachListener = (atlas) => {
|
|
97
212
|
if (!atlas?.Connection) {
|
|
@@ -103,7 +218,8 @@ var SpectrumOrbit = class _SpectrumOrbit {
|
|
|
103
218
|
}
|
|
104
219
|
const data = {
|
|
105
220
|
id: crypto.randomUUID(),
|
|
106
|
-
...query
|
|
221
|
+
...query,
|
|
222
|
+
requestId: this.currentRequestId || void 0
|
|
107
223
|
};
|
|
108
224
|
this.config.storage.storeQuery(data);
|
|
109
225
|
this.broadcast("query", data);
|
|
@@ -131,21 +247,29 @@ var SpectrumOrbit = class _SpectrumOrbit {
|
|
|
131
247
|
} catch (_e) {
|
|
132
248
|
}
|
|
133
249
|
}
|
|
250
|
+
/**
|
|
251
|
+
* Configures interception of HTTP requests and responses.
|
|
252
|
+
*
|
|
253
|
+
* @param core - The planet core instance
|
|
254
|
+
*/
|
|
134
255
|
setupHttpCollection(core) {
|
|
135
256
|
const middleware = async (c, next) => {
|
|
136
257
|
if (c.req.path.startsWith(this.config.path)) {
|
|
137
258
|
return await next();
|
|
138
259
|
}
|
|
260
|
+
const requestId = crypto.randomUUID();
|
|
261
|
+
this.currentRequestId = requestId;
|
|
139
262
|
const startTime = performance.now();
|
|
140
263
|
const startTimestamp = Date.now();
|
|
141
264
|
const res = await next();
|
|
142
265
|
const duration = performance.now() - startTime;
|
|
266
|
+
this.currentRequestId = null;
|
|
143
267
|
if (!this.shouldCapture()) {
|
|
144
268
|
return res;
|
|
145
269
|
}
|
|
146
270
|
const finalRes = res || c.res;
|
|
147
271
|
const request = {
|
|
148
|
-
id:
|
|
272
|
+
id: requestId,
|
|
149
273
|
method: c.req.method,
|
|
150
274
|
path: c.req.path,
|
|
151
275
|
url: c.req.url,
|
|
@@ -155,7 +279,8 @@ var SpectrumOrbit = class _SpectrumOrbit {
|
|
|
155
279
|
ip: c.req.header("x-forwarded-for") || "127.0.0.1",
|
|
156
280
|
userAgent: c.req.header("user-agent"),
|
|
157
281
|
requestHeaders: Object.fromEntries(c.req.raw.headers.entries()),
|
|
158
|
-
responseHeaders: finalRes ? Object.fromEntries(finalRes.headers.entries()) : {}
|
|
282
|
+
responseHeaders: finalRes ? Object.fromEntries(finalRes.headers.entries()) : {},
|
|
283
|
+
requestId
|
|
159
284
|
};
|
|
160
285
|
this.config.storage.storeRequest(request);
|
|
161
286
|
this.broadcast("request", request);
|
|
@@ -163,6 +288,13 @@ var SpectrumOrbit = class _SpectrumOrbit {
|
|
|
163
288
|
};
|
|
164
289
|
core.adapter.use("*", middleware);
|
|
165
290
|
}
|
|
291
|
+
/**
|
|
292
|
+
* Configures interception of application logs.
|
|
293
|
+
*
|
|
294
|
+
* Wraps the core logger to capture all log calls.
|
|
295
|
+
*
|
|
296
|
+
* @param core - The planet core instance
|
|
297
|
+
*/
|
|
166
298
|
setupLogCollection(core) {
|
|
167
299
|
const originalLogger = core.logger;
|
|
168
300
|
const spectrumLogger = {
|
|
@@ -185,6 +317,13 @@ var SpectrumOrbit = class _SpectrumOrbit {
|
|
|
185
317
|
};
|
|
186
318
|
core.logger = spectrumLogger;
|
|
187
319
|
}
|
|
320
|
+
/**
|
|
321
|
+
* Processes and stores a captured log entry.
|
|
322
|
+
*
|
|
323
|
+
* @param level - Log severity level
|
|
324
|
+
* @param message - Primary message string
|
|
325
|
+
* @param args - Additional log arguments
|
|
326
|
+
*/
|
|
188
327
|
captureLog(level, message, args) {
|
|
189
328
|
if (!this.shouldCapture()) {
|
|
190
329
|
return;
|
|
@@ -194,11 +333,17 @@ var SpectrumOrbit = class _SpectrumOrbit {
|
|
|
194
333
|
level,
|
|
195
334
|
message,
|
|
196
335
|
args,
|
|
197
|
-
timestamp: Date.now()
|
|
336
|
+
timestamp: Date.now(),
|
|
337
|
+
requestId: this.currentRequestId || void 0
|
|
198
338
|
};
|
|
199
339
|
this.config.storage.storeLog(log);
|
|
200
340
|
this.broadcast("log", log);
|
|
201
341
|
}
|
|
342
|
+
/**
|
|
343
|
+
* Registers all API and UI routes for the Spectrum dashboard.
|
|
344
|
+
*
|
|
345
|
+
* @param core - The planet core instance
|
|
346
|
+
*/
|
|
202
347
|
registerRoutes(core) {
|
|
203
348
|
const router = core.router;
|
|
204
349
|
const apiPath = `${this.config.path}/api`;
|
|
@@ -242,6 +387,13 @@ var SpectrumOrbit = class _SpectrumOrbit {
|
|
|
242
387
|
router.post(
|
|
243
388
|
`${apiPath}/clear`,
|
|
244
389
|
wrap(async (c) => {
|
|
390
|
+
if (c.req && typeof c.req.header === "function") {
|
|
391
|
+
const token = c.req.header("x-csrf-token") || (await c.req.parseBody())?._csrf;
|
|
392
|
+
const expectedToken = getCsrfToken(c);
|
|
393
|
+
if (expectedToken && (!token || token !== expectedToken)) {
|
|
394
|
+
return c.json({ error: "Invalid CSRF token" }, 419);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
245
397
|
await this.config.storage.clear();
|
|
246
398
|
return c.json({ success: true });
|
|
247
399
|
})
|
|
@@ -252,23 +404,37 @@ var SpectrumOrbit = class _SpectrumOrbit {
|
|
|
252
404
|
const { readable, writable } = new TransformStream();
|
|
253
405
|
const writer = writable.getWriter();
|
|
254
406
|
const encoder = new TextEncoder();
|
|
407
|
+
let isClosed = false;
|
|
408
|
+
const cleanup = () => {
|
|
409
|
+
if (isClosed) {
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
isClosed = true;
|
|
413
|
+
clearInterval(heartbeat);
|
|
414
|
+
this.listeners.delete(send);
|
|
415
|
+
if (typeof writer.close === "function") {
|
|
416
|
+
writer.close().catch(() => {
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
};
|
|
255
420
|
const send = (payload) => {
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
`));
|
|
260
|
-
} catch (_e) {
|
|
261
|
-
this.listeners.delete(send);
|
|
421
|
+
if (isClosed) {
|
|
422
|
+
return;
|
|
262
423
|
}
|
|
424
|
+
Promise.resolve().then(() => writer.write(encoder.encode(`data: ${payload}
|
|
425
|
+
|
|
426
|
+
`))).catch(() => {
|
|
427
|
+
cleanup();
|
|
428
|
+
});
|
|
263
429
|
};
|
|
264
430
|
this.listeners.add(send);
|
|
265
431
|
const heartbeat = setInterval(() => {
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
} catch (_e) {
|
|
269
|
-
clearInterval(heartbeat);
|
|
270
|
-
this.listeners.delete(send);
|
|
432
|
+
if (isClosed) {
|
|
433
|
+
return;
|
|
271
434
|
}
|
|
435
|
+
Promise.resolve().then(() => writer.write(encoder.encode(": heartbeat\n\n"))).catch(() => {
|
|
436
|
+
cleanup();
|
|
437
|
+
});
|
|
272
438
|
}, 3e4);
|
|
273
439
|
return new Response(readable, {
|
|
274
440
|
headers: {
|
|
@@ -282,6 +448,13 @@ var SpectrumOrbit = class _SpectrumOrbit {
|
|
|
282
448
|
router.post(
|
|
283
449
|
`${apiPath}/replay/:id`,
|
|
284
450
|
wrap(async (c) => {
|
|
451
|
+
if (c.req && typeof c.req.header === "function") {
|
|
452
|
+
const token = c.req.header("x-csrf-token") || (await c.req.parseBody())?._csrf;
|
|
453
|
+
const expectedToken = getCsrfToken(c);
|
|
454
|
+
if (expectedToken && (!token || token !== expectedToken)) {
|
|
455
|
+
return c.json({ error: "Invalid CSRF token" }, 419);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
285
458
|
const id = c.req.param("id");
|
|
286
459
|
if (!id) {
|
|
287
460
|
return c.json({ error: "ID required" }, 400);
|
|
@@ -294,7 +467,6 @@ var SpectrumOrbit = class _SpectrumOrbit {
|
|
|
294
467
|
const replayReq = new Request(req.url, {
|
|
295
468
|
method: req.method,
|
|
296
469
|
headers: req.requestHeaders
|
|
297
|
-
// Body logic would be needed here (if captured)
|
|
298
470
|
});
|
|
299
471
|
const res = await core.adapter.fetch(replayReq);
|
|
300
472
|
return c.json({
|
|
@@ -445,7 +617,8 @@ var SpectrumOrbit = class _SpectrumOrbit {
|
|
|
445
617
|
logs: [],
|
|
446
618
|
queries: [],
|
|
447
619
|
connected: false,
|
|
448
|
-
eventSource: null
|
|
620
|
+
eventSource: null,
|
|
621
|
+
csrfToken: this.getCsrfTokenFromCookie()
|
|
449
622
|
}
|
|
450
623
|
},
|
|
451
624
|
computed: {
|
|
@@ -467,6 +640,14 @@ var SpectrumOrbit = class _SpectrumOrbit {
|
|
|
467
640
|
this.fetchData();
|
|
468
641
|
this.initRealtime();
|
|
469
642
|
},
|
|
643
|
+
unmounted() {
|
|
644
|
+
// Cleanup EventSource to prevent memory leaks
|
|
645
|
+
if (this.eventSource) {
|
|
646
|
+
this.eventSource.close();
|
|
647
|
+
this.eventSource = null;
|
|
648
|
+
this.connected = false;
|
|
649
|
+
}
|
|
650
|
+
},
|
|
470
651
|
methods: {
|
|
471
652
|
initRealtime() {
|
|
472
653
|
this.eventSource = new EventSource('${apiPath}/events');
|
|
@@ -505,13 +686,35 @@ var SpectrumOrbit = class _SpectrumOrbit {
|
|
|
505
686
|
},
|
|
506
687
|
async clearData() {
|
|
507
688
|
if (confirm('Are you sure you want to clear all debug data?')) {
|
|
508
|
-
|
|
509
|
-
|
|
689
|
+
try {
|
|
690
|
+
const res = await fetch('${apiPath}/clear', {
|
|
691
|
+
method: 'POST',
|
|
692
|
+
headers: {
|
|
693
|
+
'X-CSRF-Token': this.csrfToken
|
|
694
|
+
}
|
|
695
|
+
});
|
|
696
|
+
if (res.status === 419) {
|
|
697
|
+
alert('CSRF token invalid. Please refresh the page.');
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
700
|
+
this.fetchData();
|
|
701
|
+
} catch (e) {
|
|
702
|
+
alert('Failed to clear data: ' + e.message);
|
|
703
|
+
}
|
|
510
704
|
}
|
|
511
705
|
},
|
|
512
706
|
async replayRequest(id) {
|
|
513
707
|
try {
|
|
514
|
-
const res = await fetch('${apiPath}/replay/' + id, {
|
|
708
|
+
const res = await fetch('${apiPath}/replay/' + id, {
|
|
709
|
+
method: 'POST',
|
|
710
|
+
headers: {
|
|
711
|
+
'X-CSRF-Token': this.csrfToken
|
|
712
|
+
}
|
|
713
|
+
});
|
|
714
|
+
if (res.status === 419) {
|
|
715
|
+
alert('CSRF token invalid. Please refresh the page.');
|
|
716
|
+
return;
|
|
717
|
+
}
|
|
515
718
|
const data = await res.json();
|
|
516
719
|
if (data.success) {
|
|
517
720
|
alert('Replay successful! Status: ' + data.status);
|
|
@@ -550,6 +753,10 @@ var SpectrumOrbit = class _SpectrumOrbit {
|
|
|
550
753
|
'debug': 'text-slate-500'
|
|
551
754
|
};
|
|
552
755
|
return map[l] || 'text-slate-400';
|
|
756
|
+
},
|
|
757
|
+
getCsrfTokenFromCookie() {
|
|
758
|
+
const match = document.cookie.match(/csrf_token=([^;]+)/);
|
|
759
|
+
return match ? match[1] : '';
|
|
553
760
|
}
|
|
554
761
|
}
|
|
555
762
|
}).mount('#app')
|
|
@@ -566,6 +773,16 @@ var SpectrumOrbit = class _SpectrumOrbit {
|
|
|
566
773
|
import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
567
774
|
import { join } from "path";
|
|
568
775
|
var FileStorage = class {
|
|
776
|
+
/**
|
|
777
|
+
* Initializes a new instance of FileStorage.
|
|
778
|
+
*
|
|
779
|
+
* @param config - Configuration including the target directory for JSONL files
|
|
780
|
+
*
|
|
781
|
+
* @example
|
|
782
|
+
* ```typescript
|
|
783
|
+
* const storage = new FileStorage({ directory: './storage' });
|
|
784
|
+
* ```
|
|
785
|
+
*/
|
|
569
786
|
constructor(config) {
|
|
570
787
|
this.config = config;
|
|
571
788
|
this.requestsPath = join(config.directory, "spectrum-requests.jsonl");
|
|
@@ -583,6 +800,12 @@ var FileStorage = class {
|
|
|
583
800
|
logs: [],
|
|
584
801
|
queries: []
|
|
585
802
|
};
|
|
803
|
+
/**
|
|
804
|
+
* Initializes the storage by creating the directory and loading existing files into memory.
|
|
805
|
+
*
|
|
806
|
+
* @returns Resolves when initialization is complete
|
|
807
|
+
* @throws {Error} If directory creation fails
|
|
808
|
+
*/
|
|
586
809
|
async init() {
|
|
587
810
|
if (!existsSync(this.config.directory)) {
|
|
588
811
|
mkdirSync(this.config.directory, { recursive: true });
|
|
@@ -591,6 +814,12 @@ var FileStorage = class {
|
|
|
591
814
|
this.loadCache(this.logsPath, this.cache.logs);
|
|
592
815
|
this.loadCache(this.queriesPath, this.cache.queries);
|
|
593
816
|
}
|
|
817
|
+
/**
|
|
818
|
+
* Loads newline-delimited JSON data from a file into a target array.
|
|
819
|
+
*
|
|
820
|
+
* @param path - The absolute path to the JSONL file
|
|
821
|
+
* @param target - The array to populate with parsed objects
|
|
822
|
+
*/
|
|
594
823
|
loadCache(path, target) {
|
|
595
824
|
if (!existsSync(path)) {
|
|
596
825
|
return;
|
|
@@ -608,6 +837,13 @@ var FileStorage = class {
|
|
|
608
837
|
console.error(`[Spectrum] Failed to load cache from ${path}`, e);
|
|
609
838
|
}
|
|
610
839
|
}
|
|
840
|
+
/**
|
|
841
|
+
* Internal helper to append data to both the in-memory cache and the JSONL file.
|
|
842
|
+
*
|
|
843
|
+
* @param path - File path to append to
|
|
844
|
+
* @param data - The telemetry object
|
|
845
|
+
* @param list - The cache array
|
|
846
|
+
*/
|
|
611
847
|
async append(path, data, list) {
|
|
612
848
|
list.unshift(data);
|
|
613
849
|
try {
|
|
@@ -617,27 +853,72 @@ var FileStorage = class {
|
|
|
617
853
|
console.error(`[Spectrum] Failed to write to ${path}`, e);
|
|
618
854
|
}
|
|
619
855
|
}
|
|
856
|
+
/**
|
|
857
|
+
* Persists a captured HTTP request.
|
|
858
|
+
*
|
|
859
|
+
* @param req - The request snapshot
|
|
860
|
+
*/
|
|
620
861
|
async storeRequest(req) {
|
|
621
862
|
await this.append(this.requestsPath, req, this.cache.requests);
|
|
622
863
|
}
|
|
864
|
+
/**
|
|
865
|
+
* Persists a captured log entry.
|
|
866
|
+
*
|
|
867
|
+
* @param log - The log snapshot
|
|
868
|
+
*/
|
|
623
869
|
async storeLog(log) {
|
|
624
870
|
await this.append(this.logsPath, log, this.cache.logs);
|
|
625
871
|
}
|
|
872
|
+
/**
|
|
873
|
+
* Persists a captured database query.
|
|
874
|
+
*
|
|
875
|
+
* @param query - The query snapshot
|
|
876
|
+
*/
|
|
626
877
|
async storeQuery(query) {
|
|
627
878
|
await this.append(this.queriesPath, query, this.cache.queries);
|
|
628
879
|
}
|
|
880
|
+
/**
|
|
881
|
+
* Retrieves recent HTTP requests from storage.
|
|
882
|
+
*
|
|
883
|
+
* @param limit - Maximum number of items to return
|
|
884
|
+
* @param offset - Pagination offset
|
|
885
|
+
* @returns Array of requests
|
|
886
|
+
*/
|
|
629
887
|
async getRequests(limit = 100, offset = 0) {
|
|
630
888
|
return this.cache.requests.slice(offset, offset + limit);
|
|
631
889
|
}
|
|
890
|
+
/**
|
|
891
|
+
* Retrieves a specific HTTP request by its unique ID.
|
|
892
|
+
*
|
|
893
|
+
* @param id - The snapshot ID
|
|
894
|
+
* @returns The request or null if not found
|
|
895
|
+
*/
|
|
632
896
|
async getRequest(id) {
|
|
633
897
|
return this.cache.requests.find((r) => r.id === id) || null;
|
|
634
898
|
}
|
|
899
|
+
/**
|
|
900
|
+
* Retrieves recent logs from storage.
|
|
901
|
+
*
|
|
902
|
+
* @param limit - Maximum number of items to return
|
|
903
|
+
* @param offset - Pagination offset
|
|
904
|
+
* @returns Array of logs
|
|
905
|
+
*/
|
|
635
906
|
async getLogs(limit = 100, offset = 0) {
|
|
636
907
|
return this.cache.logs.slice(offset, offset + limit);
|
|
637
908
|
}
|
|
909
|
+
/**
|
|
910
|
+
* Retrieves recent database queries from storage.
|
|
911
|
+
*
|
|
912
|
+
* @param limit - Maximum number of items to return
|
|
913
|
+
* @param offset - Pagination offset
|
|
914
|
+
* @returns Array of queries
|
|
915
|
+
*/
|
|
638
916
|
async getQueries(limit = 100, offset = 0) {
|
|
639
917
|
return this.cache.queries.slice(offset, offset + limit);
|
|
640
918
|
}
|
|
919
|
+
/**
|
|
920
|
+
* Wipes all data from both cache and files.
|
|
921
|
+
*/
|
|
641
922
|
async clear() {
|
|
642
923
|
this.cache.requests = [];
|
|
643
924
|
this.cache.logs = [];
|
|
@@ -646,12 +927,31 @@ var FileStorage = class {
|
|
|
646
927
|
writeFileSync(this.logsPath, "");
|
|
647
928
|
writeFileSync(this.queriesPath, "");
|
|
648
929
|
}
|
|
930
|
+
/**
|
|
931
|
+
* Truncates the storage to stay within the specified limit.
|
|
932
|
+
*
|
|
933
|
+
* @param maxItems - The maximum allowed records per category
|
|
934
|
+
*/
|
|
649
935
|
async prune(maxItems) {
|
|
650
936
|
if (this.cache.requests.length > maxItems) {
|
|
651
937
|
this.cache.requests = this.cache.requests.slice(0, maxItems);
|
|
652
938
|
this.rewrite(this.requestsPath, this.cache.requests);
|
|
653
939
|
}
|
|
940
|
+
if (this.cache.logs.length > maxItems) {
|
|
941
|
+
this.cache.logs = this.cache.logs.slice(0, maxItems);
|
|
942
|
+
this.rewrite(this.logsPath, this.cache.logs);
|
|
943
|
+
}
|
|
944
|
+
if (this.cache.queries.length > maxItems) {
|
|
945
|
+
this.cache.queries = this.cache.queries.slice(0, maxItems);
|
|
946
|
+
this.rewrite(this.queriesPath, this.cache.queries);
|
|
947
|
+
}
|
|
654
948
|
}
|
|
949
|
+
/**
|
|
950
|
+
* Overwrites the file content with the current in-memory data.
|
|
951
|
+
*
|
|
952
|
+
* @param path - File path to overwrite
|
|
953
|
+
* @param data - The array of data objects
|
|
954
|
+
*/
|
|
655
955
|
rewrite(path, data) {
|
|
656
956
|
const content = `${data.slice().reverse().map((d) => JSON.stringify(d)).join("\n")}
|
|
657
957
|
`;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gravito/spectrum",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.2",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
@@ -28,23 +28,25 @@
|
|
|
28
28
|
"scripts": {
|
|
29
29
|
"build": "bun run build.ts",
|
|
30
30
|
"typecheck": "bun tsc -p tsconfig.json --noEmit --skipLibCheck",
|
|
31
|
-
"test": "bun test",
|
|
32
|
-
"test:coverage": "bun test --coverage --coverage-
|
|
33
|
-
"test:ci": "bun test --coverage --coverage-
|
|
31
|
+
"test": "bun test --timeout=10000",
|
|
32
|
+
"test:coverage": "bun test --timeout=10000 --coverage --coverage-reporter=lcov --coverage-dir coverage && bun run --bun scripts/check-coverage.ts",
|
|
33
|
+
"test:ci": "bun test --timeout=10000 --coverage --coverage-reporter=lcov --coverage-dir coverage && bun run --bun scripts/check-coverage.ts",
|
|
34
|
+
"test:unit": "bun test tests/ --timeout=10000",
|
|
35
|
+
"test:integration": "test $(find tests -name '*.integration.test.ts' 2>/dev/null | wc -l) -gt 0 && find tests -name '*.integration.test.ts' -print0 | xargs -0 bun test --timeout=10000 || echo 'No integration tests found'"
|
|
34
36
|
},
|
|
35
37
|
"peerDependencies": {
|
|
36
|
-
"@gravito/core": "
|
|
37
|
-
"@gravito/photon": "
|
|
38
|
+
"@gravito/core": "^1.6.1",
|
|
39
|
+
"@gravito/photon": "^1.0.1"
|
|
38
40
|
},
|
|
39
41
|
"devDependencies": {
|
|
40
|
-
"
|
|
42
|
+
"bun-types": "latest",
|
|
41
43
|
"@opentelemetry/api": "^1.9.0",
|
|
42
44
|
"@opentelemetry/sdk-node": "^0.57.0",
|
|
43
45
|
"@opentelemetry/exporter-trace-otlp-http": "^0.57.0",
|
|
44
46
|
"@opentelemetry/resources": "^1.29.0",
|
|
45
47
|
"@opentelemetry/semantic-conventions": "^1.28.0",
|
|
46
|
-
"@gravito/core": "
|
|
47
|
-
"tsup": "^8.
|
|
48
|
+
"@gravito/core": "^1.6.1",
|
|
49
|
+
"tsup": "^8.0.0",
|
|
48
50
|
"typescript": "^5.9.3"
|
|
49
51
|
},
|
|
50
52
|
"optionalDependencies": {
|
|
@@ -70,4 +72,4 @@
|
|
|
70
72
|
"url": "https://github.com/gravito-framework/gravito.git",
|
|
71
73
|
"directory": "packages/monitor"
|
|
72
74
|
}
|
|
73
|
-
}
|
|
75
|
+
}
|