@zintrust/trace 0.9.1 → 0.9.3
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/dist/TraceConnection.d.ts +25 -0
- package/dist/TraceConnection.js +98 -0
- package/dist/build-manifest.json +21 -25
- package/dist/dashboard/routes.js +12 -16
- package/dist/dashboard/ui.js +32 -4
- package/dist/register.js +1 -67
- package/package.json +2 -2
- package/src/TraceConnection.ts +182 -0
- package/src/dashboard/routes.ts +24 -26
- package/src/dashboard/ui.ts +32 -4
- package/src/register.ts +6 -112
- package/dist/storage/DebuggerStorage.d.ts +0 -13
- package/dist/storage/DebuggerStorage.js +0 -195
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { IDatabase } from '@zintrust/core';
|
|
2
|
+
type TraceErrorFactory = {
|
|
3
|
+
createConfigError?(message: string, details?: unknown): Error;
|
|
4
|
+
};
|
|
5
|
+
type TraceErrorApi = {
|
|
6
|
+
ErrorFactory?: TraceErrorFactory;
|
|
7
|
+
};
|
|
8
|
+
type TraceEnvApi = {
|
|
9
|
+
get(key: string, fallback: string): string;
|
|
10
|
+
};
|
|
11
|
+
export declare const TRACE_REQUIRED_TABLES: readonly ["zin_trace_entries", "zin_trace_entries_tags", "zin_trace_monitoring"];
|
|
12
|
+
export declare const createTraceConfigError: (coreApi: TraceErrorApi, message: string, details?: unknown) => Error;
|
|
13
|
+
export declare const getRuntimeTraceConnectionName: () => string | undefined;
|
|
14
|
+
export declare const resolveDashboardTraceConnectionName: (coreApi: TraceErrorApi, input: {
|
|
15
|
+
explicitConnectionName?: string;
|
|
16
|
+
configuredConnectionName?: string;
|
|
17
|
+
}) => string;
|
|
18
|
+
export declare const resolveTraceConnectionName: (env: Pick<TraceEnvApi, "get"> | undefined, configuredConnection?: string) => string;
|
|
19
|
+
export declare const resolveObservedConnectionName: (env: Pick<TraceEnvApi, "get"> | undefined, configuredObservedConnection: string | undefined, storageConnectionName: string) => string;
|
|
20
|
+
export declare function assertTraceConnectionResolved(coreApi: TraceErrorApi, db: IDatabase | undefined, params: {
|
|
21
|
+
connectionName: string;
|
|
22
|
+
envKey: 'TRACE_DB_CONNECTION' | 'TRACE_QUERY_CONNECTION';
|
|
23
|
+
}): asserts db is IDatabase;
|
|
24
|
+
export declare const assertTraceStorageReady: (coreApi: TraceErrorApi, db: IDatabase, connectionName: string, operation?: string) => Promise<void>;
|
|
25
|
+
export {};
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
export const TRACE_REQUIRED_TABLES = [
|
|
2
|
+
'zin_trace_entries',
|
|
3
|
+
'zin_trace_entries_tags',
|
|
4
|
+
'zin_trace_monitoring',
|
|
5
|
+
];
|
|
6
|
+
const createFallbackTraceConfigError = (message, details) => {
|
|
7
|
+
const error = new globalThis.Error(message);
|
|
8
|
+
error.name = 'ConfigError';
|
|
9
|
+
error.code = 'CONFIG_ERROR';
|
|
10
|
+
error.statusCode = 500;
|
|
11
|
+
error.details = details;
|
|
12
|
+
return error;
|
|
13
|
+
};
|
|
14
|
+
export const createTraceConfigError = (coreApi, message, details) => {
|
|
15
|
+
if (coreApi.ErrorFactory?.createConfigError !== undefined) {
|
|
16
|
+
return coreApi.ErrorFactory.createConfigError(message, details);
|
|
17
|
+
}
|
|
18
|
+
return createFallbackTraceConfigError(message, details);
|
|
19
|
+
};
|
|
20
|
+
export const getRuntimeTraceConnectionName = () => {
|
|
21
|
+
const runtimeConnection = globalThis.__zintrust_system_trace_connection_name__?.trim();
|
|
22
|
+
return runtimeConnection === undefined || runtimeConnection === ''
|
|
23
|
+
? undefined
|
|
24
|
+
: runtimeConnection;
|
|
25
|
+
};
|
|
26
|
+
export const resolveDashboardTraceConnectionName = (coreApi, input) => {
|
|
27
|
+
const explicitConnection = input.explicitConnectionName?.trim();
|
|
28
|
+
if (explicitConnection !== undefined && explicitConnection !== '') {
|
|
29
|
+
return explicitConnection;
|
|
30
|
+
}
|
|
31
|
+
const runtimeConnection = getRuntimeTraceConnectionName();
|
|
32
|
+
if (runtimeConnection !== undefined) {
|
|
33
|
+
return runtimeConnection;
|
|
34
|
+
}
|
|
35
|
+
const configuredConnection = input.configuredConnectionName?.trim();
|
|
36
|
+
if (configuredConnection !== undefined && configuredConnection !== '') {
|
|
37
|
+
return configuredConnection;
|
|
38
|
+
}
|
|
39
|
+
throw createTraceConfigError(coreApi, 'Trace dashboard connection is not configured.', {
|
|
40
|
+
envKey: 'TRACE_DB_CONNECTION',
|
|
41
|
+
hint: 'Import @zintrust/trace/register before mounting the dashboard, pass connectionName explicitly, or set TRACE_DB_CONNECTION to the trace storage connection.',
|
|
42
|
+
});
|
|
43
|
+
};
|
|
44
|
+
export const resolveTraceConnectionName = (env, configuredConnection) => {
|
|
45
|
+
const resolveDefaultConnection = () => {
|
|
46
|
+
const defaultConnection = env?.get('DB_CONNECTION', '').trim() ?? '';
|
|
47
|
+
if (defaultConnection === '' || defaultConnection === 'default')
|
|
48
|
+
return 'default';
|
|
49
|
+
return defaultConnection;
|
|
50
|
+
};
|
|
51
|
+
const explicitConnection = configuredConnection?.trim();
|
|
52
|
+
if (explicitConnection !== undefined && explicitConnection !== '') {
|
|
53
|
+
return explicitConnection === 'default' ? resolveDefaultConnection() : explicitConnection;
|
|
54
|
+
}
|
|
55
|
+
return resolveDefaultConnection();
|
|
56
|
+
};
|
|
57
|
+
export const resolveObservedConnectionName = (env, configuredObservedConnection, storageConnectionName) => {
|
|
58
|
+
if (typeof configuredObservedConnection === 'string' &&
|
|
59
|
+
configuredObservedConnection.trim() !== '') {
|
|
60
|
+
return resolveTraceConnectionName(env, configuredObservedConnection);
|
|
61
|
+
}
|
|
62
|
+
const defaultConnectionName = resolveTraceConnectionName(env);
|
|
63
|
+
if (storageConnectionName !== defaultConnectionName) {
|
|
64
|
+
return defaultConnectionName;
|
|
65
|
+
}
|
|
66
|
+
return storageConnectionName;
|
|
67
|
+
};
|
|
68
|
+
export function assertTraceConnectionResolved(coreApi, db, params) {
|
|
69
|
+
if (db !== undefined) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const pluginRequested = globalThis.__zintrust_system_trace_plugin_requested__ === true;
|
|
73
|
+
let hint = 'Configure TRACE_QUERY_CONNECTION, or ensure DB_CONNECTION resolves to an existing database connection.';
|
|
74
|
+
if (params.envKey === 'TRACE_DB_CONNECTION') {
|
|
75
|
+
hint = pluginRequested
|
|
76
|
+
? 'Configure TRACE_DB_CONNECTION to an existing database connection before enabling TRACE_ENABLED.'
|
|
77
|
+
: 'If this module is being imported from zintrust.plugins.*, switch that import to @zintrust/trace/plugin so trace registration runs after database runtime registration. Otherwise configure TRACE_DB_CONNECTION to an existing database connection before enabling TRACE_ENABLED.';
|
|
78
|
+
}
|
|
79
|
+
throw createTraceConfigError(coreApi, `Trace connection "${params.connectionName}" could not be resolved.`, {
|
|
80
|
+
connectionName: params.connectionName,
|
|
81
|
+
envKey: params.envKey,
|
|
82
|
+
hint,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
export const assertTraceStorageReady = async (coreApi, db, connectionName, operation = 'Trace storage connection') => {
|
|
86
|
+
try {
|
|
87
|
+
await Promise.all(TRACE_REQUIRED_TABLES.map(async (table) => {
|
|
88
|
+
await db.queryOne(`SELECT 1 AS ok FROM ${table} LIMIT 1`, []);
|
|
89
|
+
}));
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
throw createTraceConfigError(coreApi, `${operation} "${connectionName}" is not ready. Create the database if needed and run \`zin migrate:trace\` before enabling TRACE_ENABLED.`, {
|
|
93
|
+
connectionName,
|
|
94
|
+
error,
|
|
95
|
+
requiredTables: [...TRACE_REQUIRED_TABLES],
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
};
|
package/dist/build-manifest.json
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zintrust/trace",
|
|
3
|
-
"version": "0.9.
|
|
4
|
-
"buildDate": "2026-04-
|
|
3
|
+
"version": "0.9.3",
|
|
4
|
+
"buildDate": "2026-04-22T15:48:24.454Z",
|
|
5
5
|
"buildEnvironment": {
|
|
6
|
-
"node": "
|
|
7
|
-
"platform": "
|
|
8
|
-
"arch": "
|
|
6
|
+
"node": "v20.20.2",
|
|
7
|
+
"platform": "linux",
|
|
8
|
+
"arch": "x64"
|
|
9
9
|
},
|
|
10
10
|
"git": {
|
|
11
|
-
"commit": "
|
|
12
|
-
"branch": "
|
|
11
|
+
"commit": "552dfdf4",
|
|
12
|
+
"branch": "master"
|
|
13
13
|
},
|
|
14
14
|
"package": {
|
|
15
15
|
"engines": {
|
|
@@ -21,9 +21,13 @@
|
|
|
21
21
|
]
|
|
22
22
|
},
|
|
23
23
|
"files": {
|
|
24
|
-
"
|
|
25
|
-
"size":
|
|
26
|
-
"sha256": "
|
|
24
|
+
"TraceConnection.d.ts": {
|
|
25
|
+
"size": 1500,
|
|
26
|
+
"sha256": "0748601bebff011a0b3dbab736617d5e3dffe4af671f4b6f7af03012d58e5464"
|
|
27
|
+
},
|
|
28
|
+
"TraceConnection.js": {
|
|
29
|
+
"size": 4640,
|
|
30
|
+
"sha256": "c51cc312046b6b2bbe1673f1ff9508425cc7140a1d2341907f67aa36069c09f9"
|
|
27
31
|
},
|
|
28
32
|
"cli-register.d.ts": {
|
|
29
33
|
"size": 255,
|
|
@@ -62,16 +66,16 @@
|
|
|
62
66
|
"sha256": "0ac87bc54e57978e8a9ac5924d248e1f984927bb6c819ea5528ab1179b0b50b8"
|
|
63
67
|
},
|
|
64
68
|
"dashboard/routes.js": {
|
|
65
|
-
"size":
|
|
66
|
-
"sha256": "
|
|
69
|
+
"size": 2501,
|
|
70
|
+
"sha256": "06f0bf42214ed7691907443c0a1d5a4d8c3db3ce3f7b8269ba401ff421a54dfd"
|
|
67
71
|
},
|
|
68
72
|
"dashboard/ui.d.ts": {
|
|
69
73
|
"size": 117,
|
|
70
74
|
"sha256": "4862b41e0477f01afa0dbb446d4553b65c22ed774cd1e2db3489059ced392f94"
|
|
71
75
|
},
|
|
72
76
|
"dashboard/ui.js": {
|
|
73
|
-
"size":
|
|
74
|
-
"sha256": "
|
|
77
|
+
"size": 84221,
|
|
78
|
+
"sha256": "b260cd17012cd5bf5f24f24eb8958d0cf79bed1d976412def3706f960b3aefa6"
|
|
75
79
|
},
|
|
76
80
|
"index.d.ts": {
|
|
77
81
|
"size": 2599,
|
|
@@ -79,7 +83,7 @@
|
|
|
79
83
|
},
|
|
80
84
|
"index.js": {
|
|
81
85
|
"size": 3324,
|
|
82
|
-
"sha256": "
|
|
86
|
+
"sha256": "93a911d7f3813199936568bf8a63d598ec8f32a0006810453f0487a73e042ab3"
|
|
83
87
|
},
|
|
84
88
|
"migrations/20260331000001_create_zin_trace_entries_table.d.ts": {
|
|
85
89
|
"size": 304,
|
|
@@ -134,16 +138,8 @@
|
|
|
134
138
|
"sha256": "71d366165dd36f1675aa253a76262b226fb6c62e5ab632746b8aea61c0c625fc"
|
|
135
139
|
},
|
|
136
140
|
"register.js": {
|
|
137
|
-
"size":
|
|
138
|
-
"sha256": "
|
|
139
|
-
},
|
|
140
|
-
"storage/DebuggerStorage.d.ts": {
|
|
141
|
-
"size": 517,
|
|
142
|
-
"sha256": "c9c215aaa414f7b0c1fec6c82b054fc52bdf97af58f96f35c7f96672fb859c31"
|
|
143
|
-
},
|
|
144
|
-
"storage/DebuggerStorage.js": {
|
|
145
|
-
"size": 7442,
|
|
146
|
-
"sha256": "5ecce0fcfcf695df587a7b90a7a5c7efd2e64ad13c9f2d104b392f89f34f0dc4"
|
|
141
|
+
"size": 16585,
|
|
142
|
+
"sha256": "1e940ce8c216991572c633f34bc66ca6ef980c11e84042b02e957d99e142dbe5"
|
|
147
143
|
},
|
|
148
144
|
"storage/TraceContentBudget.d.ts": {
|
|
149
145
|
"size": 1306,
|
package/dist/dashboard/routes.js
CHANGED
|
@@ -3,25 +3,12 @@
|
|
|
3
3
|
* Mounts the SPA + all REST API endpoints under the configured basePath.
|
|
4
4
|
* Auth is NOT applied here — callers add middleware via routeOptions.
|
|
5
5
|
*/
|
|
6
|
-
import { appConfig, Router, useDatabase } from '@zintrust/core';
|
|
6
|
+
import { appConfig, ErrorFactory, Router, useDatabase, } from '@zintrust/core';
|
|
7
7
|
import { TraceConfig } from '../config.js';
|
|
8
8
|
import { TraceStorage } from '../storage/index.js';
|
|
9
|
+
import { assertTraceConnectionResolved, resolveDashboardTraceConnectionName, } from '../TraceConnection.js';
|
|
9
10
|
import { addMonitoring, clearEntries, getBatch, getEntry, getMonitoring, getStats, listEntries, removeMonitoring, setHandlerStorage, } from './handlers.js';
|
|
10
11
|
import { buildDashboardHtml } from './ui.js';
|
|
11
|
-
const resolveDashboardConnectionName = (connectionName) => {
|
|
12
|
-
const explicitConnection = connectionName?.trim();
|
|
13
|
-
if (explicitConnection !== undefined && explicitConnection !== '') {
|
|
14
|
-
return explicitConnection;
|
|
15
|
-
}
|
|
16
|
-
const runtimeConnection = globalThis.__zintrust_system_trace_connection_name__?.trim();
|
|
17
|
-
if (runtimeConnection !== undefined && runtimeConnection !== '') {
|
|
18
|
-
return runtimeConnection;
|
|
19
|
-
}
|
|
20
|
-
const configuredConnection = TraceConfig.merge().connection?.trim();
|
|
21
|
-
return configuredConnection === undefined || configuredConnection === ''
|
|
22
|
-
? undefined
|
|
23
|
-
: configuredConnection;
|
|
24
|
-
};
|
|
25
12
|
export const registerTraceRoutes = (router, storage, options = {}) => {
|
|
26
13
|
setHandlerStorage(storage);
|
|
27
14
|
const base = options.basePath ?? '/trace';
|
|
@@ -49,6 +36,15 @@ export const registerTraceRoutes = (router, storage, options = {}) => {
|
|
|
49
36
|
});
|
|
50
37
|
};
|
|
51
38
|
export const registerTraceDashboard = (router, options = {}) => {
|
|
52
|
-
const
|
|
39
|
+
const connectionName = resolveDashboardTraceConnectionName({ ErrorFactory }, {
|
|
40
|
+
explicitConnectionName: options.connectionName,
|
|
41
|
+
configuredConnectionName: TraceConfig.merge().connection,
|
|
42
|
+
});
|
|
43
|
+
const db = useDatabase(undefined, connectionName);
|
|
44
|
+
assertTraceConnectionResolved({ ErrorFactory }, db, {
|
|
45
|
+
connectionName,
|
|
46
|
+
envKey: 'TRACE_DB_CONNECTION',
|
|
47
|
+
});
|
|
48
|
+
const storage = TraceStorage.resolveStorage(db);
|
|
53
49
|
registerTraceRoutes(router, storage, options);
|
|
54
50
|
};
|
package/dist/dashboard/ui.js
CHANGED
|
@@ -72,7 +72,7 @@ const DASHBOARD_DOCUMENT = String.raw `<!DOCTYPE html>
|
|
|
72
72
|
.section-head{display:flex;justify-content:space-between;align-items:flex-start;gap:12px;padding:22px 24px 16px}.section-head h3{margin:0;font-size:1.04rem}.section-head p{margin:6px 0 0;color:var(--muted);font-size:.92rem}.toolbar{display:flex;flex-wrap:wrap;gap:10px;padding:0 24px 18px}.control,.toolbar input,.toolbar select{height:44px;border-radius:13px;border:1px solid var(--line);background:var(--surface-strong);color:var(--text);padding:0 14px;min-width:0}.toolbar input,.toolbar select{flex:1 1 180px}.toolbar input::placeholder{color:var(--muted)}.btn{height:44px;border:none;border-radius:13px;padding:0 16px;cursor:pointer;font-weight:800}.btn-primary{background:linear-gradient(135deg,var(--accent-strong),var(--accent));color:#fff}.btn-danger{background:rgba(239,68,68,.12);color:var(--danger);border:1px solid rgba(239,68,68,.18)}.btn-ghost{background:var(--surface-soft);color:var(--text);border:1px solid var(--line)}
|
|
73
73
|
.table-wrap{overflow:auto;padding:0 12px 12px}.table-wrap table{width:100%;border-collapse:separate;border-spacing:0;min-width:880px}th{padding:14px;color:var(--muted);font-size:.74rem;font-weight:800;letter-spacing:.12em;text-transform:uppercase;text-align:left;border-bottom:1px solid var(--line)}td{padding:15px 14px;border-bottom:1px solid var(--line);vertical-align:top}.row-button{cursor:pointer}.row-button:hover td{background:rgba(56,189,248,.05)}.summary{font-size:.93rem;font-weight:700;line-height:1.4;color:var(--text)}.summary-sub{margin-top:6px;color:var(--muted);font-size:.82rem;line-height:1.4}.mono{font-family:var(--mono)}.empty{padding:44px 24px;color:var(--muted);line-height:1.65;text-align:center}.pagination{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:0 24px 24px;color:var(--muted);flex-wrap:wrap}.pagination-controls{display:flex;gap:8px}.pagination button{height:40px;min-width:92px;padding:0 14px;border-radius:12px;border:1px solid var(--line);background:var(--surface-strong);color:var(--text);cursor:pointer}.pagination button:disabled{opacity:.45;cursor:not-allowed}
|
|
74
74
|
.activity-list{list-style:none;margin:0;padding:0 24px 24px}.activity-item{padding:14px 0;border-top:1px solid var(--line)}.activity-item:first-child{border-top:none}.activity-head{display:flex;align-items:center;gap:10px;flex-wrap:wrap}.activity-time{color:var(--muted);font-size:.85rem}.activity-summary{margin-top:8px;color:var(--text);line-height:1.48}.back-link{display:inline-flex;align-items:center;gap:8px;margin:0 0 14px;color:var(--accent);font-weight:800;cursor:pointer}.detail-card{padding:24px}.detail-meta{display:flex;flex-wrap:wrap;gap:10px;margin:14px 0 20px;color:var(--muted);font-size:.9rem;overflow-wrap:anywhere}.detail-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:14px}.detail-stack{display:grid;gap:16px;margin-top:18px}.detail-box{padding:16px;border-radius:16px;background:var(--surface-soft);border:1px solid var(--line)}.detail-box h4{margin:0 0 10px;font-size:.92rem}.detail-box dl{margin:0;display:grid;gap:8px}.detail-box dt{font-size:.76rem;letter-spacing:.08em;text-transform:uppercase;color:var(--muted);font-weight:800}.detail-box dd{margin:0;color:var(--text);line-height:1.45;overflow-wrap:anywhere}.trace-tabs{display:flex;gap:10px;flex-wrap:wrap;margin:20px 0 16px}.trace-tab{border:none;border-radius:12px;padding:10px 12px;background:transparent;color:var(--muted);cursor:pointer;box-shadow:inset 0 0 0 1px var(--line);font-weight:800}.trace-tab.active{background:rgba(56,189,248,.12);color:var(--text);box-shadow:inset 0 0 0 1px rgba(56,189,248,.28)}.trace-panel{display:grid;gap:14px}.trace-item{padding:18px;border-radius:16px;background:var(--surface-soft);border:1px solid var(--line)}.trace-item-head{display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap}.trace-item-summary{margin-top:10px;display:grid;gap:10px}.trace-note{color:var(--muted);line-height:1.6}.trace-disclosure{padding:0;overflow:hidden}.trace-disclosure[open]{padding-bottom:18px}.trace-disclosure .trace-item-summary{margin-top:0}.trace-disclosure-body{display:grid;gap:12px;padding:0 18px}.trace-summary{list-style:none;cursor:pointer;padding:18px}.trace-summary::-webkit-details-marker{display:none}.trace-summary-main{display:grid;gap:10px;min-width:0;flex:1}.trace-summary-copy{display:grid;gap:6px;min-width:0}.trace-summary-copy .summary,.trace-summary-copy .summary-sub{display:block;overflow-wrap:anywhere}.trace-disclosure-body .summary-sub{overflow-wrap:anywhere}
|
|
75
|
-
.tag{display:inline-flex;align-items:center;gap:6px;padding:4px 10px;border-radius:999px;background:rgba(56,189,248,.12);color:#bae6fd;font-size:.78rem;font-weight:800;margin:0 6px 6px 0;border:1px solid rgba(56,189,248,.18);text-decoration:none}button.tag{cursor:pointer}html[data-theme='light'] .tag{color:#075985}.tag.failed{background:rgba(239,68,68,.14);color:#fecaca;border-color:rgba(239,68,68,.2)}html[data-theme='light'] .tag.failed{color:#b91c1c}.tag.slow{background:rgba(245,158,11,.12);color:#fde68a;border-color:rgba(245,158,11,.18)}html[data-theme='light'] .tag.slow{color:#92400e}.type-pill{display:inline-flex;align-items:center;gap:6px;padding:6px 10px;border-radius:999px;font-size:.74rem;font-weight:900;text-transform:uppercase;letter-spacing:.08em;border:1px solid transparent}.pill-request{background:rgba(56,189,248,.14);color:#93c5fd}.pill-request.method-get{background:rgba(34,197,94,.16);color:#bbf7d0}.pill-request.method-post{background:rgba(59,130,246,.16);color:#bfdbfe}.pill-request.method-other{background:rgba(245,158,11,.16);color:#fde68a}.pill-query{background:rgba(34,197,94,.12);color:#86efac}.pill-exception{background:rgba(239,68,68,.14);color:#fecaca}.pill-log{background:rgba(168,85,247,.14);color:#ddd6fe}.pill-job,.pill-batch{background:rgba(245,158,11,.14);color:#fde68a}.pill-cache{background:rgba(20,184,166,.12);color:#99f6e4}.pill-schedule,.pill-command{background:rgba(14,165,233,.14);color:#bae6fd}.pill-mail,.pill-notification{background:rgba(236,72,153,.14);color:#fbcfe8}.pill-auth{background:rgba(148,163,184,.16);color:#e2e8f0}.pill-event,.pill-model{background:rgba(74,222,128,.14);color:#bbf7d0}.pill-redis{background:rgba(239,68,68,.12);color:#fecaca}.pill-gate{background:rgba(99,102,241,.14);color:#c7d2fe}.pill-middleware{background:rgba(45,212,191,.12);color:#ccfbf1}.pill-dump,.pill-view{background:rgba(148,163,184,.14);color:#e2e8f0}.pill-client-request{background:rgba(59,130,246,.14);color:#bfdbfe}html[data-theme='light'] .pill-request{color:#1d4ed8}html[data-theme='light'] .pill-request.method-get{color:#166534}html[data-theme='light'] .pill-request.method-post{color:#1d4ed8}html[data-theme='light'] .pill-request.method-other{color:#92400e}html[data-theme='light'] .pill-query{color:#166534}html[data-theme='light'] .pill-exception{color:#b91c1c}html[data-theme='light'] .pill-log{color:#6d28d9}html[data-theme='light'] .pill-job,html[data-theme='light'] .pill-batch{color:#92400e}html[data-theme='light'] .pill-cache{color:#115e59}html[data-theme='light'] .pill-schedule,html[data-theme='light'] .pill-command{color:#0c4a6e}html[data-theme='light'] .pill-mail,html[data-theme='light'] .pill-notification{color:#9d174d}html[data-theme='light'] .pill-auth,html[data-theme='light'] .pill-dump,html[data-theme='light'] .pill-view{color:#334155}html[data-theme='light'] .pill-event,html[data-theme='light'] .pill-model{color:#166534}html[data-theme='light'] .pill-redis{color:#991b1b}html[data-theme='light'] .pill-gate{color:#3730a3}html[data-theme='light'] .pill-middleware{color:#155e75}html[data-theme='light'] .pill-client-request{color:#1d4ed8}
|
|
75
|
+
.tag{display:inline-flex;align-items:center;gap:6px;padding:4px 10px;border-radius:999px;background:rgba(56,189,248,.12);color:#bae6fd;font-size:.78rem;font-weight:800;margin:0 6px 6px 0;border:1px solid rgba(56,189,248,.18);text-decoration:none}button.tag{cursor:pointer}html[data-theme='light'] .tag{color:#075985}.tag.failed{background:rgba(239,68,68,.14);color:#fecaca;border-color:rgba(239,68,68,.2)}html[data-theme='light'] .tag.failed{color:#b91c1c}.tag.slow{background:rgba(245,158,11,.12);color:#fde68a;border-color:rgba(245,158,11,.18)}html[data-theme='light'] .tag.slow{color:#92400e}.type-pill,.status-pill{display:inline-flex;align-items:center;gap:6px;padding:6px 10px;border-radius:999px;font-size:.74rem;font-weight:900;text-transform:uppercase;letter-spacing:.08em;border:1px solid transparent}.pill-request{background:rgba(56,189,248,.14);color:#93c5fd}.pill-request.method-get{background:rgba(34,197,94,.16);color:#bbf7d0}.pill-request.method-post{background:rgba(59,130,246,.16);color:#bfdbfe}.pill-request.method-other{background:rgba(245,158,11,.16);color:#fde68a}.pill-query{background:rgba(34,197,94,.12);color:#86efac}.pill-exception{background:rgba(239,68,68,.14);color:#fecaca}.pill-log{background:rgba(168,85,247,.14);color:#ddd6fe}.pill-job,.pill-batch{background:rgba(245,158,11,.14);color:#fde68a}.pill-cache{background:rgba(20,184,166,.12);color:#99f6e4}.pill-schedule,.pill-command{background:rgba(14,165,233,.14);color:#bae6fd}.pill-mail,.pill-notification{background:rgba(236,72,153,.14);color:#fbcfe8}.pill-auth{background:rgba(148,163,184,.16);color:#e2e8f0}.pill-event,.pill-model{background:rgba(74,222,128,.14);color:#bbf7d0}.pill-redis{background:rgba(239,68,68,.12);color:#fecaca}.pill-gate{background:rgba(99,102,241,.14);color:#c7d2fe}.pill-middleware{background:rgba(45,212,191,.12);color:#ccfbf1}.pill-dump,.pill-view{background:rgba(148,163,184,.14);color:#e2e8f0}.pill-client-request{background:rgba(59,130,246,.14);color:#bfdbfe}.status-pill{white-space:nowrap}.status-pill.status-2xx{background:rgba(34,197,94,.16);color:#bbf7d0;border-color:rgba(34,197,94,.24)}.status-pill.status-4xx{background:rgba(245,158,11,.16);color:#fde68a;border-color:rgba(245,158,11,.24)}.status-pill.status-5xx{background:rgba(239,68,68,.16);color:#fecaca;border-color:rgba(239,68,68,.24)}.status-pill.status-other{background:rgba(148,163,184,.14);color:#e2e8f0;border-color:rgba(148,163,184,.2)}html[data-theme='light'] .pill-request{color:#1d4ed8}html[data-theme='light'] .pill-request.method-get{color:#166534}html[data-theme='light'] .pill-request.method-post{color:#1d4ed8}html[data-theme='light'] .pill-request.method-other{color:#92400e}html[data-theme='light'] .pill-query{color:#166534}html[data-theme='light'] .pill-exception{color:#b91c1c}html[data-theme='light'] .pill-log{color:#6d28d9}html[data-theme='light'] .pill-job,html[data-theme='light'] .pill-batch{color:#92400e}html[data-theme='light'] .pill-cache{color:#115e59}html[data-theme='light'] .pill-schedule,html[data-theme='light'] .pill-command{color:#0c4a6e}html[data-theme='light'] .pill-mail,html[data-theme='light'] .pill-notification{color:#9d174d}html[data-theme='light'] .pill-auth,html[data-theme='light'] .pill-dump,html[data-theme='light'] .pill-view{color:#334155}html[data-theme='light'] .pill-event,html[data-theme='light'] .pill-model{color:#166534}html[data-theme='light'] .pill-redis{color:#991b1b}html[data-theme='light'] .pill-gate{color:#3730a3}html[data-theme='light'] .pill-middleware{color:#155e75}html[data-theme='light'] .pill-client-request{color:#1d4ed8}html[data-theme='light'] .status-pill.status-2xx{color:#166534}html[data-theme='light'] .status-pill.status-4xx{color:#92400e}html[data-theme='light'] .status-pill.status-5xx{color:#b91c1c}html[data-theme='light'] .status-pill.status-other{color:#334155}
|
|
76
76
|
.monitoring-wrap{padding:0 24px 24px}.tag-list{display:flex;flex-wrap:wrap;gap:10px;margin-bottom:18px}.tag-item{display:inline-flex;align-items:center;gap:10px;padding:10px 14px;border-radius:999px;border:1px solid var(--line);background:var(--surface-strong)}.tag-remove{border:none;background:rgba(239,68,68,.14);color:var(--danger);border-radius:999px;width:24px;height:24px;cursor:pointer;font-size:1rem;line-height:1}.helper-text{color:var(--muted);line-height:1.6}
|
|
77
77
|
.duration-chip{display:inline-flex;align-items:center;padding:5px 9px;border-radius:999px;border:1px solid transparent;font-size:.8rem;font-weight:700;color:var(--text);white-space:nowrap}.duration-chip.vfast{background:rgba(34,197,94,.14);border-color:rgba(34,197,94,.28);color:#bbf7d0}.duration-chip.fast{background:rgba(56,189,248,.12);border-color:rgba(56,189,248,.24);color:#bae6fd}.duration-chip.slow{background:rgba(245,158,11,.12);border-color:rgba(245,158,11,.22);color:#fde68a}.duration-chip.vslow{background:rgba(239,68,68,.14);border-color:rgba(239,68,68,.24);color:#fecaca}html[data-theme='light'] .duration-chip.vfast{color:#166534}html[data-theme='light'] .duration-chip.fast{color:#1d4ed8}html[data-theme='light'] .duration-chip.slow{color:#92400e}html[data-theme='light'] .duration-chip.vslow{color:#b91c1c}
|
|
78
78
|
.code-card{border-radius:16px;border:1px solid var(--code-border);background:var(--surface-soft);overflow:hidden}.code-toolbar{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:12px 14px;border-bottom:1px solid var(--line)}.code-label{font-size:.76rem;letter-spacing:.12em;text-transform:uppercase;color:var(--muted);font-weight:800}.copy-button{display:inline-flex;align-items:center;justify-content:center;gap:8px;width:38px;height:38px;border-radius:12px;border:1px solid var(--line);background:var(--surface-strong);color:var(--text);cursor:pointer;transition:border-color .16s ease,color .16s ease}.copy-button:hover{border-color:rgba(56,189,248,.35);color:var(--accent)}.copy-button[data-copied='true']{color:var(--success);border-color:rgba(34,197,94,.28)}.copy-button svg{width:16px;height:16px;display:block}.code-block{margin:0;padding:18px 20px;background:var(--code-bg);color:#dbeafe;border:0;overflow:auto;white-space:pre;line-height:1.72;font-family:var(--mono);font-size:.92rem}.code-block.wrap{white-space:pre-wrap;overflow-wrap:anywhere;word-break:break-word}.code-block code{font-family:inherit}.html-preview-wrap{padding:14px;background:var(--surface-strong);border-top:1px solid var(--line)}.html-preview{display:block;width:100%;min-height:320px;border:1px solid var(--line);border-radius:14px;background:#fff}.inline-collapse{margin:0;border-top:1px solid var(--line);background:var(--surface-strong)}.inline-collapse summary{cursor:pointer;list-style:none;padding:14px 16px;font-size:.82rem;letter-spacing:.08em;text-transform:uppercase;color:var(--muted);font-weight:800}.inline-collapse summary::-webkit-details-marker{display:none}.inline-collapse[open] summary{border-bottom:1px solid var(--line)}.inline-collapse .code-block{border-top:none}.tok-key{color:#93c5fd}.tok-string{color:#86efac}.tok-number{color:#f9a8d4}.tok-boolean{color:#facc15}.tok-null{color:#fb7185}.tok-punctuation{color:#94a3b8}.tok-sql-keyword{color:#f472b6;font-weight:700}.tok-sql-identifier{color:#93c5fd}.tok-sql-string{color:#86efac}.tok-sql-number{color:#facc15}.tok-sql-comment{color:#64748b;font-style:italic}html[data-theme='light'] .code-block{color:#0f172a}html[data-theme='light'] .tok-key{color:#1d4ed8}html[data-theme='light'] .tok-string{color:#15803d}html[data-theme='light'] .tok-number{color:#c026d3}html[data-theme='light'] .tok-boolean{color:#b45309}html[data-theme='light'] .tok-null{color:#dc2626}html[data-theme='light'] .tok-punctuation{color:#64748b}html[data-theme='light'] .tok-sql-keyword{color:#db2777}html[data-theme='light'] .tok-sql-identifier{color:#2563eb}html[data-theme='light'] .tok-sql-string{color:#15803d}html[data-theme='light'] .tok-sql-number{color:#b45309}html[data-theme='light'] .tok-sql-comment{color:#6b7280}
|
|
@@ -258,6 +258,21 @@ const DASHBOARD_DOCUMENT = String.raw `<!DOCTYPE html>
|
|
|
258
258
|
return 'type-pill pill-' + String(type || '').replace(/_/g, '-') + requestMethodClass(entry);
|
|
259
259
|
};
|
|
260
260
|
|
|
261
|
+
const statusToneClass = (value) => {
|
|
262
|
+
const status = Number(value);
|
|
263
|
+
if (!Number.isFinite(status)) return 'status-other';
|
|
264
|
+
if (status >= 500 && status < 600) return 'status-5xx';
|
|
265
|
+
if (status >= 400 && status < 500) return 'status-4xx';
|
|
266
|
+
if (status >= 200 && status < 300) return 'status-2xx';
|
|
267
|
+
return 'status-other';
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
const statusBadgeHtml = (value) => {
|
|
271
|
+
const status = Number(value);
|
|
272
|
+
if (!Number.isFinite(status)) return '';
|
|
273
|
+
return '<span class="status-pill ' + statusToneClass(status) + '">' + escapeHtml(String(status)) + '</span>';
|
|
274
|
+
};
|
|
275
|
+
|
|
261
276
|
const timeSince = (value) => {
|
|
262
277
|
const createdAt = Number(value);
|
|
263
278
|
if (!Number.isFinite(createdAt)) return 'Unknown';
|
|
@@ -518,6 +533,19 @@ const DASHBOARD_DOCUMENT = String.raw `<!DOCTYPE html>
|
|
|
518
533
|
};
|
|
519
534
|
|
|
520
535
|
const entrySummaryHtml = (entry) => {
|
|
536
|
+
const content = entry && entry.content ? entry.content : {};
|
|
537
|
+
if (entry.type === 'request') {
|
|
538
|
+
return '<div class="summary">' + statusBadgeHtml(content.responseStatus) + ' <span class="mono">' + escapeHtml(content.uri || '') + '</span></div><div class="summary-sub">Incoming request</div>';
|
|
539
|
+
}
|
|
540
|
+
if (entry.type === 'client_request') {
|
|
541
|
+
const clientParts = [
|
|
542
|
+
content.method ? '<span class="mono">' + escapeHtml(content.method) + '</span>' : '',
|
|
543
|
+
content.url ? '<span class="mono">' + escapeHtml(content.url) + '</span>' : '',
|
|
544
|
+
statusBadgeHtml(content.responseStatus) || (content.error ? '<span class="status-pill status-5xx">Failed</span>' : '')
|
|
545
|
+
].filter(Boolean).join(' ');
|
|
546
|
+
return '<div class="summary">' + clientParts + '</div><div class="summary-sub">Outbound HTTP call</div>';
|
|
547
|
+
}
|
|
548
|
+
|
|
521
549
|
const summary = escapeHtml(entrySummaryText(entry) || 'No summary available');
|
|
522
550
|
const secondary = [
|
|
523
551
|
entry.type === 'request' ? 'Incoming request' : '',
|
|
@@ -613,7 +641,7 @@ const DASHBOARD_DOCUMENT = String.raw `<!DOCTYPE html>
|
|
|
613
641
|
renderMetricBox('Request', [
|
|
614
642
|
{ label: 'Method', value: escapeHtml(content.method || '') },
|
|
615
643
|
{ label: 'URL', value: '<span class="mono">' + escapeHtml(content.url || '') + '</span>' },
|
|
616
|
-
{ label: 'Status', value:
|
|
644
|
+
{ label: 'Status', value: statusBadgeHtml(content.responseStatus) || escapeHtml(content.error ? 'Failed' : 'Pending') },
|
|
617
645
|
{ label: 'Duration', value: escapeHtml(formatDuration(getEntryDuration(entry))) }
|
|
618
646
|
]),
|
|
619
647
|
renderMetricBox('Runtime', [
|
|
@@ -797,7 +825,7 @@ const DASHBOARD_DOCUMENT = String.raw `<!DOCTYPE html>
|
|
|
797
825
|
].join(''),
|
|
798
826
|
payload: detailJson(content.payload || {}, 'Payload Json'),
|
|
799
827
|
headers: '<div class="detail-stack">' + detailJson(content.headers || {}, 'Request Header Json') + detailJson(content.responseHeaders || {}, 'Response Header Json') + '</div>',
|
|
800
|
-
response: '<div class="detail-stack"><div class="detail-grid">' + renderMetricBox('Status', [{ label: 'Response status', value: escapeHtml(content.responseStatus || '') }, { label: 'Duration', value: escapeHtml(formatDuration(getEntryDuration(entry))) }]) + '</div>' + (content.responseBody === undefined ? '<p class="trace-note">No response body was captured for this request.</p>' : detailJson(content.responseBody, 'Response Body Json')) + detailJson(content.responseHeaders || {}, 'Response Header Json') + '</div>',
|
|
828
|
+
response: '<div class="detail-stack"><div class="detail-grid">' + renderMetricBox('Status', [{ label: 'Response status', value: statusBadgeHtml(content.responseStatus) || escapeHtml(content.responseStatus || '') }, { label: 'Duration', value: escapeHtml(formatDuration(getEntryDuration(entry))) }]) + '</div>' + (content.responseBody === undefined ? '<p class="trace-note">No response body was captured for this request.</p>' : detailJson(content.responseBody, 'Response Body Json')) + detailJson(content.responseHeaders || {}, 'Response Header Json') + '</div>',
|
|
801
829
|
queries: renderDetailBatchPanel('queries'),
|
|
802
830
|
middleware: renderDetailBatchPanel('middleware'),
|
|
803
831
|
models: renderDetailBatchPanel('models'),
|
|
@@ -812,7 +840,7 @@ const DASHBOARD_DOCUMENT = String.raw `<!DOCTYPE html>
|
|
|
812
840
|
'<span class="back-link" data-action="close-detail"><- Back to entries</span>',
|
|
813
841
|
'<section class="panel detail-card">',
|
|
814
842
|
'<div>' + (entry.type === 'request'
|
|
815
|
-
? '<span class="' + typeClass(entry) + '">' + escapeHtml(entryTypeLabel(entry)) + '</span>
|
|
843
|
+
? '<span class="' + typeClass(entry) + '">' + escapeHtml(entryTypeLabel(entry)) + '</span> ' + statusBadgeHtml(content.responseStatus) + ' <span class="mono">' + escapeHtml(content.uri || '') + '</span> ' + tagsHtml(entry.tags)
|
|
816
844
|
: '<span class="' + typeClass(entry) + '">' + escapeHtml(entryTypeLabel(entry)) + '</span> ' + tagsHtml(entry.tags)) + '</div>',
|
|
817
845
|
'<div class="detail-meta"><span>UUID <span class="mono">' + escapeHtml(entry.uuid) + '</span></span><span>Batch <span class="mono">' + escapeHtml(entry.batchId || '-') + '</span></span><span>' + durationHtml(entry) + '</span><span>' + escapeHtml(new Date(Number(entry.createdAt)).toISOString()) + '</span></div>',
|
|
818
846
|
'<div class="trace-tabs">',
|
package/dist/register.js
CHANGED
|
@@ -26,8 +26,8 @@ import { TraceContentBudget } from './storage/TraceContentBudget.js';
|
|
|
26
26
|
import { TraceContentRedaction } from './storage/TraceContentRedaction.js';
|
|
27
27
|
import { TraceEntryFiltering } from './storage/TraceEntryFiltering.js';
|
|
28
28
|
import { TraceWriteDiagnostics } from './storage/TraceWriteDiagnostics.js';
|
|
29
|
+
import { assertTraceConnectionResolved, assertTraceStorageReady, resolveObservedConnectionName, resolveTraceConnectionName, } from './TraceConnection.js';
|
|
29
30
|
const globalTraceRegisterState = globalThis;
|
|
30
|
-
globalTraceRegisterState.__zintrust_system_trace_plugin_requested__ = true;
|
|
31
31
|
const traceAlreadyInitialized = globalTraceRegisterState.__zintrust_system_trace_register_initialized__ === true;
|
|
32
32
|
if (!traceAlreadyInitialized) {
|
|
33
33
|
globalTraceRegisterState.__zintrust_system_trace_register_initialized__ = true;
|
|
@@ -40,11 +40,6 @@ const importCore = async () => {
|
|
|
40
40
|
return {};
|
|
41
41
|
}
|
|
42
42
|
};
|
|
43
|
-
const TRACE_REQUIRED_TABLES = [
|
|
44
|
-
'zin_trace_entries',
|
|
45
|
-
'zin_trace_entries_tags',
|
|
46
|
-
'zin_trace_monitoring',
|
|
47
|
-
];
|
|
48
43
|
const resolveRegisterMiddleware = () => {
|
|
49
44
|
const globalMiddlewareRegistrarState = globalThis;
|
|
50
45
|
return (middleware) => {
|
|
@@ -57,30 +52,6 @@ const resolveRegisterMiddleware = () => {
|
|
|
57
52
|
globalMiddlewareRegistrarState.__zintrust_pending_global_middlewares__.push(middleware);
|
|
58
53
|
};
|
|
59
54
|
};
|
|
60
|
-
const resolveTraceConnectionName = (env, configuredConnection) => {
|
|
61
|
-
const resolveDefaultConnection = () => {
|
|
62
|
-
const defaultConnection = env?.get('DB_CONNECTION', '').trim() ?? '';
|
|
63
|
-
if (defaultConnection === '' || defaultConnection === 'default')
|
|
64
|
-
return 'default';
|
|
65
|
-
return defaultConnection;
|
|
66
|
-
};
|
|
67
|
-
const explicitConnection = configuredConnection?.trim();
|
|
68
|
-
if (explicitConnection !== undefined && explicitConnection !== '') {
|
|
69
|
-
return explicitConnection === 'default' ? resolveDefaultConnection() : explicitConnection;
|
|
70
|
-
}
|
|
71
|
-
return resolveDefaultConnection();
|
|
72
|
-
};
|
|
73
|
-
const resolveObservedConnectionName = (env, configuredObservedConnection, storageConnectionName) => {
|
|
74
|
-
if (typeof configuredObservedConnection === 'string' &&
|
|
75
|
-
configuredObservedConnection.trim() !== '') {
|
|
76
|
-
return resolveTraceConnectionName(env, configuredObservedConnection);
|
|
77
|
-
}
|
|
78
|
-
const defaultConnectionName = resolveTraceConnectionName(env);
|
|
79
|
-
if (storageConnectionName !== defaultConnectionName) {
|
|
80
|
-
return defaultConnectionName;
|
|
81
|
-
}
|
|
82
|
-
return storageConnectionName;
|
|
83
|
-
};
|
|
84
55
|
const isObjectValue = (value) => {
|
|
85
56
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
86
57
|
};
|
|
@@ -160,43 +131,6 @@ const buildTraceRedactionOverrides = (input) => {
|
|
|
160
131
|
? redaction
|
|
161
132
|
: undefined;
|
|
162
133
|
};
|
|
163
|
-
const createTraceConfigError = (coreApi, message, details) => {
|
|
164
|
-
if (coreApi.ErrorFactory?.createConfigError !== undefined) {
|
|
165
|
-
return coreApi.ErrorFactory.createConfigError(message, details);
|
|
166
|
-
}
|
|
167
|
-
const error = new globalThis.Error(message);
|
|
168
|
-
error.name = 'ConfigError';
|
|
169
|
-
error.code = 'CONFIG_ERROR';
|
|
170
|
-
error.statusCode = 500;
|
|
171
|
-
error.details = details;
|
|
172
|
-
return error;
|
|
173
|
-
};
|
|
174
|
-
function assertTraceConnectionResolved(coreApi, db, params) {
|
|
175
|
-
if (db !== undefined) {
|
|
176
|
-
return;
|
|
177
|
-
}
|
|
178
|
-
throw createTraceConfigError(coreApi, `Trace connection "${params.connectionName}" could not be resolved.`, {
|
|
179
|
-
connectionName: params.connectionName,
|
|
180
|
-
envKey: params.envKey,
|
|
181
|
-
hint: params.envKey === 'TRACE_DB_CONNECTION'
|
|
182
|
-
? 'Configure TRACE_DB_CONNECTION to an existing database connection before enabling TRACE_ENABLED.'
|
|
183
|
-
: 'Configure TRACE_QUERY_CONNECTION, or ensure DB_CONNECTION resolves to an existing database connection.',
|
|
184
|
-
});
|
|
185
|
-
}
|
|
186
|
-
const assertTraceStorageReady = async (coreApi, db, connectionName) => {
|
|
187
|
-
try {
|
|
188
|
-
await Promise.all(TRACE_REQUIRED_TABLES.map(async (table) => {
|
|
189
|
-
await db.queryOne(`SELECT 1 AS ok FROM ${table} LIMIT 1`, []);
|
|
190
|
-
}));
|
|
191
|
-
}
|
|
192
|
-
catch (error) {
|
|
193
|
-
throw createTraceConfigError(coreApi, `Trace storage connection "${connectionName}" is not ready. Create the database if needed and run \`zin migrate:trace\` before enabling TRACE_ENABLED.`, {
|
|
194
|
-
connectionName,
|
|
195
|
-
error,
|
|
196
|
-
requiredTables: [...TRACE_REQUIRED_TABLES],
|
|
197
|
-
});
|
|
198
|
-
}
|
|
199
|
-
};
|
|
200
134
|
const core = (await importCore());
|
|
201
135
|
const Env = core.Env;
|
|
202
136
|
const startupOverrides = await resolveTraceStartupOverrides(core);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zintrust/trace",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.3",
|
|
4
4
|
"description": "Trace assistant for ZinTrust: logs requests, queries, exceptions, jobs, and more.",
|
|
5
5
|
"private": false,
|
|
6
6
|
"type": "module",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"node": ">=20.0.0"
|
|
41
41
|
},
|
|
42
42
|
"peerDependencies": {
|
|
43
|
-
"@zintrust/core": "
|
|
43
|
+
"@zintrust/core": "^0.9.2"
|
|
44
44
|
},
|
|
45
45
|
"publishConfig": {
|
|
46
46
|
"access": "public"
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import type { IDatabase } from '@zintrust/core';
|
|
2
|
+
|
|
3
|
+
type TraceErrorFactory = {
|
|
4
|
+
createConfigError?(message: string, details?: unknown): Error;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
type TraceErrorApi = {
|
|
8
|
+
ErrorFactory?: TraceErrorFactory;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
type TraceEnvApi = {
|
|
12
|
+
get(key: string, fallback: string): string;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
type GlobalTraceConnectionState = {
|
|
16
|
+
__zintrust_system_trace_connection_name__?: string;
|
|
17
|
+
__zintrust_system_trace_plugin_requested__?: boolean;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export const TRACE_REQUIRED_TABLES = [
|
|
21
|
+
'zin_trace_entries',
|
|
22
|
+
'zin_trace_entries_tags',
|
|
23
|
+
'zin_trace_monitoring',
|
|
24
|
+
] as const;
|
|
25
|
+
|
|
26
|
+
const createFallbackTraceConfigError = (message: string, details?: unknown): Error => {
|
|
27
|
+
const error = new globalThis.Error(message) as Error & {
|
|
28
|
+
code?: string;
|
|
29
|
+
details?: unknown;
|
|
30
|
+
name?: string;
|
|
31
|
+
statusCode?: number;
|
|
32
|
+
};
|
|
33
|
+
error.name = 'ConfigError';
|
|
34
|
+
error.code = 'CONFIG_ERROR';
|
|
35
|
+
error.statusCode = 500;
|
|
36
|
+
error.details = details;
|
|
37
|
+
return error;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export const createTraceConfigError = (
|
|
41
|
+
coreApi: TraceErrorApi,
|
|
42
|
+
message: string,
|
|
43
|
+
details?: unknown
|
|
44
|
+
): Error => {
|
|
45
|
+
if (coreApi.ErrorFactory?.createConfigError !== undefined) {
|
|
46
|
+
return coreApi.ErrorFactory.createConfigError(message, details);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return createFallbackTraceConfigError(message, details);
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export const getRuntimeTraceConnectionName = (): string | undefined => {
|
|
53
|
+
const runtimeConnection = (
|
|
54
|
+
globalThis as GlobalTraceConnectionState
|
|
55
|
+
).__zintrust_system_trace_connection_name__?.trim();
|
|
56
|
+
|
|
57
|
+
return runtimeConnection === undefined || runtimeConnection === ''
|
|
58
|
+
? undefined
|
|
59
|
+
: runtimeConnection;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export const resolveDashboardTraceConnectionName = (
|
|
63
|
+
coreApi: TraceErrorApi,
|
|
64
|
+
input: {
|
|
65
|
+
explicitConnectionName?: string;
|
|
66
|
+
configuredConnectionName?: string;
|
|
67
|
+
}
|
|
68
|
+
): string => {
|
|
69
|
+
const explicitConnection = input.explicitConnectionName?.trim();
|
|
70
|
+
if (explicitConnection !== undefined && explicitConnection !== '') {
|
|
71
|
+
return explicitConnection;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const runtimeConnection = getRuntimeTraceConnectionName();
|
|
75
|
+
if (runtimeConnection !== undefined) {
|
|
76
|
+
return runtimeConnection;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const configuredConnection = input.configuredConnectionName?.trim();
|
|
80
|
+
if (configuredConnection !== undefined && configuredConnection !== '') {
|
|
81
|
+
return configuredConnection;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
throw createTraceConfigError(coreApi, 'Trace dashboard connection is not configured.', {
|
|
85
|
+
envKey: 'TRACE_DB_CONNECTION',
|
|
86
|
+
hint: 'Import @zintrust/trace/register before mounting the dashboard, pass connectionName explicitly, or set TRACE_DB_CONNECTION to the trace storage connection.',
|
|
87
|
+
});
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
export const resolveTraceConnectionName = (
|
|
91
|
+
env: Pick<TraceEnvApi, 'get'> | undefined,
|
|
92
|
+
configuredConnection?: string
|
|
93
|
+
): string => {
|
|
94
|
+
const resolveDefaultConnection = (): string => {
|
|
95
|
+
const defaultConnection = env?.get('DB_CONNECTION', '').trim() ?? '';
|
|
96
|
+
if (defaultConnection === '' || defaultConnection === 'default') return 'default';
|
|
97
|
+
return defaultConnection;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const explicitConnection = configuredConnection?.trim();
|
|
101
|
+
if (explicitConnection !== undefined && explicitConnection !== '') {
|
|
102
|
+
return explicitConnection === 'default' ? resolveDefaultConnection() : explicitConnection;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return resolveDefaultConnection();
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
export const resolveObservedConnectionName = (
|
|
109
|
+
env: Pick<TraceEnvApi, 'get'> | undefined,
|
|
110
|
+
configuredObservedConnection: string | undefined,
|
|
111
|
+
storageConnectionName: string
|
|
112
|
+
): string => {
|
|
113
|
+
if (
|
|
114
|
+
typeof configuredObservedConnection === 'string' &&
|
|
115
|
+
configuredObservedConnection.trim() !== ''
|
|
116
|
+
) {
|
|
117
|
+
return resolveTraceConnectionName(env, configuredObservedConnection);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const defaultConnectionName = resolveTraceConnectionName(env);
|
|
121
|
+
if (storageConnectionName !== defaultConnectionName) {
|
|
122
|
+
return defaultConnectionName;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return storageConnectionName;
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
export function assertTraceConnectionResolved(
|
|
129
|
+
coreApi: TraceErrorApi,
|
|
130
|
+
db: IDatabase | undefined,
|
|
131
|
+
params: { connectionName: string; envKey: 'TRACE_DB_CONNECTION' | 'TRACE_QUERY_CONNECTION' }
|
|
132
|
+
): asserts db is IDatabase {
|
|
133
|
+
if (db !== undefined) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const pluginRequested =
|
|
138
|
+
(globalThis as GlobalTraceConnectionState).__zintrust_system_trace_plugin_requested__ === true;
|
|
139
|
+
let hint =
|
|
140
|
+
'Configure TRACE_QUERY_CONNECTION, or ensure DB_CONNECTION resolves to an existing database connection.';
|
|
141
|
+
|
|
142
|
+
if (params.envKey === 'TRACE_DB_CONNECTION') {
|
|
143
|
+
hint = pluginRequested
|
|
144
|
+
? 'Configure TRACE_DB_CONNECTION to an existing database connection before enabling TRACE_ENABLED.'
|
|
145
|
+
: 'If this module is being imported from zintrust.plugins.*, switch that import to @zintrust/trace/plugin so trace registration runs after database runtime registration. Otherwise configure TRACE_DB_CONNECTION to an existing database connection before enabling TRACE_ENABLED.';
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
throw createTraceConfigError(
|
|
149
|
+
coreApi,
|
|
150
|
+
`Trace connection "${params.connectionName}" could not be resolved.`,
|
|
151
|
+
{
|
|
152
|
+
connectionName: params.connectionName,
|
|
153
|
+
envKey: params.envKey,
|
|
154
|
+
hint,
|
|
155
|
+
}
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export const assertTraceStorageReady = async (
|
|
160
|
+
coreApi: TraceErrorApi,
|
|
161
|
+
db: IDatabase,
|
|
162
|
+
connectionName: string,
|
|
163
|
+
operation = 'Trace storage connection'
|
|
164
|
+
): Promise<void> => {
|
|
165
|
+
try {
|
|
166
|
+
await Promise.all(
|
|
167
|
+
TRACE_REQUIRED_TABLES.map(async (table) => {
|
|
168
|
+
await db.queryOne(`SELECT 1 AS ok FROM ${table} LIMIT 1`, []);
|
|
169
|
+
})
|
|
170
|
+
);
|
|
171
|
+
} catch (error) {
|
|
172
|
+
throw createTraceConfigError(
|
|
173
|
+
coreApi,
|
|
174
|
+
`${operation} "${connectionName}" is not ready. Create the database if needed and run \`zin migrate:trace\` before enabling TRACE_ENABLED.`,
|
|
175
|
+
{
|
|
176
|
+
connectionName,
|
|
177
|
+
error,
|
|
178
|
+
requiredTables: [...TRACE_REQUIRED_TABLES],
|
|
179
|
+
}
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
};
|
package/src/dashboard/routes.ts
CHANGED
|
@@ -3,9 +3,20 @@
|
|
|
3
3
|
* Mounts the SPA + all REST API endpoints under the configured basePath.
|
|
4
4
|
* Auth is NOT applied here — callers add middleware via routeOptions.
|
|
5
5
|
*/
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
appConfig,
|
|
8
|
+
ErrorFactory,
|
|
9
|
+
Router,
|
|
10
|
+
useDatabase,
|
|
11
|
+
type IRouter,
|
|
12
|
+
type RouteOptions,
|
|
13
|
+
} from '@zintrust/core';
|
|
7
14
|
import { TraceConfig } from '../config';
|
|
8
15
|
import { TraceStorage } from '../storage';
|
|
16
|
+
import {
|
|
17
|
+
assertTraceConnectionResolved,
|
|
18
|
+
resolveDashboardTraceConnectionName,
|
|
19
|
+
} from '../TraceConnection';
|
|
9
20
|
import type { ITraceStorage } from '../types';
|
|
10
21
|
import {
|
|
11
22
|
addMonitoring,
|
|
@@ -36,29 +47,6 @@ export type TraceDashboardRegistrationOptions = TraceDashboardOptions & {
|
|
|
36
47
|
connectionName?: string;
|
|
37
48
|
};
|
|
38
49
|
|
|
39
|
-
type GlobalTraceDashboardState = {
|
|
40
|
-
__zintrust_system_trace_connection_name__?: string;
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
const resolveDashboardConnectionName = (connectionName?: string): string | undefined => {
|
|
44
|
-
const explicitConnection = connectionName?.trim();
|
|
45
|
-
if (explicitConnection !== undefined && explicitConnection !== '') {
|
|
46
|
-
return explicitConnection;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
const runtimeConnection = (
|
|
50
|
-
globalThis as GlobalTraceDashboardState
|
|
51
|
-
).__zintrust_system_trace_connection_name__?.trim();
|
|
52
|
-
if (runtimeConnection !== undefined && runtimeConnection !== '') {
|
|
53
|
-
return runtimeConnection;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
const configuredConnection = TraceConfig.merge().connection?.trim();
|
|
57
|
-
return configuredConnection === undefined || configuredConnection === ''
|
|
58
|
-
? undefined
|
|
59
|
-
: configuredConnection;
|
|
60
|
-
};
|
|
61
|
-
|
|
62
50
|
export const registerTraceRoutes = (
|
|
63
51
|
router: IRouter,
|
|
64
52
|
storage: ITraceStorage,
|
|
@@ -108,9 +96,19 @@ export const registerTraceDashboard = (
|
|
|
108
96
|
router: IRouter,
|
|
109
97
|
options: TraceDashboardRegistrationOptions = {}
|
|
110
98
|
): void => {
|
|
111
|
-
const
|
|
112
|
-
|
|
99
|
+
const connectionName = resolveDashboardTraceConnectionName(
|
|
100
|
+
{ ErrorFactory },
|
|
101
|
+
{
|
|
102
|
+
explicitConnectionName: options.connectionName,
|
|
103
|
+
configuredConnectionName: TraceConfig.merge().connection,
|
|
104
|
+
}
|
|
113
105
|
);
|
|
106
|
+
const db = useDatabase(undefined, connectionName);
|
|
107
|
+
assertTraceConnectionResolved({ ErrorFactory }, db, {
|
|
108
|
+
connectionName,
|
|
109
|
+
envKey: 'TRACE_DB_CONNECTION',
|
|
110
|
+
});
|
|
111
|
+
const storage = TraceStorage.resolveStorage(db);
|
|
114
112
|
|
|
115
113
|
registerTraceRoutes(router, storage, options);
|
|
116
114
|
};
|
package/src/dashboard/ui.ts
CHANGED
|
@@ -78,7 +78,7 @@ const DASHBOARD_DOCUMENT = String.raw`<!DOCTYPE html>
|
|
|
78
78
|
.section-head{display:flex;justify-content:space-between;align-items:flex-start;gap:12px;padding:22px 24px 16px}.section-head h3{margin:0;font-size:1.04rem}.section-head p{margin:6px 0 0;color:var(--muted);font-size:.92rem}.toolbar{display:flex;flex-wrap:wrap;gap:10px;padding:0 24px 18px}.control,.toolbar input,.toolbar select{height:44px;border-radius:13px;border:1px solid var(--line);background:var(--surface-strong);color:var(--text);padding:0 14px;min-width:0}.toolbar input,.toolbar select{flex:1 1 180px}.toolbar input::placeholder{color:var(--muted)}.btn{height:44px;border:none;border-radius:13px;padding:0 16px;cursor:pointer;font-weight:800}.btn-primary{background:linear-gradient(135deg,var(--accent-strong),var(--accent));color:#fff}.btn-danger{background:rgba(239,68,68,.12);color:var(--danger);border:1px solid rgba(239,68,68,.18)}.btn-ghost{background:var(--surface-soft);color:var(--text);border:1px solid var(--line)}
|
|
79
79
|
.table-wrap{overflow:auto;padding:0 12px 12px}.table-wrap table{width:100%;border-collapse:separate;border-spacing:0;min-width:880px}th{padding:14px;color:var(--muted);font-size:.74rem;font-weight:800;letter-spacing:.12em;text-transform:uppercase;text-align:left;border-bottom:1px solid var(--line)}td{padding:15px 14px;border-bottom:1px solid var(--line);vertical-align:top}.row-button{cursor:pointer}.row-button:hover td{background:rgba(56,189,248,.05)}.summary{font-size:.93rem;font-weight:700;line-height:1.4;color:var(--text)}.summary-sub{margin-top:6px;color:var(--muted);font-size:.82rem;line-height:1.4}.mono{font-family:var(--mono)}.empty{padding:44px 24px;color:var(--muted);line-height:1.65;text-align:center}.pagination{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:0 24px 24px;color:var(--muted);flex-wrap:wrap}.pagination-controls{display:flex;gap:8px}.pagination button{height:40px;min-width:92px;padding:0 14px;border-radius:12px;border:1px solid var(--line);background:var(--surface-strong);color:var(--text);cursor:pointer}.pagination button:disabled{opacity:.45;cursor:not-allowed}
|
|
80
80
|
.activity-list{list-style:none;margin:0;padding:0 24px 24px}.activity-item{padding:14px 0;border-top:1px solid var(--line)}.activity-item:first-child{border-top:none}.activity-head{display:flex;align-items:center;gap:10px;flex-wrap:wrap}.activity-time{color:var(--muted);font-size:.85rem}.activity-summary{margin-top:8px;color:var(--text);line-height:1.48}.back-link{display:inline-flex;align-items:center;gap:8px;margin:0 0 14px;color:var(--accent);font-weight:800;cursor:pointer}.detail-card{padding:24px}.detail-meta{display:flex;flex-wrap:wrap;gap:10px;margin:14px 0 20px;color:var(--muted);font-size:.9rem;overflow-wrap:anywhere}.detail-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:14px}.detail-stack{display:grid;gap:16px;margin-top:18px}.detail-box{padding:16px;border-radius:16px;background:var(--surface-soft);border:1px solid var(--line)}.detail-box h4{margin:0 0 10px;font-size:.92rem}.detail-box dl{margin:0;display:grid;gap:8px}.detail-box dt{font-size:.76rem;letter-spacing:.08em;text-transform:uppercase;color:var(--muted);font-weight:800}.detail-box dd{margin:0;color:var(--text);line-height:1.45;overflow-wrap:anywhere}.trace-tabs{display:flex;gap:10px;flex-wrap:wrap;margin:20px 0 16px}.trace-tab{border:none;border-radius:12px;padding:10px 12px;background:transparent;color:var(--muted);cursor:pointer;box-shadow:inset 0 0 0 1px var(--line);font-weight:800}.trace-tab.active{background:rgba(56,189,248,.12);color:var(--text);box-shadow:inset 0 0 0 1px rgba(56,189,248,.28)}.trace-panel{display:grid;gap:14px}.trace-item{padding:18px;border-radius:16px;background:var(--surface-soft);border:1px solid var(--line)}.trace-item-head{display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap}.trace-item-summary{margin-top:10px;display:grid;gap:10px}.trace-note{color:var(--muted);line-height:1.6}.trace-disclosure{padding:0;overflow:hidden}.trace-disclosure[open]{padding-bottom:18px}.trace-disclosure .trace-item-summary{margin-top:0}.trace-disclosure-body{display:grid;gap:12px;padding:0 18px}.trace-summary{list-style:none;cursor:pointer;padding:18px}.trace-summary::-webkit-details-marker{display:none}.trace-summary-main{display:grid;gap:10px;min-width:0;flex:1}.trace-summary-copy{display:grid;gap:6px;min-width:0}.trace-summary-copy .summary,.trace-summary-copy .summary-sub{display:block;overflow-wrap:anywhere}.trace-disclosure-body .summary-sub{overflow-wrap:anywhere}
|
|
81
|
-
.tag{display:inline-flex;align-items:center;gap:6px;padding:4px 10px;border-radius:999px;background:rgba(56,189,248,.12);color:#bae6fd;font-size:.78rem;font-weight:800;margin:0 6px 6px 0;border:1px solid rgba(56,189,248,.18);text-decoration:none}button.tag{cursor:pointer}html[data-theme='light'] .tag{color:#075985}.tag.failed{background:rgba(239,68,68,.14);color:#fecaca;border-color:rgba(239,68,68,.2)}html[data-theme='light'] .tag.failed{color:#b91c1c}.tag.slow{background:rgba(245,158,11,.12);color:#fde68a;border-color:rgba(245,158,11,.18)}html[data-theme='light'] .tag.slow{color:#92400e}.type-pill{display:inline-flex;align-items:center;gap:6px;padding:6px 10px;border-radius:999px;font-size:.74rem;font-weight:900;text-transform:uppercase;letter-spacing:.08em;border:1px solid transparent}.pill-request{background:rgba(56,189,248,.14);color:#93c5fd}.pill-request.method-get{background:rgba(34,197,94,.16);color:#bbf7d0}.pill-request.method-post{background:rgba(59,130,246,.16);color:#bfdbfe}.pill-request.method-other{background:rgba(245,158,11,.16);color:#fde68a}.pill-query{background:rgba(34,197,94,.12);color:#86efac}.pill-exception{background:rgba(239,68,68,.14);color:#fecaca}.pill-log{background:rgba(168,85,247,.14);color:#ddd6fe}.pill-job,.pill-batch{background:rgba(245,158,11,.14);color:#fde68a}.pill-cache{background:rgba(20,184,166,.12);color:#99f6e4}.pill-schedule,.pill-command{background:rgba(14,165,233,.14);color:#bae6fd}.pill-mail,.pill-notification{background:rgba(236,72,153,.14);color:#fbcfe8}.pill-auth{background:rgba(148,163,184,.16);color:#e2e8f0}.pill-event,.pill-model{background:rgba(74,222,128,.14);color:#bbf7d0}.pill-redis{background:rgba(239,68,68,.12);color:#fecaca}.pill-gate{background:rgba(99,102,241,.14);color:#c7d2fe}.pill-middleware{background:rgba(45,212,191,.12);color:#ccfbf1}.pill-dump,.pill-view{background:rgba(148,163,184,.14);color:#e2e8f0}.pill-client-request{background:rgba(59,130,246,.14);color:#bfdbfe}html[data-theme='light'] .pill-request{color:#1d4ed8}html[data-theme='light'] .pill-request.method-get{color:#166534}html[data-theme='light'] .pill-request.method-post{color:#1d4ed8}html[data-theme='light'] .pill-request.method-other{color:#92400e}html[data-theme='light'] .pill-query{color:#166534}html[data-theme='light'] .pill-exception{color:#b91c1c}html[data-theme='light'] .pill-log{color:#6d28d9}html[data-theme='light'] .pill-job,html[data-theme='light'] .pill-batch{color:#92400e}html[data-theme='light'] .pill-cache{color:#115e59}html[data-theme='light'] .pill-schedule,html[data-theme='light'] .pill-command{color:#0c4a6e}html[data-theme='light'] .pill-mail,html[data-theme='light'] .pill-notification{color:#9d174d}html[data-theme='light'] .pill-auth,html[data-theme='light'] .pill-dump,html[data-theme='light'] .pill-view{color:#334155}html[data-theme='light'] .pill-event,html[data-theme='light'] .pill-model{color:#166534}html[data-theme='light'] .pill-redis{color:#991b1b}html[data-theme='light'] .pill-gate{color:#3730a3}html[data-theme='light'] .pill-middleware{color:#155e75}html[data-theme='light'] .pill-client-request{color:#1d4ed8}
|
|
81
|
+
.tag{display:inline-flex;align-items:center;gap:6px;padding:4px 10px;border-radius:999px;background:rgba(56,189,248,.12);color:#bae6fd;font-size:.78rem;font-weight:800;margin:0 6px 6px 0;border:1px solid rgba(56,189,248,.18);text-decoration:none}button.tag{cursor:pointer}html[data-theme='light'] .tag{color:#075985}.tag.failed{background:rgba(239,68,68,.14);color:#fecaca;border-color:rgba(239,68,68,.2)}html[data-theme='light'] .tag.failed{color:#b91c1c}.tag.slow{background:rgba(245,158,11,.12);color:#fde68a;border-color:rgba(245,158,11,.18)}html[data-theme='light'] .tag.slow{color:#92400e}.type-pill,.status-pill{display:inline-flex;align-items:center;gap:6px;padding:6px 10px;border-radius:999px;font-size:.74rem;font-weight:900;text-transform:uppercase;letter-spacing:.08em;border:1px solid transparent}.pill-request{background:rgba(56,189,248,.14);color:#93c5fd}.pill-request.method-get{background:rgba(34,197,94,.16);color:#bbf7d0}.pill-request.method-post{background:rgba(59,130,246,.16);color:#bfdbfe}.pill-request.method-other{background:rgba(245,158,11,.16);color:#fde68a}.pill-query{background:rgba(34,197,94,.12);color:#86efac}.pill-exception{background:rgba(239,68,68,.14);color:#fecaca}.pill-log{background:rgba(168,85,247,.14);color:#ddd6fe}.pill-job,.pill-batch{background:rgba(245,158,11,.14);color:#fde68a}.pill-cache{background:rgba(20,184,166,.12);color:#99f6e4}.pill-schedule,.pill-command{background:rgba(14,165,233,.14);color:#bae6fd}.pill-mail,.pill-notification{background:rgba(236,72,153,.14);color:#fbcfe8}.pill-auth{background:rgba(148,163,184,.16);color:#e2e8f0}.pill-event,.pill-model{background:rgba(74,222,128,.14);color:#bbf7d0}.pill-redis{background:rgba(239,68,68,.12);color:#fecaca}.pill-gate{background:rgba(99,102,241,.14);color:#c7d2fe}.pill-middleware{background:rgba(45,212,191,.12);color:#ccfbf1}.pill-dump,.pill-view{background:rgba(148,163,184,.14);color:#e2e8f0}.pill-client-request{background:rgba(59,130,246,.14);color:#bfdbfe}.status-pill{white-space:nowrap}.status-pill.status-2xx{background:rgba(34,197,94,.16);color:#bbf7d0;border-color:rgba(34,197,94,.24)}.status-pill.status-4xx{background:rgba(245,158,11,.16);color:#fde68a;border-color:rgba(245,158,11,.24)}.status-pill.status-5xx{background:rgba(239,68,68,.16);color:#fecaca;border-color:rgba(239,68,68,.24)}.status-pill.status-other{background:rgba(148,163,184,.14);color:#e2e8f0;border-color:rgba(148,163,184,.2)}html[data-theme='light'] .pill-request{color:#1d4ed8}html[data-theme='light'] .pill-request.method-get{color:#166534}html[data-theme='light'] .pill-request.method-post{color:#1d4ed8}html[data-theme='light'] .pill-request.method-other{color:#92400e}html[data-theme='light'] .pill-query{color:#166534}html[data-theme='light'] .pill-exception{color:#b91c1c}html[data-theme='light'] .pill-log{color:#6d28d9}html[data-theme='light'] .pill-job,html[data-theme='light'] .pill-batch{color:#92400e}html[data-theme='light'] .pill-cache{color:#115e59}html[data-theme='light'] .pill-schedule,html[data-theme='light'] .pill-command{color:#0c4a6e}html[data-theme='light'] .pill-mail,html[data-theme='light'] .pill-notification{color:#9d174d}html[data-theme='light'] .pill-auth,html[data-theme='light'] .pill-dump,html[data-theme='light'] .pill-view{color:#334155}html[data-theme='light'] .pill-event,html[data-theme='light'] .pill-model{color:#166534}html[data-theme='light'] .pill-redis{color:#991b1b}html[data-theme='light'] .pill-gate{color:#3730a3}html[data-theme='light'] .pill-middleware{color:#155e75}html[data-theme='light'] .pill-client-request{color:#1d4ed8}html[data-theme='light'] .status-pill.status-2xx{color:#166534}html[data-theme='light'] .status-pill.status-4xx{color:#92400e}html[data-theme='light'] .status-pill.status-5xx{color:#b91c1c}html[data-theme='light'] .status-pill.status-other{color:#334155}
|
|
82
82
|
.monitoring-wrap{padding:0 24px 24px}.tag-list{display:flex;flex-wrap:wrap;gap:10px;margin-bottom:18px}.tag-item{display:inline-flex;align-items:center;gap:10px;padding:10px 14px;border-radius:999px;border:1px solid var(--line);background:var(--surface-strong)}.tag-remove{border:none;background:rgba(239,68,68,.14);color:var(--danger);border-radius:999px;width:24px;height:24px;cursor:pointer;font-size:1rem;line-height:1}.helper-text{color:var(--muted);line-height:1.6}
|
|
83
83
|
.duration-chip{display:inline-flex;align-items:center;padding:5px 9px;border-radius:999px;border:1px solid transparent;font-size:.8rem;font-weight:700;color:var(--text);white-space:nowrap}.duration-chip.vfast{background:rgba(34,197,94,.14);border-color:rgba(34,197,94,.28);color:#bbf7d0}.duration-chip.fast{background:rgba(56,189,248,.12);border-color:rgba(56,189,248,.24);color:#bae6fd}.duration-chip.slow{background:rgba(245,158,11,.12);border-color:rgba(245,158,11,.22);color:#fde68a}.duration-chip.vslow{background:rgba(239,68,68,.14);border-color:rgba(239,68,68,.24);color:#fecaca}html[data-theme='light'] .duration-chip.vfast{color:#166534}html[data-theme='light'] .duration-chip.fast{color:#1d4ed8}html[data-theme='light'] .duration-chip.slow{color:#92400e}html[data-theme='light'] .duration-chip.vslow{color:#b91c1c}
|
|
84
84
|
.code-card{border-radius:16px;border:1px solid var(--code-border);background:var(--surface-soft);overflow:hidden}.code-toolbar{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:12px 14px;border-bottom:1px solid var(--line)}.code-label{font-size:.76rem;letter-spacing:.12em;text-transform:uppercase;color:var(--muted);font-weight:800}.copy-button{display:inline-flex;align-items:center;justify-content:center;gap:8px;width:38px;height:38px;border-radius:12px;border:1px solid var(--line);background:var(--surface-strong);color:var(--text);cursor:pointer;transition:border-color .16s ease,color .16s ease}.copy-button:hover{border-color:rgba(56,189,248,.35);color:var(--accent)}.copy-button[data-copied='true']{color:var(--success);border-color:rgba(34,197,94,.28)}.copy-button svg{width:16px;height:16px;display:block}.code-block{margin:0;padding:18px 20px;background:var(--code-bg);color:#dbeafe;border:0;overflow:auto;white-space:pre;line-height:1.72;font-family:var(--mono);font-size:.92rem}.code-block.wrap{white-space:pre-wrap;overflow-wrap:anywhere;word-break:break-word}.code-block code{font-family:inherit}.html-preview-wrap{padding:14px;background:var(--surface-strong);border-top:1px solid var(--line)}.html-preview{display:block;width:100%;min-height:320px;border:1px solid var(--line);border-radius:14px;background:#fff}.inline-collapse{margin:0;border-top:1px solid var(--line);background:var(--surface-strong)}.inline-collapse summary{cursor:pointer;list-style:none;padding:14px 16px;font-size:.82rem;letter-spacing:.08em;text-transform:uppercase;color:var(--muted);font-weight:800}.inline-collapse summary::-webkit-details-marker{display:none}.inline-collapse[open] summary{border-bottom:1px solid var(--line)}.inline-collapse .code-block{border-top:none}.tok-key{color:#93c5fd}.tok-string{color:#86efac}.tok-number{color:#f9a8d4}.tok-boolean{color:#facc15}.tok-null{color:#fb7185}.tok-punctuation{color:#94a3b8}.tok-sql-keyword{color:#f472b6;font-weight:700}.tok-sql-identifier{color:#93c5fd}.tok-sql-string{color:#86efac}.tok-sql-number{color:#facc15}.tok-sql-comment{color:#64748b;font-style:italic}html[data-theme='light'] .code-block{color:#0f172a}html[data-theme='light'] .tok-key{color:#1d4ed8}html[data-theme='light'] .tok-string{color:#15803d}html[data-theme='light'] .tok-number{color:#c026d3}html[data-theme='light'] .tok-boolean{color:#b45309}html[data-theme='light'] .tok-null{color:#dc2626}html[data-theme='light'] .tok-punctuation{color:#64748b}html[data-theme='light'] .tok-sql-keyword{color:#db2777}html[data-theme='light'] .tok-sql-identifier{color:#2563eb}html[data-theme='light'] .tok-sql-string{color:#15803d}html[data-theme='light'] .tok-sql-number{color:#b45309}html[data-theme='light'] .tok-sql-comment{color:#6b7280}
|
|
@@ -264,6 +264,21 @@ const DASHBOARD_DOCUMENT = String.raw`<!DOCTYPE html>
|
|
|
264
264
|
return 'type-pill pill-' + String(type || '').replace(/_/g, '-') + requestMethodClass(entry);
|
|
265
265
|
};
|
|
266
266
|
|
|
267
|
+
const statusToneClass = (value) => {
|
|
268
|
+
const status = Number(value);
|
|
269
|
+
if (!Number.isFinite(status)) return 'status-other';
|
|
270
|
+
if (status >= 500 && status < 600) return 'status-5xx';
|
|
271
|
+
if (status >= 400 && status < 500) return 'status-4xx';
|
|
272
|
+
if (status >= 200 && status < 300) return 'status-2xx';
|
|
273
|
+
return 'status-other';
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
const statusBadgeHtml = (value) => {
|
|
277
|
+
const status = Number(value);
|
|
278
|
+
if (!Number.isFinite(status)) return '';
|
|
279
|
+
return '<span class="status-pill ' + statusToneClass(status) + '">' + escapeHtml(String(status)) + '</span>';
|
|
280
|
+
};
|
|
281
|
+
|
|
267
282
|
const timeSince = (value) => {
|
|
268
283
|
const createdAt = Number(value);
|
|
269
284
|
if (!Number.isFinite(createdAt)) return 'Unknown';
|
|
@@ -524,6 +539,19 @@ const DASHBOARD_DOCUMENT = String.raw`<!DOCTYPE html>
|
|
|
524
539
|
};
|
|
525
540
|
|
|
526
541
|
const entrySummaryHtml = (entry) => {
|
|
542
|
+
const content = entry && entry.content ? entry.content : {};
|
|
543
|
+
if (entry.type === 'request') {
|
|
544
|
+
return '<div class="summary">' + statusBadgeHtml(content.responseStatus) + ' <span class="mono">' + escapeHtml(content.uri || '') + '</span></div><div class="summary-sub">Incoming request</div>';
|
|
545
|
+
}
|
|
546
|
+
if (entry.type === 'client_request') {
|
|
547
|
+
const clientParts = [
|
|
548
|
+
content.method ? '<span class="mono">' + escapeHtml(content.method) + '</span>' : '',
|
|
549
|
+
content.url ? '<span class="mono">' + escapeHtml(content.url) + '</span>' : '',
|
|
550
|
+
statusBadgeHtml(content.responseStatus) || (content.error ? '<span class="status-pill status-5xx">Failed</span>' : '')
|
|
551
|
+
].filter(Boolean).join(' ');
|
|
552
|
+
return '<div class="summary">' + clientParts + '</div><div class="summary-sub">Outbound HTTP call</div>';
|
|
553
|
+
}
|
|
554
|
+
|
|
527
555
|
const summary = escapeHtml(entrySummaryText(entry) || 'No summary available');
|
|
528
556
|
const secondary = [
|
|
529
557
|
entry.type === 'request' ? 'Incoming request' : '',
|
|
@@ -619,7 +647,7 @@ const DASHBOARD_DOCUMENT = String.raw`<!DOCTYPE html>
|
|
|
619
647
|
renderMetricBox('Request', [
|
|
620
648
|
{ label: 'Method', value: escapeHtml(content.method || '') },
|
|
621
649
|
{ label: 'URL', value: '<span class="mono">' + escapeHtml(content.url || '') + '</span>' },
|
|
622
|
-
{ label: 'Status', value:
|
|
650
|
+
{ label: 'Status', value: statusBadgeHtml(content.responseStatus) || escapeHtml(content.error ? 'Failed' : 'Pending') },
|
|
623
651
|
{ label: 'Duration', value: escapeHtml(formatDuration(getEntryDuration(entry))) }
|
|
624
652
|
]),
|
|
625
653
|
renderMetricBox('Runtime', [
|
|
@@ -803,7 +831,7 @@ const DASHBOARD_DOCUMENT = String.raw`<!DOCTYPE html>
|
|
|
803
831
|
].join(''),
|
|
804
832
|
payload: detailJson(content.payload || {}, 'Payload Json'),
|
|
805
833
|
headers: '<div class="detail-stack">' + detailJson(content.headers || {}, 'Request Header Json') + detailJson(content.responseHeaders || {}, 'Response Header Json') + '</div>',
|
|
806
|
-
response: '<div class="detail-stack"><div class="detail-grid">' + renderMetricBox('Status', [{ label: 'Response status', value: escapeHtml(content.responseStatus || '') }, { label: 'Duration', value: escapeHtml(formatDuration(getEntryDuration(entry))) }]) + '</div>' + (content.responseBody === undefined ? '<p class="trace-note">No response body was captured for this request.</p>' : detailJson(content.responseBody, 'Response Body Json')) + detailJson(content.responseHeaders || {}, 'Response Header Json') + '</div>',
|
|
834
|
+
response: '<div class="detail-stack"><div class="detail-grid">' + renderMetricBox('Status', [{ label: 'Response status', value: statusBadgeHtml(content.responseStatus) || escapeHtml(content.responseStatus || '') }, { label: 'Duration', value: escapeHtml(formatDuration(getEntryDuration(entry))) }]) + '</div>' + (content.responseBody === undefined ? '<p class="trace-note">No response body was captured for this request.</p>' : detailJson(content.responseBody, 'Response Body Json')) + detailJson(content.responseHeaders || {}, 'Response Header Json') + '</div>',
|
|
807
835
|
queries: renderDetailBatchPanel('queries'),
|
|
808
836
|
middleware: renderDetailBatchPanel('middleware'),
|
|
809
837
|
models: renderDetailBatchPanel('models'),
|
|
@@ -818,7 +846,7 @@ const DASHBOARD_DOCUMENT = String.raw`<!DOCTYPE html>
|
|
|
818
846
|
'<span class="back-link" data-action="close-detail"><- Back to entries</span>',
|
|
819
847
|
'<section class="panel detail-card">',
|
|
820
848
|
'<div>' + (entry.type === 'request'
|
|
821
|
-
? '<span class="' + typeClass(entry) + '">' + escapeHtml(entryTypeLabel(entry)) + '</span>
|
|
849
|
+
? '<span class="' + typeClass(entry) + '">' + escapeHtml(entryTypeLabel(entry)) + '</span> ' + statusBadgeHtml(content.responseStatus) + ' <span class="mono">' + escapeHtml(content.uri || '') + '</span> ' + tagsHtml(entry.tags)
|
|
822
850
|
: '<span class="' + typeClass(entry) + '">' + escapeHtml(entryTypeLabel(entry)) + '</span> ' + tagsHtml(entry.tags)) + '</div>',
|
|
823
851
|
'<div class="detail-meta"><span>UUID <span class="mono">' + escapeHtml(entry.uuid) + '</span></span><span>Batch <span class="mono">' + escapeHtml(entry.batchId || '-') + '</span></span><span>' + durationHtml(entry) + '</span><span>' + escapeHtml(new Date(Number(entry.createdAt)).toISOString()) + '</span></div>',
|
|
824
852
|
'<div class="trace-tabs">',
|
package/src/register.ts
CHANGED
|
@@ -26,6 +26,12 @@ import { TraceContentBudget } from './storage/TraceContentBudget';
|
|
|
26
26
|
import { TraceContentRedaction } from './storage/TraceContentRedaction';
|
|
27
27
|
import { TraceEntryFiltering } from './storage/TraceEntryFiltering';
|
|
28
28
|
import { TraceWriteDiagnostics } from './storage/TraceWriteDiagnostics';
|
|
29
|
+
import {
|
|
30
|
+
assertTraceConnectionResolved,
|
|
31
|
+
assertTraceStorageReady,
|
|
32
|
+
resolveObservedConnectionName,
|
|
33
|
+
resolveTraceConnectionName,
|
|
34
|
+
} from './TraceConnection';
|
|
29
35
|
import type { ITraceWatcherConfig, TraceConfigOverrides } from './types';
|
|
30
36
|
|
|
31
37
|
export type {}; // side-effect ESM module
|
|
@@ -38,7 +44,6 @@ type GlobalTraceRegisterState = {
|
|
|
38
44
|
};
|
|
39
45
|
|
|
40
46
|
const globalTraceRegisterState = globalThis as unknown as GlobalTraceRegisterState;
|
|
41
|
-
globalTraceRegisterState.__zintrust_system_trace_plugin_requested__ = true;
|
|
42
47
|
const traceAlreadyInitialized =
|
|
43
48
|
globalTraceRegisterState.__zintrust_system_trace_register_initialized__ === true;
|
|
44
49
|
|
|
@@ -80,14 +85,6 @@ type CoreApi = {
|
|
|
80
85
|
};
|
|
81
86
|
};
|
|
82
87
|
|
|
83
|
-
type CoreDatabase = import('@zintrust/core').IDatabase;
|
|
84
|
-
|
|
85
|
-
const TRACE_REQUIRED_TABLES = [
|
|
86
|
-
'zin_trace_entries',
|
|
87
|
-
'zin_trace_entries_tags',
|
|
88
|
-
'zin_trace_monitoring',
|
|
89
|
-
] as const;
|
|
90
|
-
|
|
91
88
|
type GlobalMiddlewareRegistrarState = {
|
|
92
89
|
__zintrust_register_global_middleware__?: ITraceWatcherConfig['registerMiddleware'];
|
|
93
90
|
__zintrust_pending_global_middlewares__?: Array<
|
|
@@ -111,44 +108,6 @@ const resolveRegisterMiddleware = (): NonNullable<ITraceWatcherConfig['registerM
|
|
|
111
108
|
};
|
|
112
109
|
};
|
|
113
110
|
|
|
114
|
-
const resolveTraceConnectionName = (
|
|
115
|
-
env: Pick<NonNullable<CoreApi['Env']>, 'get'> | undefined,
|
|
116
|
-
configuredConnection?: string
|
|
117
|
-
): string => {
|
|
118
|
-
const resolveDefaultConnection = (): string => {
|
|
119
|
-
const defaultConnection = env?.get('DB_CONNECTION', '').trim() ?? '';
|
|
120
|
-
if (defaultConnection === '' || defaultConnection === 'default') return 'default';
|
|
121
|
-
return defaultConnection;
|
|
122
|
-
};
|
|
123
|
-
|
|
124
|
-
const explicitConnection = configuredConnection?.trim();
|
|
125
|
-
if (explicitConnection !== undefined && explicitConnection !== '') {
|
|
126
|
-
return explicitConnection === 'default' ? resolveDefaultConnection() : explicitConnection;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
return resolveDefaultConnection();
|
|
130
|
-
};
|
|
131
|
-
|
|
132
|
-
const resolveObservedConnectionName = (
|
|
133
|
-
env: Pick<NonNullable<CoreApi['Env']>, 'get'> | undefined,
|
|
134
|
-
configuredObservedConnection: string | undefined,
|
|
135
|
-
storageConnectionName: string
|
|
136
|
-
): string => {
|
|
137
|
-
if (
|
|
138
|
-
typeof configuredObservedConnection === 'string' &&
|
|
139
|
-
configuredObservedConnection.trim() !== ''
|
|
140
|
-
) {
|
|
141
|
-
return resolveTraceConnectionName(env, configuredObservedConnection);
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
const defaultConnectionName = resolveTraceConnectionName(env);
|
|
145
|
-
if (storageConnectionName !== defaultConnectionName) {
|
|
146
|
-
return defaultConnectionName;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
return storageConnectionName;
|
|
150
|
-
};
|
|
151
|
-
|
|
152
111
|
const isObjectValue = (value: unknown): value is Record<string, unknown> => {
|
|
153
112
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
154
113
|
};
|
|
@@ -240,71 +199,6 @@ const buildTraceRedactionOverrides = (input: {
|
|
|
240
199
|
: undefined;
|
|
241
200
|
};
|
|
242
201
|
|
|
243
|
-
const createTraceConfigError = (coreApi: CoreApi, message: string, details?: unknown): Error => {
|
|
244
|
-
if (coreApi.ErrorFactory?.createConfigError !== undefined) {
|
|
245
|
-
return coreApi.ErrorFactory.createConfigError(message, details);
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
const error = new globalThis.Error(message) as Error & {
|
|
249
|
-
code?: string;
|
|
250
|
-
details?: unknown;
|
|
251
|
-
name?: string;
|
|
252
|
-
statusCode?: number;
|
|
253
|
-
};
|
|
254
|
-
error.name = 'ConfigError';
|
|
255
|
-
error.code = 'CONFIG_ERROR';
|
|
256
|
-
error.statusCode = 500;
|
|
257
|
-
error.details = details;
|
|
258
|
-
return error;
|
|
259
|
-
};
|
|
260
|
-
|
|
261
|
-
function assertTraceConnectionResolved(
|
|
262
|
-
coreApi: CoreApi,
|
|
263
|
-
db: CoreDatabase | undefined,
|
|
264
|
-
params: { connectionName: string; envKey: 'TRACE_DB_CONNECTION' | 'TRACE_QUERY_CONNECTION' }
|
|
265
|
-
): asserts db is CoreDatabase {
|
|
266
|
-
if (db !== undefined) {
|
|
267
|
-
return;
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
throw createTraceConfigError(
|
|
271
|
-
coreApi,
|
|
272
|
-
`Trace connection "${params.connectionName}" could not be resolved.`,
|
|
273
|
-
{
|
|
274
|
-
connectionName: params.connectionName,
|
|
275
|
-
envKey: params.envKey,
|
|
276
|
-
hint:
|
|
277
|
-
params.envKey === 'TRACE_DB_CONNECTION'
|
|
278
|
-
? 'Configure TRACE_DB_CONNECTION to an existing database connection before enabling TRACE_ENABLED.'
|
|
279
|
-
: 'Configure TRACE_QUERY_CONNECTION, or ensure DB_CONNECTION resolves to an existing database connection.',
|
|
280
|
-
}
|
|
281
|
-
);
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
const assertTraceStorageReady = async (
|
|
285
|
-
coreApi: CoreApi,
|
|
286
|
-
db: CoreDatabase,
|
|
287
|
-
connectionName: string
|
|
288
|
-
): Promise<void> => {
|
|
289
|
-
try {
|
|
290
|
-
await Promise.all(
|
|
291
|
-
TRACE_REQUIRED_TABLES.map(async (table) => {
|
|
292
|
-
await db.queryOne(`SELECT 1 AS ok FROM ${table} LIMIT 1`, []);
|
|
293
|
-
})
|
|
294
|
-
);
|
|
295
|
-
} catch (error) {
|
|
296
|
-
throw createTraceConfigError(
|
|
297
|
-
coreApi,
|
|
298
|
-
`Trace storage connection "${connectionName}" is not ready. Create the database if needed and run \`zin migrate:trace\` before enabling TRACE_ENABLED.`,
|
|
299
|
-
{
|
|
300
|
-
connectionName,
|
|
301
|
-
error,
|
|
302
|
-
requiredTables: [...TRACE_REQUIRED_TABLES],
|
|
303
|
-
}
|
|
304
|
-
);
|
|
305
|
-
}
|
|
306
|
-
};
|
|
307
|
-
|
|
308
202
|
const core = (await importCore()) as CoreApi;
|
|
309
203
|
const Env = core.Env;
|
|
310
204
|
const startupOverrides = await resolveTraceStartupOverrides(core);
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* TraceStorage — sealed namespace wrapping the D1/SQLite driver.
|
|
3
|
-
* Resolves the correct IDatabase from the app config, then delegates all
|
|
4
|
-
* read/write operations to the trace storage facade.
|
|
5
|
-
*/
|
|
6
|
-
import type { IDatabase } from '@zintrust/core';
|
|
7
|
-
import type { ITraceStorage } from '../types';
|
|
8
|
-
export declare const TraceStorage: Readonly<{
|
|
9
|
-
resolveStorage: (db: IDatabase) => ITraceStorage;
|
|
10
|
-
reset: () => void;
|
|
11
|
-
familyHash: (input: string) => string;
|
|
12
|
-
}>;
|
|
13
|
-
export { type ITraceStorage } from '../types';
|
|
@@ -1,195 +0,0 @@
|
|
|
1
|
-
import { familyHash } from '../utils/familyHash.js';
|
|
2
|
-
const TABLE_ENTRIES = 'zin_trace_entries';
|
|
3
|
-
const TABLE_TAGS = 'zin_trace_entries_tags';
|
|
4
|
-
const TABLE_MONITORING = 'zin_trace_monitoring';
|
|
5
|
-
const generateUuid = () => crypto.randomUUID();
|
|
6
|
-
const rowToEntry = (row, tags) => ({
|
|
7
|
-
uuid: row.uuid,
|
|
8
|
-
batchId: row.batch_id,
|
|
9
|
-
familyHash: row.family_hash ?? undefined,
|
|
10
|
-
type: row.type,
|
|
11
|
-
content: JSON.parse(row.content),
|
|
12
|
-
tags,
|
|
13
|
-
isLatest: Boolean(row.is_latest),
|
|
14
|
-
createdAt: row.created_at,
|
|
15
|
-
});
|
|
16
|
-
const insertTags = async (db, uuid, tags) => {
|
|
17
|
-
if (tags.length === 0)
|
|
18
|
-
return;
|
|
19
|
-
await Promise.all(tags.map(async (tag) => {
|
|
20
|
-
await db.execute(`INSERT OR IGNORE INTO ${TABLE_TAGS} (entry_uuid, tag) VALUES (?, ?)`, [
|
|
21
|
-
uuid,
|
|
22
|
-
tag,
|
|
23
|
-
]);
|
|
24
|
-
}));
|
|
25
|
-
};
|
|
26
|
-
const buildEntryFilters = (opts) => {
|
|
27
|
-
const conditions = [];
|
|
28
|
-
const params = [];
|
|
29
|
-
if (opts.type) {
|
|
30
|
-
conditions.push('e.type = ?');
|
|
31
|
-
params.push(opts.type);
|
|
32
|
-
}
|
|
33
|
-
if (opts.batchId) {
|
|
34
|
-
conditions.push('e.batch_id = ?');
|
|
35
|
-
params.push(opts.batchId);
|
|
36
|
-
}
|
|
37
|
-
if (opts.from) {
|
|
38
|
-
conditions.push('e.created_at >= ?');
|
|
39
|
-
params.push(opts.from);
|
|
40
|
-
}
|
|
41
|
-
if (opts.to) {
|
|
42
|
-
conditions.push('e.created_at <= ?');
|
|
43
|
-
params.push(opts.to);
|
|
44
|
-
}
|
|
45
|
-
let joinClause = '';
|
|
46
|
-
if (opts.tag) {
|
|
47
|
-
joinClause = `INNER JOIN ${TABLE_TAGS} t ON t.entry_uuid = e.uuid AND t.tag = ?`;
|
|
48
|
-
params.unshift(opts.tag);
|
|
49
|
-
}
|
|
50
|
-
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
|
|
51
|
-
const countParams = opts.tag ? [opts.tag, ...params.slice(1)] : [...params];
|
|
52
|
-
return { joinClause, whereClause, params, countParams };
|
|
53
|
-
};
|
|
54
|
-
const loadTagsByUuid = async (db, uuids) => {
|
|
55
|
-
const tagsByUuid = new Map();
|
|
56
|
-
if (uuids.length === 0)
|
|
57
|
-
return tagsByUuid;
|
|
58
|
-
const tagRows = (await db.query(`SELECT entry_uuid, tag FROM ${TABLE_TAGS} WHERE entry_uuid IN (${uuids.map(() => '?').join(',')})`, uuids));
|
|
59
|
-
for (const tagRow of tagRows) {
|
|
60
|
-
const tags = tagsByUuid.get(tagRow.entry_uuid) ?? [];
|
|
61
|
-
tags.push(tagRow.tag);
|
|
62
|
-
tagsByUuid.set(tagRow.entry_uuid, tags);
|
|
63
|
-
}
|
|
64
|
-
return tagsByUuid;
|
|
65
|
-
};
|
|
66
|
-
// The storage facade intentionally groups related DB operations in one factory.
|
|
67
|
-
// eslint-disable-next-line max-lines-per-function
|
|
68
|
-
const createStorage = (db) => {
|
|
69
|
-
const writeEntry = async (entry) => {
|
|
70
|
-
const uuid = entry.uuid || generateUuid();
|
|
71
|
-
await db.execute(`INSERT INTO ${TABLE_ENTRIES} (uuid, batch_id, family_hash, type, content, is_latest, created_at)
|
|
72
|
-
VALUES (?, ?, ?, ?, ?, ?, ?)`, [
|
|
73
|
-
uuid,
|
|
74
|
-
entry.batchId,
|
|
75
|
-
entry.familyHash ?? null,
|
|
76
|
-
entry.type,
|
|
77
|
-
JSON.stringify(entry.content),
|
|
78
|
-
entry.isLatest ? 1 : 0,
|
|
79
|
-
entry.createdAt,
|
|
80
|
-
]);
|
|
81
|
-
await insertTags(db, uuid, entry.tags);
|
|
82
|
-
};
|
|
83
|
-
const updateEntry = async (uuid, patch) => {
|
|
84
|
-
const sets = [];
|
|
85
|
-
const params = [];
|
|
86
|
-
if (patch.content !== undefined) {
|
|
87
|
-
sets.push('content = ?');
|
|
88
|
-
params.push(JSON.stringify(patch.content));
|
|
89
|
-
}
|
|
90
|
-
if (patch.isLatest !== undefined) {
|
|
91
|
-
sets.push('is_latest = ?');
|
|
92
|
-
params.push(patch.isLatest ? 1 : 0);
|
|
93
|
-
}
|
|
94
|
-
if (sets.length === 0)
|
|
95
|
-
return;
|
|
96
|
-
params.push(uuid);
|
|
97
|
-
await db.execute(`UPDATE ${TABLE_ENTRIES} SET ${sets.join(', ')} WHERE uuid = ?`, params);
|
|
98
|
-
};
|
|
99
|
-
const markFamilyStale = async (hash, exceptUuid) => {
|
|
100
|
-
await db.execute(`UPDATE ${TABLE_ENTRIES} SET is_latest = 0
|
|
101
|
-
WHERE family_hash = ? AND uuid != ? AND is_latest = 1`, [hash, exceptUuid]);
|
|
102
|
-
};
|
|
103
|
-
const queryEntries = async (opts) => {
|
|
104
|
-
const page = opts.page ?? 1;
|
|
105
|
-
const perPage = opts.perPage ?? 50;
|
|
106
|
-
const offset = (page - 1) * perPage;
|
|
107
|
-
const { joinClause, whereClause, params, countParams } = buildEntryFilters(opts);
|
|
108
|
-
const countResult = (await db.queryOne(`SELECT COUNT(*) as cnt FROM ${TABLE_ENTRIES} e ${joinClause} ${whereClause}`, countParams));
|
|
109
|
-
const total = countResult?.cnt ?? 0;
|
|
110
|
-
const rows = (await db.query(`SELECT e.id, e.uuid, e.batch_id, e.family_hash, e.type, e.content, e.is_latest, e.created_at
|
|
111
|
-
FROM ${TABLE_ENTRIES} e ${joinClause} ${whereClause}
|
|
112
|
-
ORDER BY e.created_at DESC, e.id DESC
|
|
113
|
-
LIMIT ? OFFSET ?`, [...params, perPage, offset]));
|
|
114
|
-
const tagsByUuid = await loadTagsByUuid(db, rows.map((row) => row.uuid));
|
|
115
|
-
return {
|
|
116
|
-
data: rows.map((row) => rowToEntry(row, tagsByUuid.get(row.uuid) ?? [])),
|
|
117
|
-
total,
|
|
118
|
-
};
|
|
119
|
-
};
|
|
120
|
-
const getEntry = async (uuid) => {
|
|
121
|
-
const row = (await db.queryOne(`SELECT id, uuid, batch_id, family_hash, type, content, is_latest, created_at
|
|
122
|
-
FROM ${TABLE_ENTRIES}
|
|
123
|
-
WHERE uuid = ?`, [uuid]));
|
|
124
|
-
if (!row)
|
|
125
|
-
return null;
|
|
126
|
-
const tags = (await db.query(`SELECT tag FROM ${TABLE_TAGS} WHERE entry_uuid = ?`, [
|
|
127
|
-
uuid,
|
|
128
|
-
]));
|
|
129
|
-
return rowToEntry(row, tags.map((tag) => tag.tag));
|
|
130
|
-
};
|
|
131
|
-
const getBatch = async (batchId) => {
|
|
132
|
-
const rows = (await db.query(`SELECT id, uuid, batch_id, family_hash, type, content, is_latest, created_at
|
|
133
|
-
FROM ${TABLE_ENTRIES}
|
|
134
|
-
WHERE batch_id = ?
|
|
135
|
-
ORDER BY created_at ASC, id ASC`, [batchId]));
|
|
136
|
-
if (rows.length === 0)
|
|
137
|
-
return [];
|
|
138
|
-
const tagsByUuid = await loadTagsByUuid(db, rows.map((row) => row.uuid));
|
|
139
|
-
return rows.map((row) => rowToEntry(row, tagsByUuid.get(row.uuid) ?? []));
|
|
140
|
-
};
|
|
141
|
-
const prune = async (olderThanMs, keepExceptions = false) => {
|
|
142
|
-
const countResult = (await db.queryOne(`SELECT COUNT(*) as cnt FROM ${TABLE_ENTRIES}
|
|
143
|
-
WHERE created_at < ?
|
|
144
|
-
${keepExceptions ? "AND type != 'exception'" : ''}`, [olderThanMs]));
|
|
145
|
-
const deleted = countResult?.cnt ?? 0;
|
|
146
|
-
if (deleted === 0)
|
|
147
|
-
return 0;
|
|
148
|
-
await db.execute(`DELETE FROM ${TABLE_ENTRIES}
|
|
149
|
-
WHERE created_at < ?
|
|
150
|
-
${keepExceptions ? "AND type != 'exception'" : ''}`, [olderThanMs]);
|
|
151
|
-
return deleted;
|
|
152
|
-
};
|
|
153
|
-
const clear = async () => {
|
|
154
|
-
await db.execute(`DELETE FROM ${TABLE_ENTRIES}`, []);
|
|
155
|
-
};
|
|
156
|
-
const getMonitoring = async () => {
|
|
157
|
-
const rows = (await db.query(`SELECT tag FROM ${TABLE_MONITORING}`, []));
|
|
158
|
-
return rows.map((row) => row.tag);
|
|
159
|
-
};
|
|
160
|
-
const addMonitoring = async (tag) => {
|
|
161
|
-
await db.execute(`INSERT OR IGNORE INTO ${TABLE_MONITORING} (tag) VALUES (?)`, [tag]);
|
|
162
|
-
};
|
|
163
|
-
const removeMonitoring = async (tag) => {
|
|
164
|
-
await db.execute(`DELETE FROM ${TABLE_MONITORING} WHERE tag = ?`, [tag]);
|
|
165
|
-
};
|
|
166
|
-
const stats = async () => {
|
|
167
|
-
const rows = (await db.query(`SELECT type, COUNT(*) as cnt FROM ${TABLE_ENTRIES} GROUP BY type`, []));
|
|
168
|
-
const output = {};
|
|
169
|
-
for (const row of rows) {
|
|
170
|
-
output[row.type] = row.cnt;
|
|
171
|
-
}
|
|
172
|
-
return output;
|
|
173
|
-
};
|
|
174
|
-
return {
|
|
175
|
-
writeEntry,
|
|
176
|
-
updateEntry,
|
|
177
|
-
markFamilyStale,
|
|
178
|
-
queryEntries,
|
|
179
|
-
getEntry,
|
|
180
|
-
getBatch,
|
|
181
|
-
prune,
|
|
182
|
-
clear,
|
|
183
|
-
getMonitoring,
|
|
184
|
-
addMonitoring,
|
|
185
|
-
removeMonitoring,
|
|
186
|
-
stats,
|
|
187
|
-
};
|
|
188
|
-
};
|
|
189
|
-
const resolveStorage = (db) => {
|
|
190
|
-
return createStorage(db);
|
|
191
|
-
};
|
|
192
|
-
const reset = () => {
|
|
193
|
-
return;
|
|
194
|
-
};
|
|
195
|
-
export const TraceStorage = Object.freeze({ resolveStorage, reset, familyHash });
|