proteum 2.1.2 → 2.1.6
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 +22 -14
- package/README.md +112 -17
- package/agents/project/AGENTS.md +188 -25
- package/agents/project/CODING_STYLE.md +1 -0
- package/agents/project/client/AGENTS.md +13 -8
- package/agents/project/client/pages/AGENTS.md +17 -9
- package/agents/project/diagnostics.md +52 -0
- package/agents/project/optimizations.md +48 -0
- package/agents/project/server/routes/AGENTS.md +9 -6
- package/agents/project/server/services/AGENTS.md +10 -6
- package/agents/project/tests/AGENTS.md +11 -5
- package/cli/app/config.ts +13 -14
- package/cli/app/index.ts +58 -0
- package/cli/commands/command.ts +8 -0
- package/cli/commands/connect.ts +45 -0
- package/cli/commands/dev.ts +26 -11
- package/cli/commands/diagnose.ts +286 -0
- package/cli/commands/doctor.ts +18 -5
- package/cli/commands/explain.ts +25 -0
- package/cli/commands/perf.ts +243 -0
- package/cli/commands/session.ts +254 -0
- package/cli/commands/sessionLocalRunner.js +188 -0
- package/cli/commands/trace.ts +17 -1
- package/cli/commands/verify.ts +281 -0
- package/cli/compiler/artifacts/connectedProjects.ts +453 -0
- package/cli/compiler/artifacts/controllers.ts +198 -49
- package/cli/compiler/artifacts/discovery.ts +0 -34
- package/cli/compiler/artifacts/manifest.ts +90 -6
- package/cli/compiler/artifacts/routing.ts +2 -2
- package/cli/compiler/artifacts/services.ts +277 -130
- package/cli/compiler/client/index.ts +3 -0
- package/cli/compiler/common/files/style.ts +52 -0
- package/cli/compiler/common/generatedRouteModules.ts +34 -5
- package/cli/compiler/common/scripts.ts +11 -5
- package/cli/compiler/index.ts +2 -1
- package/cli/compiler/server/index.ts +3 -0
- package/cli/presentation/commands.ts +136 -7
- package/cli/presentation/devSession.ts +32 -7
- package/cli/runtime/commands.ts +193 -6
- package/cli/scaffold/index.ts +14 -25
- package/cli/scaffold/templates.ts +41 -27
- package/cli/utils/agents.ts +4 -2
- package/cli/utils/keyboard.ts +8 -0
- package/client/dev/profiler/ApexChart.tsx +66 -0
- package/client/dev/profiler/index.tsx +2798 -417
- package/client/dev/profiler/runtime.noop.ts +12 -0
- package/client/dev/profiler/runtime.ts +195 -4
- package/client/services/router/request/api.ts +6 -1
- package/common/applicationConfig.ts +173 -0
- package/common/applicationConfigLoader.ts +102 -0
- package/common/connectedProjects.ts +113 -0
- package/common/dev/connect.ts +267 -0
- package/common/dev/console.ts +31 -0
- package/common/dev/contractsDoctor.ts +128 -0
- package/common/dev/diagnostics.ts +59 -15
- package/common/dev/inspection.ts +491 -0
- package/common/dev/performance.ts +809 -0
- package/common/dev/profiler.ts +3 -0
- package/common/dev/proteumManifest.ts +31 -6
- package/common/dev/requestTrace.ts +56 -1
- package/common/dev/session.ts +24 -0
- package/common/env/proteumEnv.ts +176 -50
- package/common/router/index.ts +1 -0
- package/common/router/request/api.ts +2 -0
- package/config.ts +5 -0
- package/docs/dev-commands.md +5 -1
- package/docs/dev-sessions.md +90 -0
- package/docs/diagnostics.md +74 -11
- package/docs/request-tracing.md +50 -3
- package/package.json +1 -1
- package/server/app/container/config.ts +16 -87
- package/server/app/container/console/index.ts +42 -8
- package/server/app/container/index.ts +3 -1
- package/server/app/container/trace/index.ts +153 -0
- package/server/app/devDiagnostics.ts +138 -0
- package/server/app/index.ts +18 -8
- package/server/app/service/container.ts +0 -12
- package/server/app/service/index.ts +0 -2
- package/server/services/prisma/index.ts +121 -4
- package/server/services/router/http/index.ts +352 -0
- package/server/services/router/index.ts +50 -47
- package/server/services/router/request/api.ts +160 -19
- package/server/services/router/request/index.ts +8 -0
- package/server/services/router/response/index.ts +24 -1
- package/server/services/router/response/page/document.tsx +5 -0
- package/server/services/router/response/page/index.tsx +10 -0
- package/agents/framework/AGENTS.md +0 -177
- package/server/services/auth/router/service.json +0 -6
- package/server/services/auth/service.json +0 -6
- package/server/services/cron/service.json +0 -6
- package/server/services/disks/drivers/local/service.json +0 -6
- package/server/services/disks/drivers/s3/service.json +0 -6
- package/server/services/disks/service.json +0 -6
- package/server/services/fetch/service.json +0 -7
- package/server/services/prisma/service.json +0 -6
- package/server/services/router/service.json +0 -6
- package/server/services/schema/router/service.json +0 -6
- package/server/services/schema/service.json +0 -6
- package/server/services/security/encrypt/aes/service.json +0 -6
|
@@ -9,8 +9,12 @@ import {
|
|
|
9
9
|
type TTraceCallOrigin,
|
|
10
10
|
type TTraceEvent,
|
|
11
11
|
type TTraceEventType,
|
|
12
|
+
type TTraceSqlQuery,
|
|
13
|
+
type TTraceSqlQueryCallerOrigin,
|
|
14
|
+
type TTraceSqlQueryKind,
|
|
12
15
|
type TTraceSummaryValue,
|
|
13
16
|
type TRequestTrace,
|
|
17
|
+
type TTraceMemorySnapshot,
|
|
14
18
|
type TRequestTraceListItem,
|
|
15
19
|
} from '@common/dev/requestTrace';
|
|
16
20
|
|
|
@@ -38,6 +42,44 @@ const isSensitiveKeyPath = (keyPath: string[]) => sensitiveKeyPattern.test(keyPa
|
|
|
38
42
|
const summarizeString = (value: string) =>
|
|
39
43
|
value.length <= maxStringLength ? value : `${value.slice(0, maxStringLength)}…`;
|
|
40
44
|
|
|
45
|
+
const serializeJsonValue = (value: unknown, keyPath: string[], seen: WeakSet<object>): unknown => {
|
|
46
|
+
if (isSensitiveKeyPath(keyPath)) return `[redacted: Sensitive key ${keyPath[keyPath.length - 1] || 'value'}]`;
|
|
47
|
+
if (value === undefined || value === null) return value;
|
|
48
|
+
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') return value;
|
|
49
|
+
if (typeof value === 'bigint') return `${value.toString()}n`;
|
|
50
|
+
if (typeof value === 'symbol') return value.toString();
|
|
51
|
+
if (typeof value === 'function') return `[Function ${value.name || 'anonymous'}]`;
|
|
52
|
+
|
|
53
|
+
if (value instanceof Date) return value.toISOString();
|
|
54
|
+
if (value instanceof Error) return { name: value.name, message: value.message, stack: value.stack };
|
|
55
|
+
if (Buffer.isBuffer(value)) return `[Buffer ${value.byteLength} bytes]`;
|
|
56
|
+
if (value instanceof Map) return Array.from(value.entries()).map(([entryKey, entryValue], index) =>
|
|
57
|
+
serializeJsonValue([entryKey, entryValue], [...keyPath, `[${index}]`], seen),
|
|
58
|
+
);
|
|
59
|
+
if (value instanceof Set) {
|
|
60
|
+
return Array.from(value.values()).map((entryValue, index) => serializeJsonValue(entryValue, [...keyPath, `[${index}]`], seen));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (typeof value !== 'object') return String(value);
|
|
64
|
+
if (seen.has(value)) return `[Circular ${value.constructor?.name || 'Object'}]`;
|
|
65
|
+
|
|
66
|
+
seen.add(value);
|
|
67
|
+
|
|
68
|
+
if (Array.isArray(value)) {
|
|
69
|
+
return value.map((item, index) => serializeJsonValue(item, [...keyPath, `[${index}]`], seen));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const serialized: Record<string, unknown> = {};
|
|
73
|
+
for (const [entryKey, entryValue] of Object.entries(value)) {
|
|
74
|
+
const nextValue = serializeJsonValue(entryValue, [...keyPath, entryKey], seen);
|
|
75
|
+
if (nextValue !== undefined) serialized[entryKey] = nextValue;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return serialized;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const serializeCaptureValue = (value: TTraceInspectable, key: string) => serializeJsonValue(value, [key], new WeakSet<object>());
|
|
82
|
+
|
|
41
83
|
const summarizeError = (error: Error): TTraceSummaryValue => ({
|
|
42
84
|
kind: 'error',
|
|
43
85
|
name: error.name,
|
|
@@ -118,11 +160,29 @@ const summarizeCaptureValue = (value: TTraceInspectable, capture: TTraceCaptureM
|
|
|
118
160
|
summarizeValue(value, capture === 'deep' ? 3 : 1, new WeakSet<object>(), [key]);
|
|
119
161
|
|
|
120
162
|
const nowIso = () => new Date().toISOString();
|
|
163
|
+
const snapshotMemory = (): TTraceMemorySnapshot => {
|
|
164
|
+
const usage = process.memoryUsage();
|
|
165
|
+
|
|
166
|
+
return {
|
|
167
|
+
arrayBuffers: typeof usage.arrayBuffers === 'number' ? usage.arrayBuffers : 0,
|
|
168
|
+
external: usage.external,
|
|
169
|
+
heapTotal: usage.heapTotal,
|
|
170
|
+
heapUsed: usage.heapUsed,
|
|
171
|
+
rss: usage.rss,
|
|
172
|
+
};
|
|
173
|
+
};
|
|
121
174
|
|
|
122
175
|
export default class Trace {
|
|
123
176
|
private requests = new Map<string, TRequestTrace>();
|
|
124
177
|
private order: string[] = [];
|
|
125
178
|
private armedCapture?: TTraceCaptureMode;
|
|
179
|
+
private activeMeasurements = new Map<
|
|
180
|
+
string,
|
|
181
|
+
{
|
|
182
|
+
cpu: ReturnType<typeof process.cpuUsage>;
|
|
183
|
+
memory: TTraceMemorySnapshot;
|
|
184
|
+
}
|
|
185
|
+
>();
|
|
126
186
|
|
|
127
187
|
public constructor(
|
|
128
188
|
private container: typeof ApplicationContainer,
|
|
@@ -169,11 +229,14 @@ export default class Trace {
|
|
|
169
229
|
profilerParentRequestId: input.profilerParentRequestId,
|
|
170
230
|
startedAt: nowIso(),
|
|
171
231
|
droppedEvents: 0,
|
|
232
|
+
requestDataJson: serializeCaptureValue(input.data, 'requestData'),
|
|
172
233
|
calls: [],
|
|
234
|
+
sqlQueries: [],
|
|
173
235
|
events: [],
|
|
174
236
|
};
|
|
175
237
|
|
|
176
238
|
this.requests.set(trace.id, trace);
|
|
239
|
+
this.activeMeasurements.set(trace.id, { cpu: process.cpuUsage(), memory: snapshotMemory() });
|
|
177
240
|
this.order.push(trace.id);
|
|
178
241
|
this.trimRequestBuffer();
|
|
179
242
|
|
|
@@ -226,6 +289,21 @@ export default class Trace {
|
|
|
226
289
|
if (output.user) trace.user = output.user;
|
|
227
290
|
trace.statusCode = output.statusCode;
|
|
228
291
|
trace.errorMessage = output.errorMessage;
|
|
292
|
+
const measurement = this.activeMeasurements.get(requestId);
|
|
293
|
+
if (measurement) {
|
|
294
|
+
const cpu = process.cpuUsage(measurement.cpu);
|
|
295
|
+
trace.performance = {
|
|
296
|
+
cpu: {
|
|
297
|
+
systemMicros: cpu.system,
|
|
298
|
+
userMicros: cpu.user,
|
|
299
|
+
},
|
|
300
|
+
memory: {
|
|
301
|
+
after: snapshotMemory(),
|
|
302
|
+
before: measurement.memory,
|
|
303
|
+
},
|
|
304
|
+
};
|
|
305
|
+
this.activeMeasurements.delete(requestId);
|
|
306
|
+
}
|
|
229
307
|
|
|
230
308
|
this.record(
|
|
231
309
|
requestId,
|
|
@@ -250,6 +328,8 @@ export default class Trace {
|
|
|
250
328
|
method?: string;
|
|
251
329
|
path?: string;
|
|
252
330
|
fetcherId?: string;
|
|
331
|
+
connectedProjectNamespace?: string;
|
|
332
|
+
connectedControllerAccessor?: string;
|
|
253
333
|
parentId?: string;
|
|
254
334
|
requestDataKeys?: string[];
|
|
255
335
|
requestData?: TTraceInspectable;
|
|
@@ -266,9 +346,12 @@ export default class Trace {
|
|
|
266
346
|
method: input.method || '',
|
|
267
347
|
path: input.path || '',
|
|
268
348
|
fetcherId: input.fetcherId,
|
|
349
|
+
connectedProjectNamespace: input.connectedProjectNamespace,
|
|
350
|
+
connectedControllerAccessor: input.connectedControllerAccessor,
|
|
269
351
|
startedAt: nowIso(),
|
|
270
352
|
requestDataKeys: input.requestDataKeys || [],
|
|
271
353
|
requestData: input.requestData !== undefined ? summarizeCaptureValue(input.requestData, trace.capture, 'requestData') : undefined,
|
|
354
|
+
requestDataJson: input.requestData !== undefined ? serializeCaptureValue(input.requestData, 'requestData') : undefined,
|
|
272
355
|
resultKeys: [],
|
|
273
356
|
};
|
|
274
357
|
|
|
@@ -298,6 +381,66 @@ export default class Trace {
|
|
|
298
381
|
call.errorMessage = output.errorMessage;
|
|
299
382
|
call.resultKeys = output.resultKeys || [];
|
|
300
383
|
call.result = output.result !== undefined ? summarizeCaptureValue(output.result, trace.capture, 'result') : undefined;
|
|
384
|
+
call.resultJson = output.result !== undefined ? serializeCaptureValue(output.result, 'result') : undefined;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
public setRequestResult(requestId: string, result: TTraceInspectable) {
|
|
388
|
+
const trace = this.requests.get(requestId);
|
|
389
|
+
if (!trace) return;
|
|
390
|
+
|
|
391
|
+
trace.resultJson = serializeCaptureValue(result, 'result');
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
public recordSqlQuery(
|
|
395
|
+
requestId: string,
|
|
396
|
+
input: {
|
|
397
|
+
callerCallId?: string;
|
|
398
|
+
callerFetcherId?: string;
|
|
399
|
+
callerLabel?: string;
|
|
400
|
+
callerMethod?: string;
|
|
401
|
+
callerOrigin?: TTraceSqlQueryCallerOrigin;
|
|
402
|
+
callerPath?: string;
|
|
403
|
+
durationMs?: number;
|
|
404
|
+
finishedAt?: string;
|
|
405
|
+
kind: TTraceSqlQueryKind;
|
|
406
|
+
model?: string;
|
|
407
|
+
operation: string;
|
|
408
|
+
paramsJson?: unknown;
|
|
409
|
+
paramsText?: string;
|
|
410
|
+
query: string;
|
|
411
|
+
target?: string;
|
|
412
|
+
},
|
|
413
|
+
) {
|
|
414
|
+
const trace = this.requests.get(requestId);
|
|
415
|
+
if (!trace) return;
|
|
416
|
+
|
|
417
|
+
const durationMs = Math.max(0, input.durationMs || 0);
|
|
418
|
+
const finishedAt = input.finishedAt || nowIso();
|
|
419
|
+
const finishedAtMs = Date.parse(finishedAt);
|
|
420
|
+
const startedAt =
|
|
421
|
+
Number.isFinite(finishedAtMs) && durationMs > 0 ? new Date(finishedAtMs - durationMs).toISOString() : finishedAt;
|
|
422
|
+
|
|
423
|
+
const sqlQuery: TTraceSqlQuery = {
|
|
424
|
+
id: `${requestId}:sql:${trace.sqlQueries.length}`,
|
|
425
|
+
callerCallId: input.callerCallId,
|
|
426
|
+
callerFetcherId: input.callerFetcherId,
|
|
427
|
+
callerLabel: input.callerLabel,
|
|
428
|
+
callerMethod: input.callerMethod || '',
|
|
429
|
+
callerOrigin: input.callerOrigin || 'request',
|
|
430
|
+
callerPath: input.callerPath || '',
|
|
431
|
+
durationMs,
|
|
432
|
+
finishedAt,
|
|
433
|
+
kind: input.kind,
|
|
434
|
+
model: input.model,
|
|
435
|
+
operation: input.operation,
|
|
436
|
+
paramsJson: input.paramsJson,
|
|
437
|
+
paramsText: input.paramsText,
|
|
438
|
+
query: input.query.trim(),
|
|
439
|
+
startedAt,
|
|
440
|
+
target: input.target,
|
|
441
|
+
};
|
|
442
|
+
|
|
443
|
+
trace.sqlQueries.push(sqlQuery);
|
|
301
444
|
}
|
|
302
445
|
|
|
303
446
|
public listRequests(limit = 20): TRequestTraceListItem[] {
|
|
@@ -325,9 +468,18 @@ export default class Trace {
|
|
|
325
468
|
profilerParentRequestId: trace.profilerParentRequestId,
|
|
326
469
|
eventCount: trace.events.length,
|
|
327
470
|
callCount: trace.calls.length,
|
|
471
|
+
sqlQueryCount: trace.sqlQueries.length,
|
|
328
472
|
}));
|
|
329
473
|
}
|
|
330
474
|
|
|
475
|
+
public listTraceRequests(limit = this.config.requestsLimit) {
|
|
476
|
+
return [...this.order]
|
|
477
|
+
.reverse()
|
|
478
|
+
.slice(0, Math.max(1, limit))
|
|
479
|
+
.map((requestId) => this.requests.get(requestId))
|
|
480
|
+
.filter((trace): trace is TRequestTrace => trace !== undefined);
|
|
481
|
+
}
|
|
482
|
+
|
|
331
483
|
public getLatestRequest() {
|
|
332
484
|
const latestRequestId = this.order[this.order.length - 1];
|
|
333
485
|
return latestRequestId ? this.requests.get(latestRequestId) : undefined;
|
|
@@ -359,6 +511,7 @@ export default class Trace {
|
|
|
359
511
|
|
|
360
512
|
for (const requestId of this.order.splice(0, overflow)) {
|
|
361
513
|
this.requests.delete(requestId);
|
|
514
|
+
this.activeMeasurements.delete(requestId);
|
|
362
515
|
}
|
|
363
516
|
}
|
|
364
517
|
}
|
|
@@ -2,6 +2,7 @@ import fs from 'fs-extra';
|
|
|
2
2
|
import path from 'path';
|
|
3
3
|
|
|
4
4
|
import type { Application } from './index';
|
|
5
|
+
import type { TDevConsoleLogLevel, TDevConsoleLogsResponse } from '@common/dev/console';
|
|
5
6
|
import {
|
|
6
7
|
buildDoctorResponse,
|
|
7
8
|
explainSectionNames,
|
|
@@ -9,10 +10,31 @@ import {
|
|
|
9
10
|
type TDoctorResponse,
|
|
10
11
|
type TExplainSectionName,
|
|
11
12
|
} from '@common/dev/diagnostics';
|
|
13
|
+
import { buildContractsDoctorResponse } from '@common/dev/contractsDoctor';
|
|
14
|
+
import {
|
|
15
|
+
buildPerfCompareResponse,
|
|
16
|
+
buildPerfMemoryResponse,
|
|
17
|
+
buildPerfTopResponse,
|
|
18
|
+
resolvePerfRequest,
|
|
19
|
+
type TPerfCompareResponse,
|
|
20
|
+
type TPerfGroupBy,
|
|
21
|
+
type TPerfMemoryResponse,
|
|
22
|
+
type TPerfRequestResponse,
|
|
23
|
+
type TPerfTopResponse,
|
|
24
|
+
} from '@common/dev/performance';
|
|
25
|
+
import {
|
|
26
|
+
buildDiagnoseResponse,
|
|
27
|
+
explainOwner,
|
|
28
|
+
type TDiagnoseResponse,
|
|
29
|
+
type TExplainOwnerResponse,
|
|
30
|
+
} from '@common/dev/inspection';
|
|
12
31
|
import type { TProteumManifest } from '@common/dev/proteumManifest';
|
|
32
|
+
import type { TRequestTrace } from '@common/dev/requestTrace';
|
|
13
33
|
|
|
14
34
|
const isExplainSectionName = (value: string): value is TExplainSectionName =>
|
|
15
35
|
explainSectionNames.includes(value as TExplainSectionName);
|
|
36
|
+
const isConsoleLogLevel = (value: string): value is TDevConsoleLogLevel =>
|
|
37
|
+
['silly', 'log', 'info', 'warn', 'error'].includes(value);
|
|
16
38
|
|
|
17
39
|
export default class DevDiagnosticsRegistry<TApplication extends Application = Application> {
|
|
18
40
|
public constructor(private app: TApplication) {}
|
|
@@ -50,4 +72,120 @@ export default class DevDiagnosticsRegistry<TApplication extends Application = A
|
|
|
50
72
|
public doctor(strict = false): TDoctorResponse {
|
|
51
73
|
return buildDoctorResponse(this.readManifest(), strict);
|
|
52
74
|
}
|
|
75
|
+
|
|
76
|
+
public doctorContracts(strict = false): TDoctorResponse {
|
|
77
|
+
return buildContractsDoctorResponse(this.readManifest(), strict);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
public explainOwner(query: string): TExplainOwnerResponse {
|
|
81
|
+
const normalizedQuery = query.trim();
|
|
82
|
+
if (!normalizedQuery) throw new Error('Owner query is required.');
|
|
83
|
+
|
|
84
|
+
return explainOwner(this.readManifest(), normalizedQuery);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
public readLogs(limit = 100, minimumLevel: TDevConsoleLogLevel = 'log'): TDevConsoleLogsResponse {
|
|
88
|
+
return { logs: this.app.container.Console.listLogs(limit, isConsoleLogLevel(minimumLevel) ? minimumLevel : 'log') };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
private resolveRequestTrace({ path, requestId }: { path?: string; requestId?: string }): TRequestTrace | undefined {
|
|
92
|
+
if (requestId) return this.app.container.Trace.getRequest(requestId);
|
|
93
|
+
if (!path) return this.app.container.Trace.getLatestRequest();
|
|
94
|
+
|
|
95
|
+
const match = this.app.container.Trace.listRequests(200).find((request) => request.path === path);
|
|
96
|
+
return match ? this.app.container.Trace.getRequest(match.id) : undefined;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
private readPerfRequests() {
|
|
100
|
+
return this.app.container.Trace.listTraceRequests(Number.MAX_SAFE_INTEGER);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
public diagnose({
|
|
104
|
+
logsLevel = 'warn',
|
|
105
|
+
logsLimit = 40,
|
|
106
|
+
path,
|
|
107
|
+
query,
|
|
108
|
+
requestId,
|
|
109
|
+
strict = false,
|
|
110
|
+
}: {
|
|
111
|
+
logsLevel?: TDevConsoleLogLevel;
|
|
112
|
+
logsLimit?: number;
|
|
113
|
+
path?: string;
|
|
114
|
+
query?: string;
|
|
115
|
+
requestId?: string;
|
|
116
|
+
strict?: boolean;
|
|
117
|
+
} = {}): TDiagnoseResponse {
|
|
118
|
+
const manifest = this.readManifest();
|
|
119
|
+
const request = this.resolveRequestTrace({ path, requestId });
|
|
120
|
+
const resolvedQuery = query?.trim() || path?.trim() || request?.path || requestId?.trim() || '';
|
|
121
|
+
|
|
122
|
+
if (!resolvedQuery) throw new Error('Diagnose requires a query, path, request id, or an existing latest request trace.');
|
|
123
|
+
|
|
124
|
+
return buildDiagnoseResponse({
|
|
125
|
+
contracts: buildContractsDoctorResponse(manifest, strict),
|
|
126
|
+
doctor: buildDoctorResponse(manifest, strict),
|
|
127
|
+
manifest,
|
|
128
|
+
query: resolvedQuery,
|
|
129
|
+
request,
|
|
130
|
+
serverLogs: this.readLogs(logsLimit, logsLevel),
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
public perfTop({
|
|
135
|
+
groupBy = 'path',
|
|
136
|
+
limit = 12,
|
|
137
|
+
since = 'today',
|
|
138
|
+
}: {
|
|
139
|
+
groupBy?: TPerfGroupBy;
|
|
140
|
+
limit?: number;
|
|
141
|
+
since?: string;
|
|
142
|
+
} = {}): TPerfTopResponse {
|
|
143
|
+
return buildPerfTopResponse({
|
|
144
|
+
groupBy,
|
|
145
|
+
limit,
|
|
146
|
+
requests: this.readPerfRequests(),
|
|
147
|
+
since,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
public perfCompare({
|
|
152
|
+
baseline = 'yesterday',
|
|
153
|
+
groupBy = 'path',
|
|
154
|
+
limit = 12,
|
|
155
|
+
target = 'today',
|
|
156
|
+
}: {
|
|
157
|
+
baseline?: string;
|
|
158
|
+
groupBy?: TPerfGroupBy;
|
|
159
|
+
limit?: number;
|
|
160
|
+
target?: string;
|
|
161
|
+
} = {}): TPerfCompareResponse {
|
|
162
|
+
return buildPerfCompareResponse({
|
|
163
|
+
baseline,
|
|
164
|
+
groupBy,
|
|
165
|
+
limit,
|
|
166
|
+
requests: this.readPerfRequests(),
|
|
167
|
+
target,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
public perfMemory({
|
|
172
|
+
groupBy = 'path',
|
|
173
|
+
limit = 12,
|
|
174
|
+
since = 'today',
|
|
175
|
+
}: {
|
|
176
|
+
groupBy?: TPerfGroupBy;
|
|
177
|
+
limit?: number;
|
|
178
|
+
since?: string;
|
|
179
|
+
} = {}): TPerfMemoryResponse {
|
|
180
|
+
return buildPerfMemoryResponse({
|
|
181
|
+
groupBy,
|
|
182
|
+
limit,
|
|
183
|
+
requests: this.readPerfRequests(),
|
|
184
|
+
since,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
public perfRequest(requestIdOrPath: string): TPerfRequestResponse {
|
|
189
|
+
return { request: resolvePerfRequest(this.readPerfRequests(), requestIdOrPath) };
|
|
190
|
+
}
|
|
53
191
|
}
|
package/server/app/index.ts
CHANGED
|
@@ -10,12 +10,13 @@ import ApplicationService, { AnyService } from './service';
|
|
|
10
10
|
import CommandsManager from './commandsManager';
|
|
11
11
|
import DevCommandsRegistry from './devCommands';
|
|
12
12
|
import DevDiagnosticsRegistry from './devDiagnostics';
|
|
13
|
-
import ServicesContainer, { ServicesContainer as ServicesContainerClass
|
|
13
|
+
import ServicesContainer, { ServicesContainer as ServicesContainerClass } from './service/container';
|
|
14
14
|
|
|
15
15
|
// Built-in
|
|
16
16
|
import type { TServerRouter, Request as ServerRequest } from '@server/services/router';
|
|
17
17
|
import { Anomaly } from '@common/errors';
|
|
18
18
|
import { TBasicUser } from '@server/services/auth';
|
|
19
|
+
import { Application as ConfigApplication } from '@common/applicationConfig';
|
|
19
20
|
|
|
20
21
|
export { default as Services } from './service/container';
|
|
21
22
|
export type { ServiceConfig } from './service/container';
|
|
@@ -62,6 +63,9 @@ export abstract class Application<
|
|
|
62
63
|
TServicesContainer extends ServicesContainerClass = ServicesContainerClass,
|
|
63
64
|
TUser extends TBasicUser = TBasicUser,
|
|
64
65
|
> extends ApplicationService<Config, Hooks, Application, Application> {
|
|
66
|
+
public static identity = ConfigApplication.identity;
|
|
67
|
+
public static setup = ConfigApplication.setup;
|
|
68
|
+
|
|
65
69
|
public app!: this;
|
|
66
70
|
public servicesContainer!: TServicesContainer;
|
|
67
71
|
public userType!: TUser;
|
|
@@ -71,18 +75,13 @@ export abstract class Application<
|
|
|
71
75
|
----------------------------------*/
|
|
72
76
|
|
|
73
77
|
public side = 'server' as 'server';
|
|
74
|
-
public metas: TServiceMetas = {
|
|
75
|
-
id: 'application',
|
|
76
|
-
name: 'Application',
|
|
77
|
-
parent: 'root',
|
|
78
|
-
dependences: [],
|
|
79
|
-
class: () => ({ default: Application }),
|
|
80
|
-
};
|
|
81
78
|
|
|
82
79
|
// Shortcuts to ApplicationContainer
|
|
83
80
|
public container = AppContainer;
|
|
84
81
|
public env = AppContainer.Environment;
|
|
85
82
|
public identity = AppContainer.Identity;
|
|
83
|
+
public setup = AppContainer.Setup;
|
|
84
|
+
public connectedProjects = AppContainer.Environment.connectedProjects;
|
|
86
85
|
|
|
87
86
|
// Status
|
|
88
87
|
public debug: boolean = false;
|
|
@@ -195,6 +194,17 @@ export abstract class Application<
|
|
|
195
194
|
return rootServices[serviceName];
|
|
196
195
|
}
|
|
197
196
|
|
|
197
|
+
public getConnectedProject(namespace: string) {
|
|
198
|
+
return this.connectedProjects[namespace];
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
public requireConnectedProject(namespace: string) {
|
|
202
|
+
const connectedProject = this.getConnectedProject(namespace);
|
|
203
|
+
if (connectedProject) return connectedProject;
|
|
204
|
+
|
|
205
|
+
throw new Error(`Connected project "${namespace}" is not configured on ${this.identity.identifier}.`);
|
|
206
|
+
}
|
|
207
|
+
|
|
198
208
|
public register(service: AnyService) {
|
|
199
209
|
return (service as AnyService & { ready: () => Promise<any> }).ready();
|
|
200
210
|
}
|
|
@@ -9,15 +9,6 @@ import type { AnyService, AnyServiceClass, StartedServicesIndex } from '.';
|
|
|
9
9
|
- TYPES
|
|
10
10
|
----------------------------------*/
|
|
11
11
|
|
|
12
|
-
// From service/service.json
|
|
13
|
-
export type TServiceMetas<TServiceClass extends AnyService = AnyService> = {
|
|
14
|
-
id: string;
|
|
15
|
-
name: string;
|
|
16
|
-
parent: string;
|
|
17
|
-
dependences: string[];
|
|
18
|
-
class: () => { default: ClassType<TServiceClass> };
|
|
19
|
-
};
|
|
20
|
-
|
|
21
12
|
export type ServiceConfig<TServiceClass extends AnyServiceClass> = NonNullable<ConstructorParameters<TServiceClass>[1]>;
|
|
22
13
|
|
|
23
14
|
type ExactConfig<TValue, TShape> = TValue extends TShape
|
|
@@ -40,9 +31,6 @@ type ExactConfig<TValue, TShape> = TValue extends TShape
|
|
|
40
31
|
- CLASS
|
|
41
32
|
----------------------------------*/
|
|
42
33
|
export class ServicesContainer<TServicesIndex extends StartedServicesIndex = StartedServicesIndex> {
|
|
43
|
-
// All service instances by service id
|
|
44
|
-
public allServices: TServicesIndex = {} as TServicesIndex;
|
|
45
|
-
|
|
46
34
|
public config<TServiceClass extends AnyServiceClass, const TConfig extends ServiceConfig<TServiceClass>>(
|
|
47
35
|
_serviceClass: TServiceClass,
|
|
48
36
|
config: TConfig & ExactConfig<TConfig, ServiceConfig<TServiceClass>>,
|
|
@@ -5,7 +5,6 @@
|
|
|
5
5
|
// Specific
|
|
6
6
|
import type { Application } from '../index';
|
|
7
7
|
import type { Command } from '../commands';
|
|
8
|
-
import type { TServiceMetas } from './container';
|
|
9
8
|
import type { TRouterContext, TAnyRouter } from '../../services/router';
|
|
10
9
|
|
|
11
10
|
export { schema } from '../../services/router/request/validation/zod';
|
|
@@ -95,7 +94,6 @@ export default abstract class Service<
|
|
|
95
94
|
public status: 'stopped' | 'starting' | 'running' | 'paused' = 'starting';
|
|
96
95
|
|
|
97
96
|
public commands?: Command[];
|
|
98
|
-
public metas!: TServiceMetas;
|
|
99
97
|
public bindings: string[] = [];
|
|
100
98
|
|
|
101
99
|
public parent: TParent;
|
|
@@ -4,13 +4,15 @@
|
|
|
4
4
|
|
|
5
5
|
// Npm
|
|
6
6
|
import dotenv from 'dotenv';
|
|
7
|
-
import { PrismaClient } from '@generated/server/models';
|
|
7
|
+
import { Prisma, PrismaClient } from '@generated/server/models';
|
|
8
8
|
import mysql from 'mysql2/promise';
|
|
9
9
|
const safeStringify = require('fast-safe-stringify'); // remplace les références circulairs par un [Circular]
|
|
10
10
|
|
|
11
11
|
// Core
|
|
12
12
|
import type { Application } from '@server/app/index';
|
|
13
|
+
import type { ChannelInfos } from '@server/app/container/console';
|
|
13
14
|
import Service, { TServiceArgs } from '@server/app/service';
|
|
15
|
+
import context from '@server/context';
|
|
14
16
|
|
|
15
17
|
// Specific
|
|
16
18
|
import Facet, { TDelegate, TSubset, Transform } from './Facet';
|
|
@@ -29,6 +31,20 @@ type DecimalLike = {
|
|
|
29
31
|
toNumber: () => number;
|
|
30
32
|
toString: () => string;
|
|
31
33
|
};
|
|
34
|
+
type TPrismaOperationContext = { kind: 'orm' | 'raw'; model?: string; operation: string };
|
|
35
|
+
type TPrismaQueryEvent = {
|
|
36
|
+
duration: number;
|
|
37
|
+
params: string;
|
|
38
|
+
query: string;
|
|
39
|
+
target: string;
|
|
40
|
+
timestamp: Date;
|
|
41
|
+
};
|
|
42
|
+
type TPrismaExtensionOperation = {
|
|
43
|
+
args: unknown;
|
|
44
|
+
model?: string;
|
|
45
|
+
operation: string;
|
|
46
|
+
query: (args: unknown) => Promise<unknown>;
|
|
47
|
+
};
|
|
32
48
|
|
|
33
49
|
/*----------------------------------
|
|
34
50
|
- HELPERS
|
|
@@ -78,6 +94,51 @@ const normalizeSqlResult = <T>(value: T): T => {
|
|
|
78
94
|
Object.entries(value).map(([key, nestedValue]) => [key, normalizeSqlResult(nestedValue)]),
|
|
79
95
|
) as T;
|
|
80
96
|
};
|
|
97
|
+
const rawOperationNames = new Set([
|
|
98
|
+
'$executeRaw',
|
|
99
|
+
'$executeRawUnsafe',
|
|
100
|
+
'$queryRaw',
|
|
101
|
+
'$queryRawUnsafe',
|
|
102
|
+
'aggregateRaw',
|
|
103
|
+
'executeRaw',
|
|
104
|
+
'findRaw',
|
|
105
|
+
'queryRaw',
|
|
106
|
+
'runCommandRaw',
|
|
107
|
+
]);
|
|
108
|
+
|
|
109
|
+
const inferPrismaOperationKind = (model: string | undefined, operation: string): TPrismaOperationContext['kind'] =>
|
|
110
|
+
rawOperationNames.has(operation) || operation.toLowerCase().includes('raw') ? 'raw' : model ? 'orm' : 'raw';
|
|
111
|
+
|
|
112
|
+
const parseQueryParams = (value: string) => {
|
|
113
|
+
if (!value) return undefined;
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
return JSON.parse(value);
|
|
117
|
+
} catch (_error) {
|
|
118
|
+
return undefined;
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const withPrismaOperationContext = async <T>(meta: TPrismaOperationContext, execute: () => Promise<T>) => {
|
|
123
|
+
const store = context.getStore() as ChannelInfos | undefined;
|
|
124
|
+
if (!store) return execute();
|
|
125
|
+
|
|
126
|
+
const operations = store.prismaOperations || (store.prismaOperations = []);
|
|
127
|
+
operations.push(meta);
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
return await execute();
|
|
131
|
+
} finally {
|
|
132
|
+
const lastOperation = operations[operations.length - 1];
|
|
133
|
+
if (lastOperation === meta) operations.pop();
|
|
134
|
+
else {
|
|
135
|
+
const operationIndex = operations.lastIndexOf(meta);
|
|
136
|
+
if (operationIndex !== -1) operations.splice(operationIndex, 1);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (operations.length === 0) delete store.prismaOperations;
|
|
140
|
+
}
|
|
141
|
+
};
|
|
81
142
|
|
|
82
143
|
/*----------------------------------
|
|
83
144
|
- SERVICE CONFIG
|
|
@@ -112,9 +173,40 @@ export default class ModelsManager extends Service<Config, Hooks, Application, A
|
|
|
112
173
|
'DATABASE_URL is required before starting the Models service. Prisma 7 no longer auto-loads runtime env files.',
|
|
113
174
|
);
|
|
114
175
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
176
|
+
const shouldTraceQueries = this.app.container.Trace.isEnabled();
|
|
177
|
+
const prismaClient = shouldTraceQueries
|
|
178
|
+
? new PrismaClient({
|
|
179
|
+
adapter: createMariaDbAdapter(databaseUrl),
|
|
180
|
+
log: [{ emit: 'event', level: 'query' }],
|
|
181
|
+
})
|
|
182
|
+
: new PrismaClient({
|
|
183
|
+
adapter: createMariaDbAdapter(databaseUrl),
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
if (!shouldTraceQueries) {
|
|
187
|
+
this.client = prismaClient;
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
prismaClient.$on('query', (event: TPrismaQueryEvent) => this.traceQuery(event));
|
|
192
|
+
|
|
193
|
+
this.client = prismaClient.$extends(
|
|
194
|
+
Prisma.defineExtension({
|
|
195
|
+
query: {
|
|
196
|
+
async $allOperations({ args, model, operation, query }: TPrismaExtensionOperation) {
|
|
197
|
+
const normalizedModel = typeof model === 'string' ? model : undefined;
|
|
198
|
+
return withPrismaOperationContext(
|
|
199
|
+
{
|
|
200
|
+
kind: inferPrismaOperationKind(normalizedModel, operation),
|
|
201
|
+
model: normalizedModel,
|
|
202
|
+
operation,
|
|
203
|
+
},
|
|
204
|
+
() => query(args),
|
|
205
|
+
);
|
|
206
|
+
},
|
|
207
|
+
},
|
|
208
|
+
}),
|
|
209
|
+
) as PrismaClient;
|
|
118
210
|
}
|
|
119
211
|
|
|
120
212
|
public async ready() {
|
|
@@ -241,4 +333,29 @@ export default class ModelsManager extends Service<Config, Hooks, Application, A
|
|
|
241
333
|
public equalities = (data: TObjetDonnees, forStorage: boolean = false) => {
|
|
242
334
|
return Object.keys(data).map((k) => '' + k + ' = ' + this.esc(data[k], forStorage));
|
|
243
335
|
};
|
|
336
|
+
|
|
337
|
+
private traceQuery(event: TPrismaQueryEvent) {
|
|
338
|
+
const store = context.getStore() as ChannelInfos | undefined;
|
|
339
|
+
if (!store || store.channelType !== 'request' || !store.channelId) return;
|
|
340
|
+
|
|
341
|
+
const operation = store.prismaOperations?.[store.prismaOperations.length - 1];
|
|
342
|
+
|
|
343
|
+
this.app.container.Trace.recordSqlQuery(store.channelId, {
|
|
344
|
+
callerCallId: store.traceCallId,
|
|
345
|
+
callerFetcherId: store.traceCallFetcherId,
|
|
346
|
+
callerLabel: store.traceCallLabel,
|
|
347
|
+
callerMethod: store.method,
|
|
348
|
+
callerOrigin: store.traceCallOrigin || 'request',
|
|
349
|
+
callerPath: store.path,
|
|
350
|
+
durationMs: event.duration,
|
|
351
|
+
finishedAt: event.timestamp.toISOString(),
|
|
352
|
+
kind: operation?.kind || 'orm',
|
|
353
|
+
model: operation?.model,
|
|
354
|
+
operation: operation?.operation || 'query',
|
|
355
|
+
paramsJson: parseQueryParams(event.params),
|
|
356
|
+
paramsText: event.params,
|
|
357
|
+
query: event.query,
|
|
358
|
+
target: event.target,
|
|
359
|
+
});
|
|
360
|
+
}
|
|
244
361
|
}
|