@zintrust/trace 0.4.75
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +288 -0
- package/dist/build-manifest.json +365 -0
- package/dist/cli-register.d.ts +9 -0
- package/dist/cli-register.js +32 -0
- package/dist/config.d.ts +9 -0
- package/dist/config.js +38 -0
- package/dist/context.d.ts +18 -0
- package/dist/context.js +86 -0
- package/dist/dashboard/handlers.d.ts +15 -0
- package/dist/dashboard/handlers.js +179 -0
- package/dist/dashboard/routes.d.ts +19 -0
- package/dist/dashboard/routes.js +50 -0
- package/dist/dashboard/ui.d.ts +2 -0
- package/dist/dashboard/ui.js +870 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.js +50 -0
- package/dist/migrations/20260331000001_create_zin_debugger_entries_table.d.ts +10 -0
- package/dist/migrations/20260331000001_create_zin_debugger_entries_table.js +28 -0
- package/dist/migrations/20260331000002_create_zin_debugger_entries_tags_table.d.ts +10 -0
- package/dist/migrations/20260331000002_create_zin_debugger_entries_tags_table.js +21 -0
- package/dist/migrations/20260331000003_create_zin_debugger_monitoring_table.d.ts +10 -0
- package/dist/migrations/20260331000003_create_zin_debugger_monitoring_table.js +17 -0
- package/dist/migrations/index.d.ts +6 -0
- package/dist/migrations/index.js +4 -0
- package/dist/plugin.d.ts +1 -0
- package/dist/plugin.js +3 -0
- package/dist/register.d.ts +1 -0
- package/dist/register.js +140 -0
- package/dist/storage/DebuggerStorage.d.ts +13 -0
- package/dist/storage/DebuggerStorage.js +195 -0
- package/dist/storage/TraceStorage.d.ts +13 -0
- package/dist/storage/TraceStorage.js +195 -0
- package/dist/storage/index.d.ts +2 -0
- package/dist/storage/index.js +1 -0
- package/dist/types.d.ts +270 -0
- package/dist/types.js +25 -0
- package/dist/ui.d.ts +8 -0
- package/dist/ui.js +7 -0
- package/dist/utils/authTag.d.ts +5 -0
- package/dist/utils/authTag.js +18 -0
- package/dist/utils/familyHash.d.ts +1 -0
- package/dist/utils/familyHash.js +8 -0
- package/dist/utils/redact.d.ts +6 -0
- package/dist/utils/redact.js +49 -0
- package/dist/utils/requestFilter.d.ts +4 -0
- package/dist/utils/requestFilter.js +26 -0
- package/dist/utils/stackFrame.d.ts +6 -0
- package/dist/utils/stackFrame.js +38 -0
- package/dist/watchers/AuthWatcher.d.ts +6 -0
- package/dist/watchers/AuthWatcher.js +49 -0
- package/dist/watchers/BatchWatcher.d.ts +6 -0
- package/dist/watchers/BatchWatcher.js +46 -0
- package/dist/watchers/CacheWatcher.d.ts +6 -0
- package/dist/watchers/CacheWatcher.js +51 -0
- package/dist/watchers/CommandWatcher.d.ts +6 -0
- package/dist/watchers/CommandWatcher.js +49 -0
- package/dist/watchers/DumpWatcher.d.ts +7 -0
- package/dist/watchers/DumpWatcher.js +41 -0
- package/dist/watchers/EventWatcher.d.ts +6 -0
- package/dist/watchers/EventWatcher.js +42 -0
- package/dist/watchers/ExceptionWatcher.d.ts +4 -0
- package/dist/watchers/ExceptionWatcher.js +103 -0
- package/dist/watchers/GateWatcher.d.ts +6 -0
- package/dist/watchers/GateWatcher.js +45 -0
- package/dist/watchers/HttpClientWatcher.d.ts +6 -0
- package/dist/watchers/HttpClientWatcher.js +50 -0
- package/dist/watchers/HttpWatcher.d.ts +2 -0
- package/dist/watchers/HttpWatcher.js +71 -0
- package/dist/watchers/JobWatcher.d.ts +10 -0
- package/dist/watchers/JobWatcher.js +108 -0
- package/dist/watchers/LogWatcher.d.ts +2 -0
- package/dist/watchers/LogWatcher.js +50 -0
- package/dist/watchers/MailWatcher.d.ts +6 -0
- package/dist/watchers/MailWatcher.js +45 -0
- package/dist/watchers/MiddlewareWatcher.d.ts +6 -0
- package/dist/watchers/MiddlewareWatcher.js +41 -0
- package/dist/watchers/ModelWatcher.d.ts +6 -0
- package/dist/watchers/ModelWatcher.js +42 -0
- package/dist/watchers/NotificationWatcher.d.ts +6 -0
- package/dist/watchers/NotificationWatcher.js +42 -0
- package/dist/watchers/QueryWatcher.d.ts +2 -0
- package/dist/watchers/QueryWatcher.js +72 -0
- package/dist/watchers/RedisWatcher.d.ts +7 -0
- package/dist/watchers/RedisWatcher.js +38 -0
- package/dist/watchers/ScheduleWatcher.d.ts +6 -0
- package/dist/watchers/ScheduleWatcher.js +46 -0
- package/dist/watchers/ViewWatcher.d.ts +6 -0
- package/dist/watchers/ViewWatcher.js +36 -0
- package/package.json +59 -0
- package/src/cli-register.ts +63 -0
- package/src/config.ts +46 -0
- package/src/context.ts +101 -0
- package/src/dashboard/handlers.ts +197 -0
- package/src/dashboard/routes.ts +101 -0
- package/src/dashboard/ui.ts +879 -0
- package/src/dashboard/zintrust-debuger.svg +30 -0
- package/src/index.ts +88 -0
- package/src/plugin.ts +9 -0
- package/src/register.ts +219 -0
- package/src/storage/TraceStorage.ts +306 -0
- package/src/storage/index.ts +2 -0
- package/src/types.ts +317 -0
- package/src/ui.ts +9 -0
- package/src/utils/authTag.ts +20 -0
- package/src/utils/familyHash.ts +8 -0
- package/src/utils/redact.ts +64 -0
- package/src/utils/requestFilter.ts +33 -0
- package/src/utils/stackFrame.ts +44 -0
- package/src/watchers/AuthWatcher.ts +50 -0
- package/src/watchers/BatchWatcher.ts +52 -0
- package/src/watchers/CacheWatcher.ts +58 -0
- package/src/watchers/CommandWatcher.ts +55 -0
- package/src/watchers/DumpWatcher.ts +42 -0
- package/src/watchers/EventWatcher.ts +43 -0
- package/src/watchers/ExceptionWatcher.ts +114 -0
- package/src/watchers/GateWatcher.ts +50 -0
- package/src/watchers/HttpClientWatcher.ts +56 -0
- package/src/watchers/HttpWatcher.ts +94 -0
- package/src/watchers/JobWatcher.ts +121 -0
- package/src/watchers/LogWatcher.ts +61 -0
- package/src/watchers/MailWatcher.ts +47 -0
- package/src/watchers/MiddlewareWatcher.ts +42 -0
- package/src/watchers/ModelWatcher.ts +48 -0
- package/src/watchers/NotificationWatcher.ts +43 -0
- package/src/watchers/QueryWatcher.ts +85 -0
- package/src/watchers/RedisWatcher.ts +39 -0
- package/src/watchers/ScheduleWatcher.ts +54 -0
- package/src/watchers/ViewWatcher.ts +37 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
<svg width="120" height="120" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<defs>
|
|
3
|
+
<linearGradient id="zt-g1e" x1="15" y1="15" x2="85" y2="85" gradientUnits="userSpaceOnUse">
|
|
4
|
+
<stop stop-color="#38bdf8" />
|
|
5
|
+
<stop offset="1" stop-color="#22c55e" />
|
|
6
|
+
</linearGradient>
|
|
7
|
+
</defs>
|
|
8
|
+
<path
|
|
9
|
+
d="M50 8L18 22V46C18 66.2 32 84.1 50 92C68 84.1 82 66.2 82 46V22L50 8Z"
|
|
10
|
+
stroke="url(#zt-g1e)"
|
|
11
|
+
stroke-width="6"
|
|
12
|
+
stroke-linejoin="round"
|
|
13
|
+
/>
|
|
14
|
+
<path
|
|
15
|
+
d="M34 54H42L46 44L52 62L58 50H66"
|
|
16
|
+
stroke="white"
|
|
17
|
+
stroke-width="8"
|
|
18
|
+
stroke-linecap="round"
|
|
19
|
+
stroke-linejoin="round"
|
|
20
|
+
/>
|
|
21
|
+
<circle cx="34" cy="54" r="2.8" fill="white" fill-opacity="0.7" />
|
|
22
|
+
<circle cx="66" cy="50" r="2.8" fill="white" fill-opacity="0.7" />
|
|
23
|
+
<path
|
|
24
|
+
d="M30 28H70"
|
|
25
|
+
stroke="white"
|
|
26
|
+
stroke-opacity="0.12"
|
|
27
|
+
stroke-width="3"
|
|
28
|
+
stroke-linecap="round"
|
|
29
|
+
/>
|
|
30
|
+
</svg>
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @zintrust/trace — public API surface.
|
|
3
|
+
*
|
|
4
|
+
* Zero side-effects. Import watchers, storage, dashboard, and config
|
|
5
|
+
* individually. For full auto-initialisation, use:
|
|
6
|
+
* import '@zintrust/trace/register';
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
// Config
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
export { TraceConfig } from './config';
|
|
13
|
+
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// Storage
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
export { TraceStorage } from './storage';
|
|
18
|
+
export type { ITraceStorage } from './storage';
|
|
19
|
+
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
// Context
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
export { TraceContext } from './context';
|
|
24
|
+
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
// Dashboard
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
export { registerTraceDashboard, registerTraceRoutes } from './dashboard/routes';
|
|
29
|
+
export type { TraceDashboardOptions, TraceDashboardRegistrationOptions } from './dashboard/routes';
|
|
30
|
+
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
// Watchers (named re-exports for use with custom wiring)
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
export { AuthWatcher } from './watchers/AuthWatcher';
|
|
35
|
+
export { BatchWatcher } from './watchers/BatchWatcher';
|
|
36
|
+
export { CacheWatcher } from './watchers/CacheWatcher';
|
|
37
|
+
export { CommandWatcher } from './watchers/CommandWatcher';
|
|
38
|
+
export { DumpWatcher } from './watchers/DumpWatcher';
|
|
39
|
+
export { EventWatcher } from './watchers/EventWatcher';
|
|
40
|
+
export { ExceptionWatcher } from './watchers/ExceptionWatcher';
|
|
41
|
+
export { GateWatcher } from './watchers/GateWatcher';
|
|
42
|
+
export { HttpClientWatcher } from './watchers/HttpClientWatcher';
|
|
43
|
+
export { HttpWatcher } from './watchers/HttpWatcher';
|
|
44
|
+
export { JobWatcher } from './watchers/JobWatcher';
|
|
45
|
+
export { LogWatcher } from './watchers/LogWatcher';
|
|
46
|
+
export { MailWatcher } from './watchers/MailWatcher';
|
|
47
|
+
export { MiddlewareWatcher } from './watchers/MiddlewareWatcher';
|
|
48
|
+
export { ModelWatcher } from './watchers/ModelWatcher';
|
|
49
|
+
export { NotificationWatcher } from './watchers/NotificationWatcher';
|
|
50
|
+
export { QueryWatcher } from './watchers/QueryWatcher';
|
|
51
|
+
export { RedisWatcher } from './watchers/RedisWatcher';
|
|
52
|
+
export { ScheduleWatcher } from './watchers/ScheduleWatcher';
|
|
53
|
+
export { ViewWatcher } from './watchers/ViewWatcher';
|
|
54
|
+
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
// Types
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
export { EntryType } from './types';
|
|
59
|
+
export type {
|
|
60
|
+
AuthContent,
|
|
61
|
+
BatchContent,
|
|
62
|
+
CacheContent,
|
|
63
|
+
ClientRequestContent,
|
|
64
|
+
CommandContent,
|
|
65
|
+
DumpContent,
|
|
66
|
+
EntryTypeValue,
|
|
67
|
+
EventContent,
|
|
68
|
+
ExceptionContent,
|
|
69
|
+
GateContent,
|
|
70
|
+
ITraceConfig,
|
|
71
|
+
ITraceEntry,
|
|
72
|
+
ITraceWatcher,
|
|
73
|
+
ITraceWatcherConfig,
|
|
74
|
+
JobContent,
|
|
75
|
+
LogContent,
|
|
76
|
+
MailContent,
|
|
77
|
+
MiddlewareContent,
|
|
78
|
+
ModelContent,
|
|
79
|
+
NotificationContent,
|
|
80
|
+
QueryContent,
|
|
81
|
+
RedactionConfig,
|
|
82
|
+
RedisContent,
|
|
83
|
+
RequestContent,
|
|
84
|
+
ScheduleContent,
|
|
85
|
+
TraceConfigOverrides,
|
|
86
|
+
ViewContent,
|
|
87
|
+
WatcherToggles,
|
|
88
|
+
} from './types';
|
package/src/plugin.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export type {};
|
|
2
|
+
|
|
3
|
+
type GlobalTracePluginState = {
|
|
4
|
+
__zintrust_system_trace_plugin_requested__?: boolean;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
const globalTracePluginState = globalThis as unknown as GlobalTracePluginState;
|
|
8
|
+
|
|
9
|
+
globalTracePluginState.__zintrust_system_trace_plugin_requested__ = true;
|
package/src/register.ts
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @zintrust/trace register side-effect module.
|
|
3
|
+
*
|
|
4
|
+
* For plugin-file activation, prefer:
|
|
5
|
+
* import '@zintrust/trace/plugin';
|
|
6
|
+
*
|
|
7
|
+
* The framework boot layer will lazy-load this register module once the app
|
|
8
|
+
* runtime is ready. Importing this file directly is still supported for
|
|
9
|
+
* advanced manual bootstrap flows that intentionally activate the trace
|
|
10
|
+
* after databases and the kernel are available.
|
|
11
|
+
*
|
|
12
|
+
* Config is read from environment variables (TRACE_* keys) matching
|
|
13
|
+
* the defaults in TraceConfig. For custom overrides supply them via
|
|
14
|
+
* calling `initTrace(overrides)` instead.
|
|
15
|
+
*
|
|
16
|
+
* Routes are NOT auto-mounted here. Wire the dashboard into your router:
|
|
17
|
+
* import { registerTraceDashboard } from '@zintrust/trace/ui';
|
|
18
|
+
* registerTraceDashboard(router, {
|
|
19
|
+
* middleware: ['admin'],
|
|
20
|
+
* });
|
|
21
|
+
*/
|
|
22
|
+
import { TraceConfig } from './config';
|
|
23
|
+
import { TraceContext } from './context';
|
|
24
|
+
import { TraceStorage } from './storage';
|
|
25
|
+
import type { ITraceWatcherConfig } from './types';
|
|
26
|
+
|
|
27
|
+
export type {}; // side-effect ESM module
|
|
28
|
+
|
|
29
|
+
type GlobalTraceRegisterState = {
|
|
30
|
+
__zintrust_system_trace_register_initialized__?: boolean;
|
|
31
|
+
__zintrust_system_trace_plugin_requested__?: boolean;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const globalTraceRegisterState = globalThis as unknown as GlobalTraceRegisterState;
|
|
35
|
+
globalTraceRegisterState.__zintrust_system_trace_plugin_requested__ = true;
|
|
36
|
+
const traceAlreadyInitialized =
|
|
37
|
+
globalTraceRegisterState.__zintrust_system_trace_register_initialized__ === true;
|
|
38
|
+
|
|
39
|
+
if (!traceAlreadyInitialized) {
|
|
40
|
+
globalTraceRegisterState.__zintrust_system_trace_register_initialized__ = true;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const importCore = async (): Promise<unknown> => {
|
|
44
|
+
try {
|
|
45
|
+
return await import('@zintrust/core');
|
|
46
|
+
} catch {
|
|
47
|
+
return {};
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
type CoreApi = {
|
|
52
|
+
Env?: {
|
|
53
|
+
getBool(key: string, fallback: boolean): boolean;
|
|
54
|
+
get(key: string, fallback: string): string;
|
|
55
|
+
getInt(key: string, fallback: number): number;
|
|
56
|
+
};
|
|
57
|
+
useDatabase?: (config?: unknown, connection?: string) => import('@zintrust/core').IDatabase;
|
|
58
|
+
RequestContext?: {
|
|
59
|
+
current(): unknown;
|
|
60
|
+
};
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
type GlobalMiddlewareRegistrarState = {
|
|
64
|
+
__zintrust_register_global_middleware__?: ITraceWatcherConfig['registerMiddleware'];
|
|
65
|
+
__zintrust_pending_global_middlewares__?: Array<
|
|
66
|
+
Parameters<NonNullable<ITraceWatcherConfig['registerMiddleware']>>[0]
|
|
67
|
+
>;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const resolveRegisterMiddleware = (): NonNullable<ITraceWatcherConfig['registerMiddleware']> => {
|
|
71
|
+
const globalMiddlewareRegistrarState = globalThis as unknown as GlobalMiddlewareRegistrarState;
|
|
72
|
+
|
|
73
|
+
return (middleware): void => {
|
|
74
|
+
const registerMiddleware =
|
|
75
|
+
globalMiddlewareRegistrarState.__zintrust_register_global_middleware__;
|
|
76
|
+
if (typeof registerMiddleware === 'function') {
|
|
77
|
+
registerMiddleware(middleware);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
globalMiddlewareRegistrarState.__zintrust_pending_global_middlewares__ ??= [];
|
|
82
|
+
globalMiddlewareRegistrarState.__zintrust_pending_global_middlewares__.push(middleware);
|
|
83
|
+
};
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const resolveTraceConnectionName = (
|
|
87
|
+
env: Pick<NonNullable<CoreApi['Env']>, 'get'> | undefined,
|
|
88
|
+
configuredConnection?: string
|
|
89
|
+
): string => {
|
|
90
|
+
const resolveDefaultConnection = (): string => {
|
|
91
|
+
const defaultConnection = env?.get('DB_CONNECTION', '').trim() ?? '';
|
|
92
|
+
if (defaultConnection === '' || defaultConnection === 'default') return 'default';
|
|
93
|
+
return defaultConnection;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const explicitConnection = configuredConnection?.trim();
|
|
97
|
+
if (explicitConnection !== undefined && explicitConnection !== '') {
|
|
98
|
+
return explicitConnection === 'default' ? resolveDefaultConnection() : explicitConnection;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return resolveDefaultConnection();
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const core = (await importCore()) as CoreApi;
|
|
105
|
+
const Env = core.Env;
|
|
106
|
+
|
|
107
|
+
if (!traceAlreadyInitialized && Env) {
|
|
108
|
+
const enabled = Env.getBool('TRACE_ENABLED', false);
|
|
109
|
+
|
|
110
|
+
if (enabled) {
|
|
111
|
+
const connection = Env.get('TRACE_DB_CONNECTION', '') || undefined;
|
|
112
|
+
const pruneAfterHours = Env.getInt('TRACE_PRUNE_HOURS', 24);
|
|
113
|
+
const slowQueryThreshold = Env.getInt('TRACE_SLOW_QUERY_MS', 100);
|
|
114
|
+
const logMinLevel = Env.get('TRACE_LOG_LEVEL', 'info') as
|
|
115
|
+
| 'debug'
|
|
116
|
+
| 'info'
|
|
117
|
+
| 'warn'
|
|
118
|
+
| 'error'
|
|
119
|
+
| 'fatal';
|
|
120
|
+
|
|
121
|
+
const config = TraceConfig.merge({
|
|
122
|
+
enabled,
|
|
123
|
+
connection,
|
|
124
|
+
pruneAfterHours,
|
|
125
|
+
slowQueryThreshold,
|
|
126
|
+
logMinLevel,
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
const db = core.useDatabase?.(undefined, resolveTraceConnectionName(Env, connection));
|
|
130
|
+
|
|
131
|
+
if (db) {
|
|
132
|
+
const storage = TraceStorage.resolveStorage(db);
|
|
133
|
+
|
|
134
|
+
if (core.RequestContext) {
|
|
135
|
+
TraceContext.setRequestContextImpl(
|
|
136
|
+
core.RequestContext as {
|
|
137
|
+
current?: () => unknown;
|
|
138
|
+
peek?: () => unknown;
|
|
139
|
+
}
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const [
|
|
144
|
+
{ HttpWatcher },
|
|
145
|
+
{ QueryWatcher },
|
|
146
|
+
{ LogWatcher },
|
|
147
|
+
{ ExceptionWatcher },
|
|
148
|
+
{ JobWatcher },
|
|
149
|
+
{ CacheWatcher },
|
|
150
|
+
{ ScheduleWatcher },
|
|
151
|
+
{ MailWatcher },
|
|
152
|
+
{ AuthWatcher },
|
|
153
|
+
{ EventWatcher },
|
|
154
|
+
{ ModelWatcher },
|
|
155
|
+
{ NotificationWatcher },
|
|
156
|
+
{ RedisWatcher },
|
|
157
|
+
{ GateWatcher },
|
|
158
|
+
{ MiddlewareWatcher },
|
|
159
|
+
{ CommandWatcher },
|
|
160
|
+
{ BatchWatcher },
|
|
161
|
+
{ DumpWatcher },
|
|
162
|
+
{ ViewWatcher },
|
|
163
|
+
{ HttpClientWatcher },
|
|
164
|
+
] = await Promise.all([
|
|
165
|
+
import('./watchers/HttpWatcher'),
|
|
166
|
+
import('./watchers/QueryWatcher'),
|
|
167
|
+
import('./watchers/LogWatcher'),
|
|
168
|
+
import('./watchers/ExceptionWatcher'),
|
|
169
|
+
import('./watchers/JobWatcher'),
|
|
170
|
+
import('./watchers/CacheWatcher'),
|
|
171
|
+
import('./watchers/ScheduleWatcher'),
|
|
172
|
+
import('./watchers/MailWatcher'),
|
|
173
|
+
import('./watchers/AuthWatcher'),
|
|
174
|
+
import('./watchers/EventWatcher'),
|
|
175
|
+
import('./watchers/ModelWatcher'),
|
|
176
|
+
import('./watchers/NotificationWatcher'),
|
|
177
|
+
import('./watchers/RedisWatcher'),
|
|
178
|
+
import('./watchers/GateWatcher'),
|
|
179
|
+
import('./watchers/MiddlewareWatcher'),
|
|
180
|
+
import('./watchers/CommandWatcher'),
|
|
181
|
+
import('./watchers/BatchWatcher'),
|
|
182
|
+
import('./watchers/DumpWatcher'),
|
|
183
|
+
import('./watchers/ViewWatcher'),
|
|
184
|
+
import('./watchers/HttpClientWatcher'),
|
|
185
|
+
]);
|
|
186
|
+
|
|
187
|
+
const watcherArgs = { storage, config, db };
|
|
188
|
+
|
|
189
|
+
HttpWatcher.register({ ...watcherArgs, registerMiddleware: resolveRegisterMiddleware() });
|
|
190
|
+
|
|
191
|
+
QueryWatcher.register(watcherArgs);
|
|
192
|
+
LogWatcher.register(watcherArgs);
|
|
193
|
+
ExceptionWatcher.register(watcherArgs);
|
|
194
|
+
JobWatcher.register(watcherArgs);
|
|
195
|
+
CacheWatcher.register(watcherArgs);
|
|
196
|
+
ScheduleWatcher.register(watcherArgs);
|
|
197
|
+
MailWatcher.register(watcherArgs);
|
|
198
|
+
AuthWatcher.register(watcherArgs);
|
|
199
|
+
EventWatcher.register(watcherArgs);
|
|
200
|
+
ModelWatcher.register(watcherArgs);
|
|
201
|
+
NotificationWatcher.register(watcherArgs);
|
|
202
|
+
RedisWatcher.register(watcherArgs);
|
|
203
|
+
GateWatcher.register(watcherArgs);
|
|
204
|
+
MiddlewareWatcher.register(watcherArgs);
|
|
205
|
+
CommandWatcher.register(watcherArgs);
|
|
206
|
+
BatchWatcher.register(watcherArgs);
|
|
207
|
+
DumpWatcher.register(watcherArgs);
|
|
208
|
+
ViewWatcher.register(watcherArgs);
|
|
209
|
+
HttpClientWatcher.register(watcherArgs);
|
|
210
|
+
} else {
|
|
211
|
+
// eslint-disable-next-line no-console
|
|
212
|
+
console.warn('[trace] Could not resolve database connection - skipping init.');
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
} else if (!traceAlreadyInitialized) {
|
|
216
|
+
// Running outside a ZinTrust project - skip init silently.
|
|
217
|
+
// eslint-disable-next-line no-console
|
|
218
|
+
console.warn('[trace] @zintrust/core not found - trace will not be activated.');
|
|
219
|
+
}
|
|
@@ -0,0 +1,306 @@
|
|
|
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 { EntryTypeValue, ITraceEntry, ITraceStorage, QueryEntriesOptions } from '../types';
|
|
8
|
+
import { familyHash } from '../utils/familyHash';
|
|
9
|
+
|
|
10
|
+
const TABLE_ENTRIES = 'zin_trace_entries';
|
|
11
|
+
const TABLE_TAGS = 'zin_trace_entries_tags';
|
|
12
|
+
const TABLE_MONITORING = 'zin_trace_monitoring';
|
|
13
|
+
|
|
14
|
+
const generateUuid = (): string => crypto.randomUUID();
|
|
15
|
+
|
|
16
|
+
type EntryRow = {
|
|
17
|
+
id: number;
|
|
18
|
+
uuid: string;
|
|
19
|
+
batch_id: string;
|
|
20
|
+
family_hash: string | null;
|
|
21
|
+
type: string;
|
|
22
|
+
content: string;
|
|
23
|
+
is_latest: number | boolean;
|
|
24
|
+
created_at: number;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
type TagRow = { entry_uuid: string; tag: string };
|
|
28
|
+
|
|
29
|
+
const rowToEntry = (row: EntryRow, tags: string[]): ITraceEntry => ({
|
|
30
|
+
uuid: row.uuid,
|
|
31
|
+
batchId: row.batch_id,
|
|
32
|
+
familyHash: row.family_hash ?? undefined,
|
|
33
|
+
type: row.type as EntryTypeValue,
|
|
34
|
+
content: JSON.parse(row.content) as unknown,
|
|
35
|
+
tags,
|
|
36
|
+
isLatest: Boolean(row.is_latest),
|
|
37
|
+
createdAt: row.created_at,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const insertTags = async (db: IDatabase, uuid: string, tags: string[]): Promise<void> => {
|
|
41
|
+
if (tags.length === 0) return;
|
|
42
|
+
|
|
43
|
+
await Promise.all(
|
|
44
|
+
tags.map(async (tag) => {
|
|
45
|
+
await db.execute(`INSERT OR IGNORE INTO ${TABLE_TAGS} (entry_uuid, tag) VALUES (?, ?)`, [
|
|
46
|
+
uuid,
|
|
47
|
+
tag,
|
|
48
|
+
]);
|
|
49
|
+
})
|
|
50
|
+
);
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const buildEntryFilters = (
|
|
54
|
+
opts: QueryEntriesOptions
|
|
55
|
+
): { joinClause: string; whereClause: string; params: unknown[]; countParams: unknown[] } => {
|
|
56
|
+
const conditions: string[] = [];
|
|
57
|
+
const params: unknown[] = [];
|
|
58
|
+
|
|
59
|
+
if (opts.type) {
|
|
60
|
+
conditions.push('e.type = ?');
|
|
61
|
+
params.push(opts.type);
|
|
62
|
+
}
|
|
63
|
+
if (opts.batchId) {
|
|
64
|
+
conditions.push('e.batch_id = ?');
|
|
65
|
+
params.push(opts.batchId);
|
|
66
|
+
}
|
|
67
|
+
if (opts.from) {
|
|
68
|
+
conditions.push('e.created_at >= ?');
|
|
69
|
+
params.push(opts.from);
|
|
70
|
+
}
|
|
71
|
+
if (opts.to) {
|
|
72
|
+
conditions.push('e.created_at <= ?');
|
|
73
|
+
params.push(opts.to);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
let joinClause = '';
|
|
77
|
+
if (opts.tag) {
|
|
78
|
+
joinClause = `INNER JOIN ${TABLE_TAGS} t ON t.entry_uuid = e.uuid AND t.tag = ?`;
|
|
79
|
+
params.unshift(opts.tag);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
|
|
83
|
+
const countParams = opts.tag ? [opts.tag, ...params.slice(1)] : [...params];
|
|
84
|
+
|
|
85
|
+
return { joinClause, whereClause, params, countParams };
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const loadTagsByUuid = async (db: IDatabase, uuids: string[]): Promise<Map<string, string[]>> => {
|
|
89
|
+
const tagsByUuid = new Map<string, string[]>();
|
|
90
|
+
if (uuids.length === 0) return tagsByUuid;
|
|
91
|
+
|
|
92
|
+
const tagRows = (await db.query(
|
|
93
|
+
`SELECT entry_uuid, tag FROM ${TABLE_TAGS} WHERE entry_uuid IN (${uuids.map(() => '?').join(',')})`,
|
|
94
|
+
uuids
|
|
95
|
+
)) as TagRow[];
|
|
96
|
+
|
|
97
|
+
for (const tagRow of tagRows) {
|
|
98
|
+
const tags = tagsByUuid.get(tagRow.entry_uuid) ?? [];
|
|
99
|
+
tags.push(tagRow.tag);
|
|
100
|
+
tagsByUuid.set(tagRow.entry_uuid, tags);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return tagsByUuid;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
// The storage facade intentionally groups related DB operations in one factory.
|
|
107
|
+
// eslint-disable-next-line max-lines-per-function
|
|
108
|
+
const createStorage = (db: IDatabase): ITraceStorage => {
|
|
109
|
+
const writeEntry = async (entry: ITraceEntry): Promise<void> => {
|
|
110
|
+
const uuid = entry.uuid || generateUuid();
|
|
111
|
+
await db.execute(
|
|
112
|
+
`INSERT INTO ${TABLE_ENTRIES} (uuid, batch_id, family_hash, type, content, is_latest, created_at)
|
|
113
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
114
|
+
[
|
|
115
|
+
uuid,
|
|
116
|
+
entry.batchId,
|
|
117
|
+
entry.familyHash ?? null,
|
|
118
|
+
entry.type,
|
|
119
|
+
JSON.stringify(entry.content),
|
|
120
|
+
entry.isLatest ? 1 : 0,
|
|
121
|
+
entry.createdAt,
|
|
122
|
+
]
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
await insertTags(db, uuid, entry.tags);
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const updateEntry = async (
|
|
129
|
+
uuid: string,
|
|
130
|
+
patch: Partial<Pick<ITraceEntry, 'content' | 'isLatest'>>
|
|
131
|
+
): Promise<void> => {
|
|
132
|
+
const sets: string[] = [];
|
|
133
|
+
const params: unknown[] = [];
|
|
134
|
+
|
|
135
|
+
if (patch.content !== undefined) {
|
|
136
|
+
sets.push('content = ?');
|
|
137
|
+
params.push(JSON.stringify(patch.content));
|
|
138
|
+
}
|
|
139
|
+
if (patch.isLatest !== undefined) {
|
|
140
|
+
sets.push('is_latest = ?');
|
|
141
|
+
params.push(patch.isLatest ? 1 : 0);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (sets.length === 0) return;
|
|
145
|
+
params.push(uuid);
|
|
146
|
+
|
|
147
|
+
await db.execute(`UPDATE ${TABLE_ENTRIES} SET ${sets.join(', ')} WHERE uuid = ?`, params);
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const markFamilyStale = async (hash: string, exceptUuid: string): Promise<void> => {
|
|
151
|
+
await db.execute(
|
|
152
|
+
`UPDATE ${TABLE_ENTRIES} SET is_latest = 0
|
|
153
|
+
WHERE family_hash = ? AND uuid != ? AND is_latest = 1`,
|
|
154
|
+
[hash, exceptUuid]
|
|
155
|
+
);
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
const queryEntries = async (
|
|
159
|
+
opts: QueryEntriesOptions
|
|
160
|
+
): Promise<{ data: ITraceEntry[]; total: number }> => {
|
|
161
|
+
const page = opts.page ?? 1;
|
|
162
|
+
const perPage = opts.perPage ?? 50;
|
|
163
|
+
const offset = (page - 1) * perPage;
|
|
164
|
+
const { joinClause, whereClause, params, countParams } = buildEntryFilters(opts);
|
|
165
|
+
|
|
166
|
+
const countResult = (await db.queryOne(
|
|
167
|
+
`SELECT COUNT(*) as cnt FROM ${TABLE_ENTRIES} e ${joinClause} ${whereClause}`,
|
|
168
|
+
countParams
|
|
169
|
+
)) as { cnt: number } | undefined;
|
|
170
|
+
const total = countResult?.cnt ?? 0;
|
|
171
|
+
|
|
172
|
+
const rows = (await db.query(
|
|
173
|
+
`SELECT e.id, e.uuid, e.batch_id, e.family_hash, e.type, e.content, e.is_latest, e.created_at
|
|
174
|
+
FROM ${TABLE_ENTRIES} e ${joinClause} ${whereClause}
|
|
175
|
+
ORDER BY e.created_at DESC, e.id DESC
|
|
176
|
+
LIMIT ? OFFSET ?`,
|
|
177
|
+
[...params, perPage, offset]
|
|
178
|
+
)) as EntryRow[];
|
|
179
|
+
|
|
180
|
+
const tagsByUuid = await loadTagsByUuid(
|
|
181
|
+
db,
|
|
182
|
+
rows.map((row) => row.uuid)
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
return {
|
|
186
|
+
data: rows.map((row) => rowToEntry(row, tagsByUuid.get(row.uuid) ?? [])),
|
|
187
|
+
total,
|
|
188
|
+
};
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
const getEntry = async (uuid: string): Promise<ITraceEntry | null> => {
|
|
192
|
+
const row = (await db.queryOne(
|
|
193
|
+
`SELECT id, uuid, batch_id, family_hash, type, content, is_latest, created_at
|
|
194
|
+
FROM ${TABLE_ENTRIES}
|
|
195
|
+
WHERE uuid = ?`,
|
|
196
|
+
[uuid]
|
|
197
|
+
)) as EntryRow | undefined;
|
|
198
|
+
if (!row) return null;
|
|
199
|
+
|
|
200
|
+
const tags = (await db.query(`SELECT tag FROM ${TABLE_TAGS} WHERE entry_uuid = ?`, [
|
|
201
|
+
uuid,
|
|
202
|
+
])) as Array<{
|
|
203
|
+
tag: string;
|
|
204
|
+
}>;
|
|
205
|
+
return rowToEntry(
|
|
206
|
+
row,
|
|
207
|
+
tags.map((tag) => tag.tag)
|
|
208
|
+
);
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
const getBatch = async (batchId: string): Promise<ITraceEntry[]> => {
|
|
212
|
+
const rows = (await db.query(
|
|
213
|
+
`SELECT id, uuid, batch_id, family_hash, type, content, is_latest, created_at
|
|
214
|
+
FROM ${TABLE_ENTRIES}
|
|
215
|
+
WHERE batch_id = ?
|
|
216
|
+
ORDER BY created_at ASC, id ASC`,
|
|
217
|
+
[batchId]
|
|
218
|
+
)) as EntryRow[];
|
|
219
|
+
if (rows.length === 0) return [];
|
|
220
|
+
|
|
221
|
+
const tagsByUuid = await loadTagsByUuid(
|
|
222
|
+
db,
|
|
223
|
+
rows.map((row) => row.uuid)
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
return rows.map((row) => rowToEntry(row, tagsByUuid.get(row.uuid) ?? []));
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
const prune = async (olderThanMs: number, keepExceptions = false): Promise<number> => {
|
|
230
|
+
const countResult = (await db.queryOne(
|
|
231
|
+
`SELECT COUNT(*) as cnt FROM ${TABLE_ENTRIES}
|
|
232
|
+
WHERE created_at < ?
|
|
233
|
+
${keepExceptions ? "AND type != 'exception'" : ''}`,
|
|
234
|
+
[olderThanMs]
|
|
235
|
+
)) as { cnt: number } | undefined;
|
|
236
|
+
const deleted = countResult?.cnt ?? 0;
|
|
237
|
+
if (deleted === 0) return 0;
|
|
238
|
+
|
|
239
|
+
await db.execute(
|
|
240
|
+
`DELETE FROM ${TABLE_ENTRIES}
|
|
241
|
+
WHERE created_at < ?
|
|
242
|
+
${keepExceptions ? "AND type != 'exception'" : ''}`,
|
|
243
|
+
[olderThanMs]
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
return deleted;
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
const clear = async (): Promise<void> => {
|
|
250
|
+
await db.execute(`DELETE FROM ${TABLE_ENTRIES}`, []);
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
const getMonitoring = async (): Promise<string[]> => {
|
|
254
|
+
const rows = (await db.query(`SELECT tag FROM ${TABLE_MONITORING}`, [])) as Array<{
|
|
255
|
+
tag: string;
|
|
256
|
+
}>;
|
|
257
|
+
return rows.map((row) => row.tag);
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
const addMonitoring = async (tag: string): Promise<void> => {
|
|
261
|
+
await db.execute(`INSERT OR IGNORE INTO ${TABLE_MONITORING} (tag) VALUES (?)`, [tag]);
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
const removeMonitoring = async (tag: string): Promise<void> => {
|
|
265
|
+
await db.execute(`DELETE FROM ${TABLE_MONITORING} WHERE tag = ?`, [tag]);
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
const stats = async (): Promise<Record<EntryTypeValue, number>> => {
|
|
269
|
+
const rows = (await db.query(
|
|
270
|
+
`SELECT type, COUNT(*) as cnt FROM ${TABLE_ENTRIES} GROUP BY type`,
|
|
271
|
+
[]
|
|
272
|
+
)) as Array<{ type: string; cnt: number }>;
|
|
273
|
+
const output: Record<string, number> = {};
|
|
274
|
+
for (const row of rows) {
|
|
275
|
+
output[row.type] = row.cnt;
|
|
276
|
+
}
|
|
277
|
+
return output as Record<EntryTypeValue, number>;
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
return {
|
|
281
|
+
writeEntry,
|
|
282
|
+
updateEntry,
|
|
283
|
+
markFamilyStale,
|
|
284
|
+
queryEntries,
|
|
285
|
+
getEntry,
|
|
286
|
+
getBatch,
|
|
287
|
+
prune,
|
|
288
|
+
clear,
|
|
289
|
+
getMonitoring,
|
|
290
|
+
addMonitoring,
|
|
291
|
+
removeMonitoring,
|
|
292
|
+
stats,
|
|
293
|
+
};
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
const resolveStorage = (db: IDatabase): ITraceStorage => {
|
|
297
|
+
return createStorage(db);
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
const reset = (): void => {
|
|
301
|
+
return;
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
export const TraceStorage = Object.freeze({ resolveStorage, reset, familyHash });
|
|
305
|
+
|
|
306
|
+
export { type ITraceStorage } from '../types';
|