peekinside 0.0.0-stage → 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +45 -2
- package/bin/peekinside.js +8 -0
- package/index.d.ts +389 -0
- package/index.js +3 -0
- package/package.json +77 -4
- package/src/PeekInside.js +731 -0
- package/src/capture/body-capture.js +86 -0
- package/src/capture/body-decoder.js +188 -0
- package/src/capture/exchange-recorder.js +263 -0
- package/src/capture/http-message.js +138 -0
- package/src/capture/ignored-paths.js +49 -0
- package/src/capture/request-body-tap.js +40 -0
- package/src/capture/response-tap.js +83 -0
- package/src/cli.js +254 -0
- package/src/config.js +183 -0
- package/src/crypto.js +189 -0
- package/src/deployment.js +183 -0
- package/src/exceptions/exception-builder.js +339 -0
- package/src/exceptions/exception-capture.js +407 -0
- package/src/exceptions/exception-record.js +118 -0
- package/src/exceptions/fingerprint.js +60 -0
- package/src/exceptions/frame-classifier.js +114 -0
- package/src/exceptions/limits.js +95 -0
- package/src/exceptions/process-monitor.js +192 -0
- package/src/exceptions/rate-limiter.js +50 -0
- package/src/exceptions/request-context.js +337 -0
- package/src/exceptions/safe-json.js +186 -0
- package/src/exceptions/source-context.js +144 -0
- package/src/exceptions/source-maps.js +313 -0
- package/src/exceptions/stack-parser.js +179 -0
- package/src/hub/brand/peekinside-span-core-mark-dark.svg +7 -0
- package/src/hub/brand/peekinside-span-core-mark-light.svg +7 -0
- package/src/hub/client.js +184 -0
- package/src/hub/entry.js +4 -0
- package/src/hub/exception-state.js +97 -0
- package/src/hub/ingest-payload.js +362 -0
- package/src/hub/process.js +175 -0
- package/src/hub/project-log.js +559 -0
- package/src/hub/segment-log.js +282 -0
- package/src/hub/server.js +561 -0
- package/src/hub/spool-ingester.js +110 -0
- package/src/hub/store.js +404 -0
- package/src/instrumentation/console.js +83 -0
- package/src/instrumentation/http.js +146 -0
- package/src/instrumentation/index.js +234 -0
- package/src/instrumentation/jobs.js +186 -0
- package/src/instrumentation/loggers.js +135 -0
- package/src/instrumentation/mongodb.js +221 -0
- package/src/instrumentation/mysql2.js +161 -0
- package/src/instrumentation/operation.js +70 -0
- package/src/instrumentation/patch.js +72 -0
- package/src/instrumentation/pg.js +218 -0
- package/src/instrumentation/prisma.js +181 -0
- package/src/integrations/express-errors.js +135 -0
- package/src/integrations/express.js +57 -0
- package/src/integrations/fastify.js +72 -0
- package/src/integrations/framework-adapter.js +53 -0
- package/src/integrations/http-server.js +32 -0
- package/src/integrations/koa.js +72 -0
- package/src/integrations/server-binder.js +120 -0
- package/src/jobs/job-context.js +40 -0
- package/src/jobs/job-run-record.js +104 -0
- package/src/jobs/job-runner.js +173 -0
- package/src/logs/log-capture.js +143 -0
- package/src/logs/log-record.js +74 -0
- package/src/project.js +76 -0
- package/src/record.js +181 -0
- package/src/release.js +157 -0
- package/src/runtime/loop-monitor.js +162 -0
- package/src/runtime/sampler.js +93 -0
- package/src/spool.js +195 -0
- package/src/timeline/caller.js +129 -0
- package/src/timeline/execution-hash.js +110 -0
- package/src/timeline/lru.js +50 -0
- package/src/timeline/masking.js +266 -0
- package/src/timeline/shape.js +77 -0
- package/src/timeline/sql.js +396 -0
- package/src/timeline/timeline.js +642 -0
- package/src/transport/batch.js +324 -0
- package/src/transport/cloud.js +363 -0
- package/src/transport/local.js +354 -0
- package/src/user.js +83 -0
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An append-only log of records in one folder, kept in segment files:
|
|
3
|
+
*
|
|
4
|
+
* 00000001.jsonl details, one JSON object per line
|
|
5
|
+
* 00000001.idx one summary per line, with the offset and length of its detail line
|
|
6
|
+
*
|
|
7
|
+
* Summaries are kept in memory, so lists never touch the disk; details are read on demand. The log
|
|
8
|
+
* keeps its newest `maxEntries` records: files rotate in segments, and the oldest segment is
|
|
9
|
+
* deleted once the rest still holds `maxEntries`. Deleting whole files keeps retention cheap:
|
|
10
|
+
* nothing is ever rewritten.
|
|
11
|
+
*
|
|
12
|
+
* Every record gets a sequence number. Numbers keep increasing after the log is cleared (the
|
|
13
|
+
* caller stores `seqBase`), so dashboards that poll with `after` stay in step.
|
|
14
|
+
*
|
|
15
|
+
* A project's requests and its exceptions are two such logs (see ProjectLog).
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import fs from 'fs';
|
|
19
|
+
import path from 'path';
|
|
20
|
+
|
|
21
|
+
const SEGMENT_INDEX = /^(\d{8})\.idx$/;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* One log of records with summaries and details.
|
|
25
|
+
*/
|
|
26
|
+
export class SegmentLog {
|
|
27
|
+
/**
|
|
28
|
+
* Use SegmentLog.load() for a log that may have files already.
|
|
29
|
+
*
|
|
30
|
+
* @param {string} dir - The log's folder; created on the first append
|
|
31
|
+
* @param {Object} options
|
|
32
|
+
* @param {number} options.maxEntries - Records kept
|
|
33
|
+
* @param {number} options.segmentSize - Records per segment file
|
|
34
|
+
* @param {number} options.seqBase - Lowest sequence number the next record may get
|
|
35
|
+
* @param {(source: Object) => Object} options.pick - The summary of a record or index entry:
|
|
36
|
+
* only known fields, in a fixed order (index lines and API answers never carry anything else)
|
|
37
|
+
*/
|
|
38
|
+
constructor(dir, { maxEntries, segmentSize, seqBase, pick }) {
|
|
39
|
+
this.dir = dir;
|
|
40
|
+
this.maxEntries = maxEntries;
|
|
41
|
+
this.segmentSize = segmentSize;
|
|
42
|
+
this.pick = pick;
|
|
43
|
+
// Summaries with their place on disk (seg, off, len), oldest first, sorted by seq.
|
|
44
|
+
this.entries = [];
|
|
45
|
+
this.byId = new Map();
|
|
46
|
+
// Numbers of the segment files on disk, oldest first, and the one being written.
|
|
47
|
+
this.segments = [];
|
|
48
|
+
this.segment = { no: 1, count: 0, size: 0 };
|
|
49
|
+
this.seqBase = seqBase;
|
|
50
|
+
this.nextSeq = seqBase;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Reads a log from its folder. Index lines cut short by a crash are skipped; a missing folder is
|
|
55
|
+
* an empty log.
|
|
56
|
+
*
|
|
57
|
+
* @param {string} dir
|
|
58
|
+
* @param {Object} options - See the constructor
|
|
59
|
+
* @returns {SegmentLog}
|
|
60
|
+
*/
|
|
61
|
+
static load(dir, options) {
|
|
62
|
+
const log = new SegmentLog(dir, options);
|
|
63
|
+
let names;
|
|
64
|
+
try {
|
|
65
|
+
names = fs.readdirSync(dir);
|
|
66
|
+
} catch (error) {
|
|
67
|
+
if (error.code === 'ENOENT') return log;
|
|
68
|
+
throw error;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
log.segments = names
|
|
72
|
+
.map((name) => SEGMENT_INDEX.exec(name))
|
|
73
|
+
.filter(Boolean)
|
|
74
|
+
.map((match) => Number(match[1]))
|
|
75
|
+
.sort((a, b) => a - b);
|
|
76
|
+
|
|
77
|
+
let maxSeq = 0;
|
|
78
|
+
for (const no of log.segments) {
|
|
79
|
+
const lines = fs.readFileSync(log._segmentFile(no, 'idx'), 'utf8').split('\n');
|
|
80
|
+
for (const line of lines) {
|
|
81
|
+
if (!line) continue;
|
|
82
|
+
let entry;
|
|
83
|
+
try {
|
|
84
|
+
entry = JSON.parse(line);
|
|
85
|
+
} catch (error) {
|
|
86
|
+
continue; // a line cut short by a crash
|
|
87
|
+
}
|
|
88
|
+
if (!entry || typeof entry.id !== 'string' || !Number.isInteger(entry.seq) || log.byId.has(entry.id)) continue;
|
|
89
|
+
entry.seg = no;
|
|
90
|
+
log.entries.push(entry);
|
|
91
|
+
log.byId.set(entry.id, entry);
|
|
92
|
+
maxSeq = Math.max(maxSeq, entry.seq);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
log.entries.sort((a, b) => a.seq - b.seq);
|
|
96
|
+
log.nextSeq = Math.max(log.seqBase, maxSeq + 1);
|
|
97
|
+
|
|
98
|
+
const last = log.segments[log.segments.length - 1];
|
|
99
|
+
if (last) {
|
|
100
|
+
let size = 0;
|
|
101
|
+
try {
|
|
102
|
+
size = fs.statSync(log._segmentFile(last, 'jsonl')).size;
|
|
103
|
+
} catch (error) {
|
|
104
|
+
size = 0;
|
|
105
|
+
}
|
|
106
|
+
log.segment = { no: last, count: log.entries.filter((entry) => entry.seg === last).length, size };
|
|
107
|
+
}
|
|
108
|
+
return log;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Number of records kept.
|
|
113
|
+
*
|
|
114
|
+
* @type {number}
|
|
115
|
+
*/
|
|
116
|
+
get size() {
|
|
117
|
+
return this.entries.length;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* The newest record's summary entry, or null.
|
|
122
|
+
*
|
|
123
|
+
* @type {Object|null}
|
|
124
|
+
*/
|
|
125
|
+
get newest() {
|
|
126
|
+
return this.entries[this.entries.length - 1] || null;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Stores new records. Records already stored (same id) are skipped, so retried batches are
|
|
131
|
+
* harmless.
|
|
132
|
+
*
|
|
133
|
+
* @param {Object[]} records - Validated records: summary fields plus `detail`
|
|
134
|
+
* @returns {Object[]} Summaries of the stored records
|
|
135
|
+
*/
|
|
136
|
+
append(records) {
|
|
137
|
+
const added = [];
|
|
138
|
+
let data = [];
|
|
139
|
+
let index = [];
|
|
140
|
+
// One write per file and segment, rather than one per record.
|
|
141
|
+
const write = () => {
|
|
142
|
+
if (!data.length) return;
|
|
143
|
+
fs.mkdirSync(this.dir, { recursive: true, mode: 0o700 });
|
|
144
|
+
fs.appendFileSync(this._segmentFile(this.segment.no, 'jsonl'), data.join(''), { mode: 0o600 });
|
|
145
|
+
fs.appendFileSync(this._segmentFile(this.segment.no, 'idx'), index.join(''), { mode: 0o600 });
|
|
146
|
+
if (!this.segments.includes(this.segment.no)) this.segments.push(this.segment.no);
|
|
147
|
+
data = [];
|
|
148
|
+
index = [];
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
for (const record of records) {
|
|
152
|
+
if (this.byId.has(record.id)) continue;
|
|
153
|
+
if (this.segment.count >= this.segmentSize) {
|
|
154
|
+
write();
|
|
155
|
+
this.segment = { no: this.segment.no + 1, count: 0, size: 0 };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const line = JSON.stringify(record.detail ?? {});
|
|
159
|
+
const length = Buffer.byteLength(line);
|
|
160
|
+
const entry = {
|
|
161
|
+
...this.pick({ ...record, seq: this.nextSeq++ }),
|
|
162
|
+
seg: this.segment.no,
|
|
163
|
+
off: this.segment.size,
|
|
164
|
+
len: length
|
|
165
|
+
};
|
|
166
|
+
this.segment.size += length + 1;
|
|
167
|
+
this.segment.count++;
|
|
168
|
+
data.push(`${line}\n`);
|
|
169
|
+
index.push(`${JSON.stringify(entry)}\n`);
|
|
170
|
+
this.entries.push(entry);
|
|
171
|
+
this.byId.set(entry.id, entry);
|
|
172
|
+
added.push(this.pick(entry));
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
write();
|
|
176
|
+
this.enforceRetention();
|
|
177
|
+
return added;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Summaries, newest first; oldest first when `after` is given (for polling new records).
|
|
182
|
+
*
|
|
183
|
+
* @param {{ after?: number, before?: number, limit: number }} query - Sequence numbers to page by
|
|
184
|
+
* @returns {{ entries: Object[], hasMore: boolean }}
|
|
185
|
+
*/
|
|
186
|
+
list({ after, before, limit }) {
|
|
187
|
+
const entries = this.entries;
|
|
188
|
+
|
|
189
|
+
if (after !== undefined) {
|
|
190
|
+
const start = firstIndex(entries, (entry) => entry.seq > after);
|
|
191
|
+
return {
|
|
192
|
+
entries: entries.slice(start, start + limit).map(this.pick),
|
|
193
|
+
hasMore: start + limit < entries.length
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const end = before === undefined ? entries.length : firstIndex(entries, (entry) => entry.seq >= before);
|
|
198
|
+
const start = Math.max(0, end - limit);
|
|
199
|
+
return {
|
|
200
|
+
entries: entries.slice(start, end).reverse().map(this.pick),
|
|
201
|
+
hasMore: start > 0
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* The detail of one record, read from disk.
|
|
207
|
+
*
|
|
208
|
+
* @param {string} id
|
|
209
|
+
* @returns {Object|null} null when it is no longer stored
|
|
210
|
+
*/
|
|
211
|
+
detail(id) {
|
|
212
|
+
const entry = this.byId.get(id);
|
|
213
|
+
if (!entry) return null;
|
|
214
|
+
|
|
215
|
+
let fd;
|
|
216
|
+
try {
|
|
217
|
+
fd = fs.openSync(this._segmentFile(entry.seg, 'jsonl'), 'r');
|
|
218
|
+
} catch (error) {
|
|
219
|
+
if (error.code === 'ENOENT') return null;
|
|
220
|
+
throw error;
|
|
221
|
+
}
|
|
222
|
+
try {
|
|
223
|
+
const buffer = Buffer.alloc(entry.len);
|
|
224
|
+
const read = fs.readSync(fd, buffer, 0, entry.len, entry.off);
|
|
225
|
+
return JSON.parse(buffer.subarray(0, read).toString('utf8'));
|
|
226
|
+
} finally {
|
|
227
|
+
fs.closeSync(fd);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Deletes all records. Sequence numbers continue from where they were: the caller stores the
|
|
233
|
+
* new `seqBase`.
|
|
234
|
+
*
|
|
235
|
+
* @returns {void}
|
|
236
|
+
*/
|
|
237
|
+
clear() {
|
|
238
|
+
for (const no of this.segments) this._deleteSegment(no);
|
|
239
|
+
this.segments = [];
|
|
240
|
+
this.entries = [];
|
|
241
|
+
this.byId.clear();
|
|
242
|
+
this.segment = { no: this.segment.no + 1, count: 0, size: 0 };
|
|
243
|
+
this.seqBase = this.nextSeq;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Deletes the oldest segments while the newer ones still hold `maxEntries` records.
|
|
248
|
+
*
|
|
249
|
+
* @returns {void}
|
|
250
|
+
*/
|
|
251
|
+
enforceRetention() {
|
|
252
|
+
while (this.segments.length > 1) {
|
|
253
|
+
const oldest = this.segments[0];
|
|
254
|
+
const inOldest = firstIndex(this.entries, (entry) => entry.seg !== oldest);
|
|
255
|
+
if (this.entries.length - inOldest < this.maxEntries) break;
|
|
256
|
+
for (const entry of this.entries.splice(0, inOldest)) this.byId.delete(entry.id);
|
|
257
|
+
this._deleteSegment(oldest);
|
|
258
|
+
this.segments.shift();
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
_deleteSegment(no) {
|
|
263
|
+
fs.rmSync(this._segmentFile(no, 'jsonl'), { force: true });
|
|
264
|
+
fs.rmSync(this._segmentFile(no, 'idx'), { force: true });
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
_segmentFile(no, extension) {
|
|
268
|
+
return path.join(this.dir, `${String(no).padStart(8, '0')}.${extension}`);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Binary search over entries sorted by seq: the first index where `predicate` holds.
|
|
273
|
+
function firstIndex(entries, predicate) {
|
|
274
|
+
let low = 0;
|
|
275
|
+
let high = entries.length;
|
|
276
|
+
while (low < high) {
|
|
277
|
+
const middle = (low + high) >>> 1;
|
|
278
|
+
if (predicate(entries[middle])) high = middle;
|
|
279
|
+
else low = middle + 1;
|
|
280
|
+
}
|
|
281
|
+
return low;
|
|
282
|
+
}
|