proteum 2.2.0 → 2.2.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/AGENTS.md +5 -3
- package/README.md +14 -3
- package/agents/project/AGENTS.md +14 -10
- package/agents/project/app-root/AGENTS.md +2 -2
- package/agents/project/diagnostics.md +2 -2
- package/agents/project/root/AGENTS.md +9 -7
- package/agents/project/tests/AGENTS.md +8 -1
- package/cli/commands/configure.ts +12 -3
- package/cli/commands/dev.ts +162 -12
- package/cli/compiler/common/index.ts +16 -0
- package/cli/presentation/commands.ts +1 -0
- package/cli/presentation/help.ts +4 -0
- package/cli/utils/agents.ts +36 -0
- package/common/dev/requestTrace.ts +66 -0
- package/common/env/proteumEnv.ts +10 -3
- package/docs/assets/unique-domains-chip.png +0 -0
- package/docs/request-tracing.md +17 -3
- package/package.json +1 -1
- package/server/app/container/trace/index.ts +255 -74
- package/server/services/prisma/index.ts +15 -12
- package/server/services/router/http/index.ts +1 -1
- package/server/services/router/index.ts +41 -9
- package/server/services/router/request/index.ts +21 -2
|
@@ -16,6 +16,9 @@ import {
|
|
|
16
16
|
type TTraceSqlQueryCallerOrigin,
|
|
17
17
|
type TTraceSqlQueryKind,
|
|
18
18
|
type TTraceSummaryValue,
|
|
19
|
+
type TRequestProfiling,
|
|
20
|
+
type TRequestProfilingApiCall,
|
|
21
|
+
type TRequestProfilingSqlQuery,
|
|
19
22
|
type TRequestTrace,
|
|
20
23
|
type TTraceMemorySnapshot,
|
|
21
24
|
type TRequestTraceListItem,
|
|
@@ -24,6 +27,7 @@ import type { TProteumManifest } from '@common/dev/proteumManifest';
|
|
|
24
27
|
|
|
25
28
|
export type Config = {
|
|
26
29
|
enable: boolean;
|
|
30
|
+
profilerEnable: boolean;
|
|
27
31
|
requestsLimit: number;
|
|
28
32
|
eventsLimit: number;
|
|
29
33
|
capture: TTraceCaptureMode;
|
|
@@ -32,6 +36,12 @@ export type Config = {
|
|
|
32
36
|
|
|
33
37
|
type TTraceInspectable = object | PrimitiveValue | bigint | symbol | null | undefined | (() => void);
|
|
34
38
|
type TTraceDetails = { [key: string]: TTraceInspectable };
|
|
39
|
+
type TSerializeJsonValueOptions = { redactSensitive: boolean };
|
|
40
|
+
type TActiveRequestRecord = {
|
|
41
|
+
profiling: TRequestProfiling;
|
|
42
|
+
trace?: TRequestTrace;
|
|
43
|
+
capture?: TTraceCaptureMode;
|
|
44
|
+
};
|
|
35
45
|
|
|
36
46
|
const capturePriority: Record<TTraceCaptureMode, number> = { summary: 0, resolve: 1, deep: 2 };
|
|
37
47
|
const sensitiveKeyPattern =
|
|
@@ -50,8 +60,13 @@ const isSensitiveKeyPath = (keyPath: string[]) => sensitiveKeyPattern.test(keyPa
|
|
|
50
60
|
const summarizeString = (value: string) =>
|
|
51
61
|
value.length <= maxStringLength ? value : `${value.slice(0, maxStringLength)}…`;
|
|
52
62
|
|
|
53
|
-
const serializeJsonValue = (
|
|
54
|
-
|
|
63
|
+
const serializeJsonValue = (
|
|
64
|
+
value: unknown,
|
|
65
|
+
keyPath: string[],
|
|
66
|
+
seen: WeakSet<object>,
|
|
67
|
+
{ redactSensitive }: TSerializeJsonValueOptions,
|
|
68
|
+
): unknown => {
|
|
69
|
+
if (redactSensitive && isSensitiveKeyPath(keyPath)) return `[redacted: Sensitive key ${keyPath[keyPath.length - 1] || 'value'}]`;
|
|
55
70
|
if (value === undefined || value === null) return value;
|
|
56
71
|
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') return value;
|
|
57
72
|
if (typeof value === 'bigint') return `${value.toString()}n`;
|
|
@@ -61,11 +76,14 @@ const serializeJsonValue = (value: unknown, keyPath: string[], seen: WeakSet<obj
|
|
|
61
76
|
if (value instanceof Date) return value.toISOString();
|
|
62
77
|
if (value instanceof Error) return { name: value.name, message: value.message, stack: value.stack };
|
|
63
78
|
if (Buffer.isBuffer(value)) return `[Buffer ${value.byteLength} bytes]`;
|
|
64
|
-
if (value instanceof Map)
|
|
65
|
-
|
|
66
|
-
|
|
79
|
+
if (value instanceof Map)
|
|
80
|
+
return Array.from(value.entries()).map(([entryKey, entryValue], index) =>
|
|
81
|
+
serializeJsonValue([entryKey, entryValue], [...keyPath, `[${index}]`], seen, { redactSensitive }),
|
|
82
|
+
);
|
|
67
83
|
if (value instanceof Set) {
|
|
68
|
-
return Array.from(value.values()).map((entryValue, index) =>
|
|
84
|
+
return Array.from(value.values()).map((entryValue, index) =>
|
|
85
|
+
serializeJsonValue(entryValue, [...keyPath, `[${index}]`], seen, { redactSensitive }),
|
|
86
|
+
);
|
|
69
87
|
}
|
|
70
88
|
|
|
71
89
|
if (typeof value !== 'object') return String(value);
|
|
@@ -74,19 +92,22 @@ const serializeJsonValue = (value: unknown, keyPath: string[], seen: WeakSet<obj
|
|
|
74
92
|
seen.add(value);
|
|
75
93
|
|
|
76
94
|
if (Array.isArray(value)) {
|
|
77
|
-
return value.map((item, index) => serializeJsonValue(item, [...keyPath, `[${index}]`], seen));
|
|
95
|
+
return value.map((item, index) => serializeJsonValue(item, [...keyPath, `[${index}]`], seen, { redactSensitive }));
|
|
78
96
|
}
|
|
79
97
|
|
|
80
98
|
const serialized: Record<string, unknown> = {};
|
|
81
99
|
for (const [entryKey, entryValue] of Object.entries(value)) {
|
|
82
|
-
const nextValue = serializeJsonValue(entryValue, [...keyPath, entryKey], seen);
|
|
100
|
+
const nextValue = serializeJsonValue(entryValue, [...keyPath, entryKey], seen, { redactSensitive });
|
|
83
101
|
if (nextValue !== undefined) serialized[entryKey] = nextValue;
|
|
84
102
|
}
|
|
85
103
|
|
|
86
104
|
return serialized;
|
|
87
105
|
};
|
|
88
106
|
|
|
89
|
-
const serializeCaptureValue = (value: TTraceInspectable, key: string) =>
|
|
107
|
+
const serializeCaptureValue = (value: TTraceInspectable, key: string) =>
|
|
108
|
+
serializeJsonValue(value, [key], new WeakSet<object>(), { redactSensitive: true });
|
|
109
|
+
const serializeRawCaptureValue = (value: TTraceInspectable, key: string) =>
|
|
110
|
+
serializeJsonValue(value, [key], new WeakSet<object>(), { redactSensitive: false });
|
|
90
111
|
|
|
91
112
|
const summarizeError = (error: Error): TTraceSummaryValue => ({
|
|
92
113
|
kind: 'error',
|
|
@@ -181,7 +202,7 @@ const snapshotMemory = (): TTraceMemorySnapshot => {
|
|
|
181
202
|
};
|
|
182
203
|
|
|
183
204
|
export default class Trace {
|
|
184
|
-
private requests = new Map<string,
|
|
205
|
+
private requests = new Map<string, TActiveRequestRecord>();
|
|
185
206
|
private order: string[] = [];
|
|
186
207
|
private armedCapture?: TTraceCaptureMode;
|
|
187
208
|
private manifestCache?: {
|
|
@@ -202,10 +223,18 @@ export default class Trace {
|
|
|
202
223
|
private config: Config,
|
|
203
224
|
) {}
|
|
204
225
|
|
|
205
|
-
public
|
|
226
|
+
public isDevTraceEnabled() {
|
|
206
227
|
return __DEV__ && this.config.enable && this.container.Environment.profile === 'dev';
|
|
207
228
|
}
|
|
208
229
|
|
|
230
|
+
public isProfilingEnabled() {
|
|
231
|
+
return this.config.profilerEnable;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
public shouldInstrumentRequests() {
|
|
235
|
+
return this.isDevTraceEnabled() || this.isProfilingEnabled();
|
|
236
|
+
}
|
|
237
|
+
|
|
209
238
|
private getContextChannel() {
|
|
210
239
|
return context.getStore() as ChannelInfos | undefined;
|
|
211
240
|
}
|
|
@@ -270,8 +299,8 @@ export default class Trace {
|
|
|
270
299
|
return undefined;
|
|
271
300
|
}
|
|
272
301
|
|
|
273
|
-
private
|
|
274
|
-
|
|
302
|
+
private normalizeSqlQuery(query: string) {
|
|
303
|
+
return query
|
|
275
304
|
.replace(sqlCommentPattern, ' ')
|
|
276
305
|
.replace(sqlLineCommentPattern, ' ')
|
|
277
306
|
.replace(/'([^']|'')*'/g, '?')
|
|
@@ -280,12 +309,43 @@ export default class Trace {
|
|
|
280
309
|
.replace(/\s+/g, ' ')
|
|
281
310
|
.trim()
|
|
282
311
|
.toUpperCase();
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
private createSqlFingerprint(query: string) {
|
|
315
|
+
const normalized = this.normalizeSqlQuery(query);
|
|
283
316
|
|
|
284
317
|
if (!normalized) return undefined;
|
|
285
318
|
|
|
286
319
|
return createHash('sha1').update(normalized).digest('hex').slice(0, 12);
|
|
287
320
|
}
|
|
288
321
|
|
|
322
|
+
private createProfiling(input: {
|
|
323
|
+
enabled: boolean;
|
|
324
|
+
id: string;
|
|
325
|
+
method: string;
|
|
326
|
+
path: string;
|
|
327
|
+
url: string;
|
|
328
|
+
profilerOrigin?: string;
|
|
329
|
+
profilerParentRequestId?: string;
|
|
330
|
+
}): TRequestProfiling {
|
|
331
|
+
return {
|
|
332
|
+
enabled: input.enabled,
|
|
333
|
+
requestId: input.id,
|
|
334
|
+
method: input.method,
|
|
335
|
+
path: input.path,
|
|
336
|
+
url: input.url,
|
|
337
|
+
startedAt: nowIso(),
|
|
338
|
+
profilerOrigin: input.profilerOrigin,
|
|
339
|
+
profilerParentRequestId: input.profilerParentRequestId,
|
|
340
|
+
apiCalls: [],
|
|
341
|
+
sqlQueries: [],
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
private getRecord(requestId: string) {
|
|
346
|
+
return this.requests.get(requestId);
|
|
347
|
+
}
|
|
348
|
+
|
|
289
349
|
public armNextRequest(capture: string) {
|
|
290
350
|
if (!isTraceCaptureMode(capture)) {
|
|
291
351
|
throw new Error(`Unsupported trace capture mode "${capture}". Expected one of: ${traceCaptureModes.join(', ')}.`);
|
|
@@ -306,46 +366,72 @@ export default class Trace {
|
|
|
306
366
|
profilerOrigin?: string;
|
|
307
367
|
profilerParentRequestId?: string;
|
|
308
368
|
}) {
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
this.armedCapture = undefined;
|
|
313
|
-
|
|
314
|
-
const trace: TRequestTrace = {
|
|
369
|
+
const profilingEnabled = this.shouldInstrumentRequests();
|
|
370
|
+
const profiling = this.createProfiling({
|
|
371
|
+
enabled: profilingEnabled,
|
|
315
372
|
id: input.id,
|
|
316
373
|
method: input.method,
|
|
317
374
|
path: input.path,
|
|
318
375
|
url: input.url,
|
|
319
|
-
capture,
|
|
320
|
-
profilerSessionId: input.profilerSessionId,
|
|
321
376
|
profilerOrigin: input.profilerOrigin,
|
|
322
377
|
profilerParentRequestId: input.profilerParentRequestId,
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
378
|
+
});
|
|
379
|
+
if (!profilingEnabled) return profiling;
|
|
380
|
+
|
|
381
|
+
const traceEnabled = this.isDevTraceEnabled();
|
|
382
|
+
const capture = traceEnabled ? this.armedCapture ?? this.config.capture : undefined;
|
|
383
|
+
this.armedCapture = undefined;
|
|
384
|
+
|
|
385
|
+
const trace =
|
|
386
|
+
traceEnabled
|
|
387
|
+
? ({
|
|
388
|
+
id: input.id,
|
|
389
|
+
method: input.method,
|
|
390
|
+
path: input.path,
|
|
391
|
+
url: input.url,
|
|
392
|
+
capture: capture as TTraceCaptureMode,
|
|
393
|
+
profilerSessionId: input.profilerSessionId,
|
|
394
|
+
profilerOrigin: input.profilerOrigin,
|
|
395
|
+
profilerParentRequestId: input.profilerParentRequestId,
|
|
396
|
+
startedAt: profiling.startedAt,
|
|
397
|
+
droppedEvents: 0,
|
|
398
|
+
requestDataJson: serializeCaptureValue(input.data, 'requestData'),
|
|
399
|
+
calls: [],
|
|
400
|
+
sqlQueries: [],
|
|
401
|
+
events: [],
|
|
402
|
+
} satisfies TRequestTrace)
|
|
403
|
+
: undefined;
|
|
404
|
+
|
|
405
|
+
this.requests.set(input.id, {
|
|
406
|
+
profiling,
|
|
407
|
+
trace,
|
|
408
|
+
capture,
|
|
409
|
+
});
|
|
410
|
+
this.activeMeasurements.set(input.id, { cpu: process.cpuUsage(), memory: snapshotMemory() });
|
|
330
411
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
412
|
+
if (trace) {
|
|
413
|
+
this.order.push(input.id);
|
|
414
|
+
this.trimRequestBuffer();
|
|
415
|
+
this.record(input.id, 'request.start', { method: input.method, path: input.path, url: input.url, headers: input.headers, data: input.data });
|
|
416
|
+
}
|
|
335
417
|
|
|
336
|
-
|
|
418
|
+
return profiling;
|
|
337
419
|
}
|
|
338
420
|
|
|
339
421
|
public setRequestUser(requestId: string, user?: string) {
|
|
340
|
-
const
|
|
341
|
-
if (!
|
|
422
|
+
const record = this.getRecord(requestId);
|
|
423
|
+
if (!record) return;
|
|
424
|
+
|
|
425
|
+
record.profiling.user = user;
|
|
426
|
+
if (!record.trace) return;
|
|
342
427
|
|
|
428
|
+
const trace = record.trace;
|
|
343
429
|
trace.user = user;
|
|
344
430
|
if (user) this.record(requestId, 'request.user', { user });
|
|
345
431
|
}
|
|
346
432
|
|
|
347
433
|
public getCapture(requestId: string) {
|
|
348
|
-
return this.
|
|
434
|
+
return this.getRecord(requestId)?.capture;
|
|
349
435
|
}
|
|
350
436
|
|
|
351
437
|
public shouldCapture(requestId: string, minimumCapture: TTraceCaptureMode) {
|
|
@@ -356,7 +442,8 @@ export default class Trace {
|
|
|
356
442
|
}
|
|
357
443
|
|
|
358
444
|
public record(requestId: string, type: TTraceEventType, details: TTraceDetails, minimumCapture: TTraceCaptureMode = 'summary') {
|
|
359
|
-
const
|
|
445
|
+
const record = this.getRecord(requestId);
|
|
446
|
+
const trace = record?.trace;
|
|
360
447
|
if (!trace || !this.shouldCapture(requestId, minimumCapture)) return;
|
|
361
448
|
|
|
362
449
|
if (trace.events.length >= this.config.eventsLimit) {
|
|
@@ -376,28 +463,41 @@ export default class Trace {
|
|
|
376
463
|
}
|
|
377
464
|
|
|
378
465
|
public finishRequest(requestId: string, output: { statusCode: number; user?: string; errorMessage?: string }) {
|
|
379
|
-
const
|
|
380
|
-
if (!
|
|
466
|
+
const record = this.getRecord(requestId);
|
|
467
|
+
if (!record) return;
|
|
468
|
+
|
|
469
|
+
const { profiling, trace } = record;
|
|
470
|
+
if (output.user) profiling.user = output.user;
|
|
471
|
+
profiling.statusCode = output.statusCode;
|
|
472
|
+
profiling.errorMessage = output.errorMessage;
|
|
381
473
|
|
|
382
|
-
if (output.user) trace.user = output.user;
|
|
383
|
-
trace.statusCode = output.statusCode;
|
|
384
|
-
trace.errorMessage = output.errorMessage;
|
|
385
474
|
const measurement = this.activeMeasurements.get(requestId);
|
|
386
475
|
if (measurement) {
|
|
387
|
-
const cpu = process.cpuUsage(measurement.cpu);
|
|
388
|
-
trace.performance = {
|
|
389
|
-
cpu: {
|
|
390
|
-
systemMicros: cpu.system,
|
|
391
|
-
userMicros: cpu.user,
|
|
392
|
-
},
|
|
393
|
-
memory: {
|
|
394
|
-
after: snapshotMemory(),
|
|
395
|
-
before: measurement.memory,
|
|
396
|
-
},
|
|
397
|
-
};
|
|
398
476
|
this.activeMeasurements.delete(requestId);
|
|
477
|
+
|
|
478
|
+
if (trace) {
|
|
479
|
+
const cpu = process.cpuUsage(measurement.cpu);
|
|
480
|
+
trace.performance = {
|
|
481
|
+
cpu: {
|
|
482
|
+
systemMicros: cpu.system,
|
|
483
|
+
userMicros: cpu.user,
|
|
484
|
+
},
|
|
485
|
+
memory: {
|
|
486
|
+
after: snapshotMemory(),
|
|
487
|
+
before: measurement.memory,
|
|
488
|
+
},
|
|
489
|
+
};
|
|
490
|
+
}
|
|
399
491
|
}
|
|
400
492
|
|
|
493
|
+
profiling.finishedAt = nowIso();
|
|
494
|
+
profiling.durationMs = Math.max(0, Date.parse(profiling.finishedAt) - Date.parse(profiling.startedAt));
|
|
495
|
+
|
|
496
|
+
if (!trace) return;
|
|
497
|
+
|
|
498
|
+
if (output.user) trace.user = output.user;
|
|
499
|
+
trace.statusCode = output.statusCode;
|
|
500
|
+
trace.errorMessage = output.errorMessage;
|
|
401
501
|
this.record(
|
|
402
502
|
requestId,
|
|
403
503
|
'request.finish',
|
|
@@ -405,8 +505,8 @@ export default class Trace {
|
|
|
405
505
|
'summary',
|
|
406
506
|
);
|
|
407
507
|
|
|
408
|
-
trace.finishedAt =
|
|
409
|
-
trace.durationMs =
|
|
508
|
+
trace.finishedAt = profiling.finishedAt;
|
|
509
|
+
trace.durationMs = profiling.durationMs;
|
|
410
510
|
|
|
411
511
|
if (this.config.persistOnError && trace.statusCode >= 500) {
|
|
412
512
|
trace.persistedFilepath = this.exportRequest(requestId);
|
|
@@ -433,13 +533,37 @@ export default class Trace {
|
|
|
433
533
|
requestData?: TTraceInspectable;
|
|
434
534
|
},
|
|
435
535
|
) {
|
|
436
|
-
const
|
|
437
|
-
if (!
|
|
536
|
+
const record = this.getRecord(requestId);
|
|
537
|
+
if (!record) return undefined;
|
|
438
538
|
const channel = this.getContextChannel();
|
|
439
539
|
const inferredServiceLabel = input.serviceLabel || channel?.serviceLabel || this.inferServiceLabelFromStack(new Error().stack);
|
|
540
|
+
const callIndex = record.profiling.apiCalls.length;
|
|
541
|
+
const startedAt = nowIso();
|
|
542
|
+
const callId = `${requestId}:call:${callIndex}`;
|
|
543
|
+
|
|
544
|
+
const profilingCall: TRequestProfilingApiCall = {
|
|
545
|
+
id: callId,
|
|
546
|
+
origin: input.origin,
|
|
547
|
+
label: input.label,
|
|
548
|
+
method: input.method || '',
|
|
549
|
+
path: input.path || '',
|
|
550
|
+
fetcherId: input.fetcherId,
|
|
551
|
+
connectedProjectNamespace: input.connectedProjectNamespace,
|
|
552
|
+
connectedControllerAccessor: input.connectedControllerAccessor,
|
|
553
|
+
ownerLabel: input.ownerLabel || channel?.ownerLabel,
|
|
554
|
+
ownerFilepath: input.ownerFilepath || channel?.ownerFilepath,
|
|
555
|
+
serviceLabel: inferredServiceLabel,
|
|
556
|
+
startedAt,
|
|
557
|
+
requestBodyJson: input.requestData !== undefined ? serializeRawCaptureValue(input.requestData, 'requestData') : undefined,
|
|
558
|
+
};
|
|
559
|
+
|
|
560
|
+
record.profiling.apiCalls.push(profilingCall);
|
|
561
|
+
|
|
562
|
+
const trace = record.trace;
|
|
563
|
+
if (!trace) return callId;
|
|
440
564
|
|
|
441
565
|
const call: TTraceCall = {
|
|
442
|
-
id:
|
|
566
|
+
id: callId,
|
|
443
567
|
parentId: input.parentId,
|
|
444
568
|
origin: input.origin,
|
|
445
569
|
label: input.label,
|
|
@@ -453,7 +577,7 @@ export default class Trace {
|
|
|
453
577
|
serviceLabel: inferredServiceLabel,
|
|
454
578
|
cacheKey: input.cacheKey || channel?.cacheKey,
|
|
455
579
|
cachePhase: input.cachePhase || channel?.cachePhase,
|
|
456
|
-
startedAt
|
|
580
|
+
startedAt,
|
|
457
581
|
requestDataKeys: input.requestDataKeys || [],
|
|
458
582
|
requestData: input.requestData !== undefined ? summarizeCaptureValue(input.requestData, trace.capture, 'requestData') : undefined,
|
|
459
583
|
requestDataJson: input.requestData !== undefined ? serializeCaptureValue(input.requestData, 'requestData') : undefined,
|
|
@@ -461,7 +585,7 @@ export default class Trace {
|
|
|
461
585
|
};
|
|
462
586
|
|
|
463
587
|
trace.calls.push(call);
|
|
464
|
-
return
|
|
588
|
+
return callId;
|
|
465
589
|
}
|
|
466
590
|
|
|
467
591
|
public finishCall(
|
|
@@ -476,12 +600,24 @@ export default class Trace {
|
|
|
476
600
|
) {
|
|
477
601
|
if (!callId) return;
|
|
478
602
|
|
|
479
|
-
const
|
|
480
|
-
const
|
|
481
|
-
if (!
|
|
603
|
+
const record = this.getRecord(requestId);
|
|
604
|
+
const profilingCall = record?.profiling.apiCalls.find((candidate) => candidate.id === callId);
|
|
605
|
+
if (!record || !profilingCall) return;
|
|
606
|
+
|
|
607
|
+
profilingCall.finishedAt = nowIso();
|
|
608
|
+
profilingCall.durationMs = Math.max(0, Date.parse(profilingCall.finishedAt) - Date.parse(profilingCall.startedAt));
|
|
609
|
+
profilingCall.statusCode = output.statusCode;
|
|
610
|
+
profilingCall.errorMessage = output.errorMessage;
|
|
611
|
+
profilingCall.responseBodyJson = output.result !== undefined ? serializeRawCaptureValue(output.result, 'result') : undefined;
|
|
482
612
|
|
|
483
|
-
|
|
484
|
-
|
|
613
|
+
const trace = record.trace;
|
|
614
|
+
if (!trace) return;
|
|
615
|
+
|
|
616
|
+
const call = trace.calls.find((candidate) => candidate.id === callId);
|
|
617
|
+
if (!call) return;
|
|
618
|
+
|
|
619
|
+
call.finishedAt = profilingCall.finishedAt;
|
|
620
|
+
call.durationMs = profilingCall.durationMs;
|
|
485
621
|
call.statusCode = output.statusCode;
|
|
486
622
|
call.errorMessage = output.errorMessage;
|
|
487
623
|
call.resultKeys = output.resultKeys || [];
|
|
@@ -490,7 +626,7 @@ export default class Trace {
|
|
|
490
626
|
}
|
|
491
627
|
|
|
492
628
|
public setRequestResult(requestId: string, result: TTraceInspectable) {
|
|
493
|
-
const trace = this.
|
|
629
|
+
const trace = this.getRecord(requestId)?.trace;
|
|
494
630
|
if (!trace) return;
|
|
495
631
|
|
|
496
632
|
trace.resultJson = serializeCaptureValue(result, 'result');
|
|
@@ -520,8 +656,8 @@ export default class Trace {
|
|
|
520
656
|
target?: string;
|
|
521
657
|
},
|
|
522
658
|
) {
|
|
523
|
-
const
|
|
524
|
-
if (!
|
|
659
|
+
const record = this.getRecord(requestId);
|
|
660
|
+
if (!record) return;
|
|
525
661
|
const channel = this.getContextChannel();
|
|
526
662
|
|
|
527
663
|
const durationMs = Math.max(0, input.durationMs || 0);
|
|
@@ -530,10 +666,41 @@ export default class Trace {
|
|
|
530
666
|
const startedAt =
|
|
531
667
|
Number.isFinite(finishedAtMs) && durationMs > 0 ? new Date(finishedAtMs - durationMs).toISOString() : finishedAt;
|
|
532
668
|
const fingerprint = this.createSqlFingerprint(input.query);
|
|
669
|
+
const normalizedQuery = this.normalizeSqlQuery(input.query) || undefined;
|
|
533
670
|
const inferredServiceLabel = input.serviceLabel || channel?.serviceLabel || this.inferServiceLabelFromStack(new Error().stack);
|
|
534
671
|
|
|
672
|
+
const profilingQuery: TRequestProfilingSqlQuery = {
|
|
673
|
+
id: `${requestId}:sql:${record.profiling.sqlQueries.length}`,
|
|
674
|
+
callerCallId: input.callerCallId,
|
|
675
|
+
callerFetcherId: input.callerFetcherId,
|
|
676
|
+
callerLabel: input.callerLabel,
|
|
677
|
+
callerMethod: input.callerMethod || '',
|
|
678
|
+
callerOrigin: input.callerOrigin || 'request',
|
|
679
|
+
callerPath: input.callerPath || '',
|
|
680
|
+
durationMs,
|
|
681
|
+
finishedAt,
|
|
682
|
+
kind: input.kind,
|
|
683
|
+
model: input.model,
|
|
684
|
+
operation: input.operation,
|
|
685
|
+
fingerprint,
|
|
686
|
+
normalizedQuery,
|
|
687
|
+
ownerLabel: input.ownerLabel || channel?.ownerLabel,
|
|
688
|
+
ownerFilepath: input.ownerFilepath || channel?.ownerFilepath,
|
|
689
|
+
serviceLabel: inferredServiceLabel,
|
|
690
|
+
connectedNamespace: input.connectedNamespace || channel?.connectedNamespace,
|
|
691
|
+
paramsJson: input.paramsJson,
|
|
692
|
+
paramsText: input.paramsText,
|
|
693
|
+
query: input.query.trim(),
|
|
694
|
+
startedAt,
|
|
695
|
+
target: input.target,
|
|
696
|
+
};
|
|
697
|
+
|
|
698
|
+
record.profiling.sqlQueries.push(profilingQuery);
|
|
699
|
+
const trace = record.trace;
|
|
700
|
+
if (!trace) return;
|
|
701
|
+
|
|
535
702
|
const sqlQuery: TTraceSqlQuery = {
|
|
536
|
-
id:
|
|
703
|
+
id: profilingQuery.id,
|
|
537
704
|
callerCallId: input.callerCallId,
|
|
538
705
|
callerFetcherId: input.callerFetcherId,
|
|
539
706
|
callerLabel: input.callerLabel,
|
|
@@ -546,6 +713,7 @@ export default class Trace {
|
|
|
546
713
|
model: input.model,
|
|
547
714
|
operation: input.operation,
|
|
548
715
|
fingerprint,
|
|
716
|
+
normalizedQuery,
|
|
549
717
|
ownerLabel: input.ownerLabel || channel?.ownerLabel,
|
|
550
718
|
ownerFilepath: input.ownerFilepath || channel?.ownerFilepath,
|
|
551
719
|
serviceLabel: inferredServiceLabel,
|
|
@@ -564,7 +732,7 @@ export default class Trace {
|
|
|
564
732
|
return [...this.order]
|
|
565
733
|
.reverse()
|
|
566
734
|
.slice(0, limit)
|
|
567
|
-
.map((requestId) => this.
|
|
735
|
+
.map((requestId) => this.getRecord(requestId)?.trace)
|
|
568
736
|
.filter((trace): trace is TRequestTrace => trace !== undefined)
|
|
569
737
|
.map((trace) => ({
|
|
570
738
|
id: trace.id,
|
|
@@ -593,21 +761,25 @@ export default class Trace {
|
|
|
593
761
|
return [...this.order]
|
|
594
762
|
.reverse()
|
|
595
763
|
.slice(0, Math.max(1, limit))
|
|
596
|
-
.map((requestId) => this.
|
|
764
|
+
.map((requestId) => this.getRecord(requestId)?.trace)
|
|
597
765
|
.filter((trace): trace is TRequestTrace => trace !== undefined);
|
|
598
766
|
}
|
|
599
767
|
|
|
600
768
|
public getLatestRequest() {
|
|
601
769
|
const latestRequestId = this.order[this.order.length - 1];
|
|
602
|
-
return latestRequestId ? this.
|
|
770
|
+
return latestRequestId ? this.getRecord(latestRequestId)?.trace : undefined;
|
|
603
771
|
}
|
|
604
772
|
|
|
605
773
|
public getRequest(requestId: string) {
|
|
606
|
-
return this.
|
|
774
|
+
return this.getRecord(requestId)?.trace;
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
public getProfiling(requestId: string) {
|
|
778
|
+
return this.getRecord(requestId)?.profiling;
|
|
607
779
|
}
|
|
608
780
|
|
|
609
781
|
public exportRequest(requestId: string, filepath?: string) {
|
|
610
|
-
const trace = this.
|
|
782
|
+
const trace = this.getRecord(requestId)?.trace;
|
|
611
783
|
if (!trace) throw new Error(`Trace ${requestId} was not found.`);
|
|
612
784
|
|
|
613
785
|
const outputFilepath =
|
|
@@ -622,6 +794,15 @@ export default class Trace {
|
|
|
622
794
|
return outputFilepath;
|
|
623
795
|
}
|
|
624
796
|
|
|
797
|
+
public releaseRequest(requestId: string) {
|
|
798
|
+
const record = this.getRecord(requestId);
|
|
799
|
+
if (!record) return;
|
|
800
|
+
if (record.trace) return;
|
|
801
|
+
|
|
802
|
+
this.requests.delete(requestId);
|
|
803
|
+
this.activeMeasurements.delete(requestId);
|
|
804
|
+
}
|
|
805
|
+
|
|
625
806
|
private trimRequestBuffer() {
|
|
626
807
|
const overflow = this.order.length - this.config.requestsLimit;
|
|
627
808
|
if (overflow <= 0) return;
|
|
@@ -46,6 +46,12 @@ type TPrismaExtensionOperation = {
|
|
|
46
46
|
query: (args: unknown) => Promise<unknown>;
|
|
47
47
|
};
|
|
48
48
|
|
|
49
|
+
declare global {
|
|
50
|
+
interface BigInt {
|
|
51
|
+
toJSON: () => number | string;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
49
55
|
/*----------------------------------
|
|
50
56
|
- HELPERS
|
|
51
57
|
----------------------------------*/
|
|
@@ -173,21 +179,18 @@ export default class ModelsManager extends Service<Config, Hooks, Application, A
|
|
|
173
179
|
'DATABASE_URL is required before starting the Models service. Prisma 7 no longer auto-loads runtime env files.',
|
|
174
180
|
);
|
|
175
181
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
log: [{ emit: 'event', level: 'query' }],
|
|
181
|
-
})
|
|
182
|
-
: new PrismaClient({
|
|
183
|
-
adapter: createMariaDbAdapter(databaseUrl),
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
if (!shouldTraceQueries) {
|
|
187
|
-
this.client = prismaClient;
|
|
182
|
+
if (!this.app.container.Trace.shouldInstrumentRequests()) {
|
|
183
|
+
this.client = new PrismaClient({
|
|
184
|
+
adapter: createMariaDbAdapter(databaseUrl),
|
|
185
|
+
});
|
|
188
186
|
return;
|
|
189
187
|
}
|
|
190
188
|
|
|
189
|
+
const prismaClient = new PrismaClient({
|
|
190
|
+
adapter: createMariaDbAdapter(databaseUrl),
|
|
191
|
+
log: [{ emit: 'event', level: 'query' }],
|
|
192
|
+
});
|
|
193
|
+
|
|
191
194
|
prismaClient.$on('query', (event: TPrismaQueryEvent) => this.traceQuery(event));
|
|
192
195
|
|
|
193
196
|
this.client = prismaClient.$extends(
|
|
@@ -510,7 +510,7 @@ export default class HttpServer<TRouter extends TServerRouter = TServerRouter> {
|
|
|
510
510
|
private registerDevTraceRoutes(routes: express.Express) {
|
|
511
511
|
if (!__DEV__ || this.app.env.profile !== 'dev') return;
|
|
512
512
|
|
|
513
|
-
if (this.app.container.Trace.
|
|
513
|
+
if (this.app.container.Trace.isDevTraceEnabled()) {
|
|
514
514
|
routes.get('/__proteum/trace/requests', (req, res) => {
|
|
515
515
|
const rawLimit = Array.isArray(req.query.limit) ? req.query.limit[0] : req.query.limit;
|
|
516
516
|
const parsedLimit = typeof rawLimit === 'string' ? Number.parseInt(rawLimit, 10) : NaN;
|