@zintrust/trace 0.4.75 → 0.4.77
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 +101 -15
- package/dist/build-manifest.json +210 -162
- package/dist/config.d.ts +1 -0
- package/dist/config.js +123 -4
- package/dist/dashboard/routes.js +4 -4
- package/dist/dashboard/ui.js +80 -23
- package/dist/index.d.ts +2 -0
- package/dist/index.js +30 -25
- package/dist/migrations/{20260331000001_create_zin_debugger_entries_table.d.ts → 20260331000001_create_zin_trace_entries_table.d.ts} +2 -2
- package/dist/migrations/{20260331000001_create_zin_debugger_entries_table.js → 20260331000001_create_zin_trace_entries_table.js} +5 -5
- package/dist/migrations/{20260331000002_create_zin_debugger_entries_tags_table.d.ts → 20260331000002_create_zin_trace_entries_tags_table.d.ts} +2 -2
- package/dist/migrations/{20260331000002_create_zin_debugger_entries_tags_table.js → 20260331000002_create_zin_trace_entries_tags_table.js} +5 -5
- package/dist/migrations/{20260331000003_create_zin_debugger_monitoring_table.d.ts → 20260331000003_create_zin_trace_monitoring_table.d.ts} +2 -2
- package/dist/migrations/{20260331000003_create_zin_debugger_monitoring_table.js → 20260331000003_create_zin_trace_monitoring_table.js} +4 -4
- package/dist/migrations/20260407193000_widen_trace_created_at_for_sql.d.ts +10 -0
- package/dist/migrations/20260407193000_widen_trace_created_at_for_sql.js +34 -0
- package/dist/migrations/index.d.ts +3 -3
- package/dist/migrations/index.js +5 -4
- package/dist/register.js +130 -32
- package/dist/storage/DebuggerStorage.js +1 -1
- package/dist/storage/TraceContentRedaction.d.ts +4 -0
- package/dist/storage/TraceContentRedaction.js +33 -0
- package/dist/storage/TraceEntryFiltering.d.ts +4 -0
- package/dist/storage/TraceEntryFiltering.js +13 -0
- package/dist/storage/TraceStorage.js +36 -6
- package/dist/storage/TraceWriteDiagnostics.d.ts +19 -0
- package/dist/storage/TraceWriteDiagnostics.js +98 -0
- package/dist/storage/index.js +1 -1
- package/dist/types.d.ts +37 -20
- package/dist/ui.js +1 -1
- package/dist/utils/authTag.js +1 -1
- package/dist/utils/entryFilter.d.ts +4 -0
- package/dist/utils/entryFilter.js +95 -0
- package/dist/utils/redact.d.ts +1 -0
- package/dist/utils/redact.js +43 -9
- package/dist/utils/requestFilter.js +1 -1
- package/dist/watchers/AuthWatcher.js +3 -3
- package/dist/watchers/BatchWatcher.js +3 -3
- package/dist/watchers/CacheWatcher.js +5 -5
- package/dist/watchers/CommandWatcher.js +5 -5
- package/dist/watchers/DumpWatcher.js +3 -3
- package/dist/watchers/EventWatcher.js +4 -4
- package/dist/watchers/ExceptionWatcher.js +6 -6
- package/dist/watchers/GateWatcher.js +3 -3
- package/dist/watchers/HttpClientWatcher.js +6 -6
- package/dist/watchers/HttpWatcher.js +108 -24
- package/dist/watchers/JobWatcher.js +4 -4
- package/dist/watchers/LogWatcher.js +5 -4
- package/dist/watchers/MailWatcher.js +3 -3
- package/dist/watchers/MiddlewareWatcher.js +3 -3
- package/dist/watchers/ModelWatcher.js +3 -3
- package/dist/watchers/NotificationWatcher.js +4 -4
- package/dist/watchers/QueryWatcher.js +5 -5
- package/dist/watchers/RedisWatcher.js +4 -4
- package/dist/watchers/ScheduleWatcher.js +3 -3
- package/dist/watchers/ViewWatcher.js +3 -3
- package/package.json +4 -4
- package/src/config.ts +152 -5
- package/src/dashboard/routes.ts +6 -2
- package/src/dashboard/ui.ts +80 -23
- package/src/index.ts +7 -0
- package/src/register.ts +137 -10
- package/src/storage/TraceContentRedaction.ts +44 -0
- package/src/storage/TraceEntryFiltering.ts +14 -0
- package/src/storage/TraceStorage.ts +52 -5
- package/src/storage/TraceWriteDiagnostics.ts +174 -0
- package/src/types.ts +40 -20
- package/src/utils/entryFilter.ts +108 -0
- package/src/utils/redact.ts +57 -9
- package/src/watchers/CommandWatcher.ts +1 -1
- package/src/watchers/HttpClientWatcher.ts +1 -1
- package/src/watchers/HttpWatcher.ts +132 -21
- package/src/watchers/LogWatcher.ts +27 -27
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Migration:
|
|
3
|
-
* Creates the tag watchlist table for @zintrust/
|
|
2
|
+
* Migration: CreateZinTraceMonitoringTable
|
|
3
|
+
* Creates the tag watchlist table for @zintrust/trace
|
|
4
4
|
*/
|
|
5
5
|
import { MigrationSchema } from '@zintrust/core';
|
|
6
6
|
export const migration = {
|
|
7
7
|
async up(db) {
|
|
8
8
|
const schema = MigrationSchema.create(db);
|
|
9
|
-
await schema.create('
|
|
9
|
+
await schema.create('zin_trace_monitoring', (table) => {
|
|
10
10
|
table.string('tag').primary();
|
|
11
11
|
});
|
|
12
12
|
},
|
|
13
13
|
async down(db) {
|
|
14
14
|
const schema = MigrationSchema.create(db);
|
|
15
|
-
await schema.dropIfExists('
|
|
15
|
+
await schema.dropIfExists('zin_trace_monitoring');
|
|
16
16
|
},
|
|
17
17
|
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Migration: WidenTraceCreatedAtForSql
|
|
3
|
+
* Ensures SQL engines that treat INTEGER as 32-bit can store millisecond timestamps.
|
|
4
|
+
*/
|
|
5
|
+
import { type IDatabase } from '@zintrust/core';
|
|
6
|
+
export interface Migration {
|
|
7
|
+
up(db: IDatabase): Promise<void>;
|
|
8
|
+
down(db: IDatabase): Promise<void>;
|
|
9
|
+
}
|
|
10
|
+
export declare const migration: Migration;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Migration: WidenTraceCreatedAtForSql
|
|
3
|
+
* Ensures SQL engines that treat INTEGER as 32-bit can store millisecond timestamps.
|
|
4
|
+
*/
|
|
5
|
+
import { MigrationSchema } from '@zintrust/core';
|
|
6
|
+
const alterCreatedAt = async (db) => {
|
|
7
|
+
const driver = db.getType?.() ?? 'sqlite';
|
|
8
|
+
if (driver === 'sqlite' || driver === 'd1' || driver === 'd1-remote')
|
|
9
|
+
return;
|
|
10
|
+
const schema = MigrationSchema.create(db);
|
|
11
|
+
if (!(await schema.hasTable('zin_trace_entries')))
|
|
12
|
+
return;
|
|
13
|
+
if (!(await schema.hasColumn('zin_trace_entries', 'created_at')))
|
|
14
|
+
return;
|
|
15
|
+
if (driver === 'mysql') {
|
|
16
|
+
await db.query('ALTER TABLE zin_trace_entries MODIFY COLUMN created_at BIGINT UNSIGNED NOT NULL', []);
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
if (driver === 'postgresql') {
|
|
20
|
+
await db.query('ALTER TABLE zin_trace_entries ALTER COLUMN created_at TYPE BIGINT USING created_at::bigint', []);
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
if (driver === 'sqlserver') {
|
|
24
|
+
await db.query('ALTER TABLE zin_trace_entries ALTER COLUMN created_at BIGINT NOT NULL', []);
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
export const migration = {
|
|
28
|
+
async up(db) {
|
|
29
|
+
await alterCreatedAt(db);
|
|
30
|
+
},
|
|
31
|
+
async down(_db) {
|
|
32
|
+
return;
|
|
33
|
+
},
|
|
34
|
+
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Migrations index for @zintrust/
|
|
2
|
+
* Migrations index for @zintrust/trace
|
|
3
3
|
* Export all migrations as an ordered array.
|
|
4
4
|
*/
|
|
5
|
-
export type { Migration } from './
|
|
6
|
-
export declare const migrations: import("./
|
|
5
|
+
export type { Migration } from './20260331000001_create_zin_trace_entries_table';
|
|
6
|
+
export declare const migrations: import("./20260331000001_create_zin_trace_entries_table").Migration[];
|
package/dist/migrations/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { migration as createEntries } from './
|
|
2
|
-
import { migration as createEntriesTags } from './
|
|
3
|
-
import { migration as createMonitoring } from './
|
|
4
|
-
|
|
1
|
+
import { migration as createEntries } from './20260331000001_create_zin_trace_entries_table.js';
|
|
2
|
+
import { migration as createEntriesTags } from './20260331000002_create_zin_trace_entries_tags_table.js';
|
|
3
|
+
import { migration as createMonitoring } from './20260331000003_create_zin_trace_monitoring_table.js';
|
|
4
|
+
import { migration as widenCreatedAt } from './20260407193000_widen_trace_created_at_for_sql.js';
|
|
5
|
+
export const migrations = [createEntries, createEntriesTags, createMonitoring, widenCreatedAt];
|
package/dist/register.js
CHANGED
|
@@ -19,9 +19,12 @@
|
|
|
19
19
|
* middleware: ['admin'],
|
|
20
20
|
* });
|
|
21
21
|
*/
|
|
22
|
-
import { TraceConfig } from './config';
|
|
23
|
-
import { TraceContext } from './context';
|
|
24
|
-
import { TraceStorage } from './storage';
|
|
22
|
+
import { TraceConfig } from './config.js';
|
|
23
|
+
import { TraceContext } from './context.js';
|
|
24
|
+
import { TraceStorage } from './storage/index.js';
|
|
25
|
+
import { TraceContentRedaction } from './storage/TraceContentRedaction.js';
|
|
26
|
+
import { TraceEntryFiltering } from './storage/TraceEntryFiltering.js';
|
|
27
|
+
import { TraceWriteDiagnostics } from './storage/TraceWriteDiagnostics.js';
|
|
25
28
|
const globalTraceRegisterState = globalThis;
|
|
26
29
|
globalTraceRegisterState.__zintrust_system_trace_plugin_requested__ = true;
|
|
27
30
|
const traceAlreadyInitialized = globalTraceRegisterState.__zintrust_system_trace_register_initialized__ === true;
|
|
@@ -61,49 +64,144 @@ const resolveTraceConnectionName = (env, configuredConnection) => {
|
|
|
61
64
|
}
|
|
62
65
|
return resolveDefaultConnection();
|
|
63
66
|
};
|
|
67
|
+
const isObjectValue = (value) => {
|
|
68
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
69
|
+
};
|
|
70
|
+
const parseEnvList = (rawValue) => {
|
|
71
|
+
const value = rawValue.trim();
|
|
72
|
+
if (value === '')
|
|
73
|
+
return undefined;
|
|
74
|
+
if (value.startsWith('[')) {
|
|
75
|
+
try {
|
|
76
|
+
const parsed = JSON.parse(value);
|
|
77
|
+
if (Array.isArray(parsed)) {
|
|
78
|
+
return parsed
|
|
79
|
+
.filter((entry) => typeof entry === 'string')
|
|
80
|
+
.map((entry) => entry.trim())
|
|
81
|
+
.filter((entry) => entry !== '');
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
// fall through to CSV parsing
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return value
|
|
89
|
+
.split(',')
|
|
90
|
+
.map((entry) => entry.trim())
|
|
91
|
+
.filter((entry) => entry !== '');
|
|
92
|
+
};
|
|
93
|
+
const resolveTraceStartupOverrides = (core) => {
|
|
94
|
+
const traceConfigFile = core.StartupConfigFile?.Trace;
|
|
95
|
+
if (typeof traceConfigFile !== 'string' || traceConfigFile.trim() === '')
|
|
96
|
+
return undefined;
|
|
97
|
+
const overrides = core.StartupConfigFileRegistry?.get(traceConfigFile);
|
|
98
|
+
return isObjectValue(overrides) ? overrides : undefined;
|
|
99
|
+
};
|
|
100
|
+
const buildTraceRedactionOverrides = (input) => {
|
|
101
|
+
const redaction = {
|
|
102
|
+
...(isObjectValue(input.startupOverrides?.redaction) ? input.startupOverrides?.redaction : {}),
|
|
103
|
+
};
|
|
104
|
+
if (input.redactionKeys === undefined) {
|
|
105
|
+
// no-op
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
redaction.keys = input.redactionKeys;
|
|
109
|
+
}
|
|
110
|
+
if (input.redactionHeaders === undefined) {
|
|
111
|
+
// no-op
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
redaction.headers = input.redactionHeaders;
|
|
115
|
+
}
|
|
116
|
+
if (input.redactionBody === undefined) {
|
|
117
|
+
// no-op
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
redaction.body = input.redactionBody;
|
|
121
|
+
}
|
|
122
|
+
if (input.redactionQuery === undefined) {
|
|
123
|
+
// no-op
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
redaction.query = input.redactionQuery;
|
|
127
|
+
}
|
|
128
|
+
return Object.keys(redaction).length > 0
|
|
129
|
+
? redaction
|
|
130
|
+
: undefined;
|
|
131
|
+
};
|
|
64
132
|
const core = (await importCore());
|
|
65
133
|
const Env = core.Env;
|
|
134
|
+
const startupOverrides = resolveTraceStartupOverrides(core);
|
|
66
135
|
if (!traceAlreadyInitialized && Env) {
|
|
67
|
-
const enabled = Env.getBool('TRACE_ENABLED', false);
|
|
136
|
+
const enabled = startupOverrides?.enabled === true || Env.getBool('TRACE_ENABLED', false);
|
|
68
137
|
if (enabled) {
|
|
69
|
-
const
|
|
70
|
-
const
|
|
71
|
-
const
|
|
72
|
-
const
|
|
138
|
+
const connectionRaw = Env.get('TRACE_DB_CONNECTION', '').trim();
|
|
139
|
+
const pruneAfterHoursRaw = Env.get('TRACE_PRUNE_HOURS', '').trim();
|
|
140
|
+
const slowQueryThresholdRaw = Env.get('TRACE_SLOW_QUERY_MS', '').trim();
|
|
141
|
+
const logMinLevelRaw = Env.get('TRACE_LOG_LEVEL', '').trim();
|
|
142
|
+
const redactionKeys = parseEnvList(Env.get('TRACE_REDACT_KEYS', ''));
|
|
143
|
+
const redactionHeaders = parseEnvList(Env.get('TRACE_REDACT_HEADERS', ''));
|
|
144
|
+
const redactionBody = parseEnvList(Env.get('TRACE_REDACT_BODY', ''));
|
|
145
|
+
const redactionQuery = parseEnvList(Env.get('TRACE_REDACT_QUERY', ''));
|
|
146
|
+
const connection = connectionRaw === '' ? startupOverrides?.connection : connectionRaw;
|
|
147
|
+
const pruneAfterHours = pruneAfterHoursRaw === ''
|
|
148
|
+
? startupOverrides?.pruneAfterHours
|
|
149
|
+
: Number.parseInt(pruneAfterHoursRaw, 10);
|
|
150
|
+
const slowQueryThreshold = slowQueryThresholdRaw === ''
|
|
151
|
+
? startupOverrides?.slowQueryThreshold
|
|
152
|
+
: Number.parseInt(slowQueryThresholdRaw, 10);
|
|
153
|
+
const logMinLevel = (logMinLevelRaw === '' ? startupOverrides?.logMinLevel : logMinLevelRaw);
|
|
154
|
+
const redaction = buildTraceRedactionOverrides({
|
|
155
|
+
startupOverrides,
|
|
156
|
+
redactionBody,
|
|
157
|
+
redactionHeaders,
|
|
158
|
+
redactionKeys,
|
|
159
|
+
redactionQuery,
|
|
160
|
+
});
|
|
73
161
|
const config = TraceConfig.merge({
|
|
162
|
+
...startupOverrides,
|
|
74
163
|
enabled,
|
|
75
164
|
connection,
|
|
76
|
-
pruneAfterHours
|
|
77
|
-
|
|
165
|
+
...(typeof pruneAfterHours === 'number' && Number.isFinite(pruneAfterHours)
|
|
166
|
+
? { pruneAfterHours }
|
|
167
|
+
: {}),
|
|
168
|
+
...(typeof slowQueryThreshold === 'number' && Number.isFinite(slowQueryThreshold)
|
|
169
|
+
? { slowQueryThreshold }
|
|
170
|
+
: {}),
|
|
78
171
|
logMinLevel,
|
|
172
|
+
...(redaction === undefined ? {} : { redaction }),
|
|
79
173
|
});
|
|
80
|
-
const
|
|
174
|
+
const resolvedConnectionName = resolveTraceConnectionName(Env, config.connection);
|
|
175
|
+
const db = core.useDatabase?.(undefined, resolvedConnectionName);
|
|
81
176
|
if (db) {
|
|
82
|
-
const storage = TraceStorage.resolveStorage(db)
|
|
177
|
+
const storage = TraceWriteDiagnostics.wrapStorage(TraceContentRedaction.wrapStorage(TraceEntryFiltering.wrapStorage(TraceStorage.resolveStorage(db), config), config.redaction), {
|
|
178
|
+
connectionName: resolvedConnectionName,
|
|
179
|
+
logger: core.Logger,
|
|
180
|
+
});
|
|
83
181
|
if (core.RequestContext) {
|
|
84
182
|
TraceContext.setRequestContextImpl(core.RequestContext);
|
|
85
183
|
}
|
|
86
184
|
const [{ HttpWatcher }, { QueryWatcher }, { LogWatcher }, { ExceptionWatcher }, { JobWatcher }, { CacheWatcher }, { ScheduleWatcher }, { MailWatcher }, { AuthWatcher }, { EventWatcher }, { ModelWatcher }, { NotificationWatcher }, { RedisWatcher }, { GateWatcher }, { MiddlewareWatcher }, { CommandWatcher }, { BatchWatcher }, { DumpWatcher }, { ViewWatcher }, { HttpClientWatcher },] = await Promise.all([
|
|
87
|
-
import('./watchers/HttpWatcher'),
|
|
88
|
-
import('./watchers/QueryWatcher'),
|
|
89
|
-
import('./watchers/LogWatcher'),
|
|
90
|
-
import('./watchers/ExceptionWatcher'),
|
|
91
|
-
import('./watchers/JobWatcher'),
|
|
92
|
-
import('./watchers/CacheWatcher'),
|
|
93
|
-
import('./watchers/ScheduleWatcher'),
|
|
94
|
-
import('./watchers/MailWatcher'),
|
|
95
|
-
import('./watchers/AuthWatcher'),
|
|
96
|
-
import('./watchers/EventWatcher'),
|
|
97
|
-
import('./watchers/ModelWatcher'),
|
|
98
|
-
import('./watchers/NotificationWatcher'),
|
|
99
|
-
import('./watchers/RedisWatcher'),
|
|
100
|
-
import('./watchers/GateWatcher'),
|
|
101
|
-
import('./watchers/MiddlewareWatcher'),
|
|
102
|
-
import('./watchers/CommandWatcher'),
|
|
103
|
-
import('./watchers/BatchWatcher'),
|
|
104
|
-
import('./watchers/DumpWatcher'),
|
|
105
|
-
import('./watchers/ViewWatcher'),
|
|
106
|
-
import('./watchers/HttpClientWatcher'),
|
|
185
|
+
import('./watchers/HttpWatcher.js'),
|
|
186
|
+
import('./watchers/QueryWatcher.js'),
|
|
187
|
+
import('./watchers/LogWatcher.js'),
|
|
188
|
+
import('./watchers/ExceptionWatcher.js'),
|
|
189
|
+
import('./watchers/JobWatcher.js'),
|
|
190
|
+
import('./watchers/CacheWatcher.js'),
|
|
191
|
+
import('./watchers/ScheduleWatcher.js'),
|
|
192
|
+
import('./watchers/MailWatcher.js'),
|
|
193
|
+
import('./watchers/AuthWatcher.js'),
|
|
194
|
+
import('./watchers/EventWatcher.js'),
|
|
195
|
+
import('./watchers/ModelWatcher.js'),
|
|
196
|
+
import('./watchers/NotificationWatcher.js'),
|
|
197
|
+
import('./watchers/RedisWatcher.js'),
|
|
198
|
+
import('./watchers/GateWatcher.js'),
|
|
199
|
+
import('./watchers/MiddlewareWatcher.js'),
|
|
200
|
+
import('./watchers/CommandWatcher.js'),
|
|
201
|
+
import('./watchers/BatchWatcher.js'),
|
|
202
|
+
import('./watchers/DumpWatcher.js'),
|
|
203
|
+
import('./watchers/ViewWatcher.js'),
|
|
204
|
+
import('./watchers/HttpClientWatcher.js'),
|
|
107
205
|
]);
|
|
108
206
|
const watcherArgs = { storage, config, db };
|
|
109
207
|
HttpWatcher.register({ ...watcherArgs, registerMiddleware: resolveRegisterMiddleware() });
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { redactUnknown } from '../utils/redact.js';
|
|
2
|
+
const collectRedactionFields = (redaction) => {
|
|
3
|
+
return [
|
|
4
|
+
...new Set([...redaction.keys, ...redaction.headers, ...redaction.body, ...redaction.query]),
|
|
5
|
+
];
|
|
6
|
+
};
|
|
7
|
+
const redactTraceEntry = (entry, redaction) => {
|
|
8
|
+
return {
|
|
9
|
+
...entry,
|
|
10
|
+
content: redactUnknown(entry.content, collectRedactionFields(redaction)),
|
|
11
|
+
};
|
|
12
|
+
};
|
|
13
|
+
const redactTracePatch = (patch, redaction) => {
|
|
14
|
+
if (patch.content === undefined)
|
|
15
|
+
return patch;
|
|
16
|
+
return {
|
|
17
|
+
...patch,
|
|
18
|
+
content: redactUnknown(patch.content, collectRedactionFields(redaction)),
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
export const TraceContentRedaction = Object.freeze({
|
|
22
|
+
wrapStorage(storage, redaction) {
|
|
23
|
+
return Object.freeze({
|
|
24
|
+
...storage,
|
|
25
|
+
writeEntry: async (entry) => {
|
|
26
|
+
await storage.writeEntry(redactTraceEntry(entry, redaction));
|
|
27
|
+
},
|
|
28
|
+
updateEntry: async (uuid, patch) => {
|
|
29
|
+
await storage.updateEntry(uuid, redactTracePatch(patch, redaction));
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
},
|
|
33
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { TraceEntryFilter } from '../utils/entryFilter.js';
|
|
2
|
+
export const TraceEntryFiltering = Object.freeze({
|
|
3
|
+
wrapStorage(storage, config) {
|
|
4
|
+
return Object.freeze({
|
|
5
|
+
...storage,
|
|
6
|
+
async writeEntry(entry) {
|
|
7
|
+
if (!TraceEntryFilter.shouldCapture(entry, config))
|
|
8
|
+
return;
|
|
9
|
+
await storage.writeEntry(entry);
|
|
10
|
+
},
|
|
11
|
+
});
|
|
12
|
+
},
|
|
13
|
+
});
|
|
@@ -1,8 +1,40 @@
|
|
|
1
|
-
import { familyHash } from '../utils/familyHash';
|
|
1
|
+
import { familyHash } from '../utils/familyHash.js';
|
|
2
2
|
const TABLE_ENTRIES = 'zin_trace_entries';
|
|
3
3
|
const TABLE_TAGS = 'zin_trace_entries_tags';
|
|
4
4
|
const TABLE_MONITORING = 'zin_trace_monitoring';
|
|
5
5
|
const generateUuid = () => crypto.randomUUID();
|
|
6
|
+
const buildIgnoreInsert = (db, table, columns, conflictColumns) => {
|
|
7
|
+
const columnList = columns.join(', ');
|
|
8
|
+
const placeholders = columns.map(() => '?').join(', ');
|
|
9
|
+
const driver = db.getType?.() ?? 'sqlite';
|
|
10
|
+
if (driver === 'sqlite' || driver === 'd1' || driver === 'd1-remote') {
|
|
11
|
+
return `INSERT OR IGNORE INTO ${table} (${columnList}) VALUES (${placeholders})`;
|
|
12
|
+
}
|
|
13
|
+
if (driver === 'mysql') {
|
|
14
|
+
return `INSERT IGNORE INTO ${table} (${columnList}) VALUES (${placeholders})`;
|
|
15
|
+
}
|
|
16
|
+
if (driver === 'postgresql') {
|
|
17
|
+
return `INSERT INTO ${table} (${columnList}) VALUES (${placeholders}) ON CONFLICT (${conflictColumns.join(', ')}) DO NOTHING`;
|
|
18
|
+
}
|
|
19
|
+
if (driver === 'sqlserver') {
|
|
20
|
+
const sourceColumns = columns.map((_, index) => `v${index + 1}`);
|
|
21
|
+
const selectClause = sourceColumns.map((name) => `? AS ${name}`).join(', ');
|
|
22
|
+
const conflictClause = conflictColumns
|
|
23
|
+
.map((column) => `target.${column} = source.${column}`)
|
|
24
|
+
.join(' AND ');
|
|
25
|
+
const insertValues = columns.map((column) => `source.${column}`).join(', ');
|
|
26
|
+
const sourceProjection = columns
|
|
27
|
+
.map((column, index) => `${sourceColumns[index]} AS ${column}`)
|
|
28
|
+
.join(', ');
|
|
29
|
+
return [
|
|
30
|
+
`MERGE INTO ${table} WITH (HOLDLOCK) AS target`,
|
|
31
|
+
`USING (SELECT ${sourceProjection} FROM (SELECT ${selectClause}) seed) AS source`,
|
|
32
|
+
`ON ${conflictClause}`,
|
|
33
|
+
`WHEN NOT MATCHED THEN INSERT (${columnList}) VALUES (${insertValues});`,
|
|
34
|
+
].join(' ');
|
|
35
|
+
}
|
|
36
|
+
return `INSERT INTO ${table} (${columnList}) VALUES (${placeholders})`;
|
|
37
|
+
};
|
|
6
38
|
const rowToEntry = (row, tags) => ({
|
|
7
39
|
uuid: row.uuid,
|
|
8
40
|
batchId: row.batch_id,
|
|
@@ -16,11 +48,9 @@ const rowToEntry = (row, tags) => ({
|
|
|
16
48
|
const insertTags = async (db, uuid, tags) => {
|
|
17
49
|
if (tags.length === 0)
|
|
18
50
|
return;
|
|
51
|
+
const sql = buildIgnoreInsert(db, TABLE_TAGS, ['entry_uuid', 'tag'], ['entry_uuid', 'tag']);
|
|
19
52
|
await Promise.all(tags.map(async (tag) => {
|
|
20
|
-
await db.execute(
|
|
21
|
-
uuid,
|
|
22
|
-
tag,
|
|
23
|
-
]);
|
|
53
|
+
await db.execute(sql, [uuid, tag]);
|
|
24
54
|
}));
|
|
25
55
|
};
|
|
26
56
|
const buildEntryFilters = (opts) => {
|
|
@@ -158,7 +188,7 @@ const createStorage = (db) => {
|
|
|
158
188
|
return rows.map((row) => row.tag);
|
|
159
189
|
};
|
|
160
190
|
const addMonitoring = async (tag) => {
|
|
161
|
-
await db.execute(
|
|
191
|
+
await db.execute(buildIgnoreInsert(db, TABLE_MONITORING, ['tag'], ['tag']), [tag]);
|
|
162
192
|
};
|
|
163
193
|
const removeMonitoring = async (tag) => {
|
|
164
194
|
await db.execute(`DELETE FROM ${TABLE_MONITORING} WHERE tag = ?`, [tag]);
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { ITraceStorage } from '../types';
|
|
2
|
+
type TraceLogger = {
|
|
3
|
+
warn: (message: string, context?: Record<string, unknown>) => void;
|
|
4
|
+
};
|
|
5
|
+
type TraceWriteDiagnosticsSnapshot = {
|
|
6
|
+
degraded: boolean;
|
|
7
|
+
lastErrorMessage: string | null;
|
|
8
|
+
lastFailureAt: number | null;
|
|
9
|
+
totalFailures: number;
|
|
10
|
+
};
|
|
11
|
+
export declare const TraceWriteDiagnostics: Readonly<{
|
|
12
|
+
getSnapshot(): TraceWriteDiagnosticsSnapshot;
|
|
13
|
+
reset(): void;
|
|
14
|
+
wrapStorage(storage: ITraceStorage, options: {
|
|
15
|
+
connectionName: string;
|
|
16
|
+
logger?: TraceLogger;
|
|
17
|
+
}): ITraceStorage;
|
|
18
|
+
}>;
|
|
19
|
+
export {};
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
const LOG_WINDOW_MS = 30_000;
|
|
2
|
+
const diagnosticsState = {
|
|
3
|
+
degraded: false,
|
|
4
|
+
lastErrorMessage: null,
|
|
5
|
+
lastFailureAt: null,
|
|
6
|
+
lastLoggedAtByFingerprint: new Map(),
|
|
7
|
+
totalFailures: 0,
|
|
8
|
+
};
|
|
9
|
+
const getErrorMessage = (error) => {
|
|
10
|
+
if (error instanceof Error && error.message.trim() !== '')
|
|
11
|
+
return error.message;
|
|
12
|
+
if (typeof error === 'string' && error.trim() !== '')
|
|
13
|
+
return error;
|
|
14
|
+
try {
|
|
15
|
+
const serialized = JSON.stringify(error);
|
|
16
|
+
if (typeof serialized === 'string' && serialized !== '')
|
|
17
|
+
return serialized;
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
// ignore serialization failures
|
|
21
|
+
}
|
|
22
|
+
return 'Unknown trace storage error';
|
|
23
|
+
};
|
|
24
|
+
const buildFingerprint = (context) => {
|
|
25
|
+
return [
|
|
26
|
+
context.connectionName,
|
|
27
|
+
context.operation,
|
|
28
|
+
context.watcherType ?? 'unknown',
|
|
29
|
+
getErrorMessage(context.error),
|
|
30
|
+
].join('|');
|
|
31
|
+
};
|
|
32
|
+
const reportFailure = (logger, context) => {
|
|
33
|
+
const now = Date.now();
|
|
34
|
+
const errorMessage = getErrorMessage(context.error);
|
|
35
|
+
const fingerprint = buildFingerprint(context);
|
|
36
|
+
const lastLoggedAt = diagnosticsState.lastLoggedAtByFingerprint.get(fingerprint);
|
|
37
|
+
diagnosticsState.degraded = true;
|
|
38
|
+
diagnosticsState.lastErrorMessage = errorMessage;
|
|
39
|
+
diagnosticsState.lastFailureAt = now;
|
|
40
|
+
diagnosticsState.totalFailures += 1;
|
|
41
|
+
if (!logger)
|
|
42
|
+
return;
|
|
43
|
+
if (typeof lastLoggedAt === 'number' && now - lastLoggedAt < LOG_WINDOW_MS)
|
|
44
|
+
return;
|
|
45
|
+
diagnosticsState.lastLoggedAtByFingerprint.set(fingerprint, now);
|
|
46
|
+
logger.warn('[trace] Trace storage write degraded', {
|
|
47
|
+
connectionName: context.connectionName,
|
|
48
|
+
error: errorMessage,
|
|
49
|
+
lastFailureAt: now,
|
|
50
|
+
operation: context.operation,
|
|
51
|
+
totalFailures: diagnosticsState.totalFailures,
|
|
52
|
+
watcherType: context.watcherType ?? null,
|
|
53
|
+
});
|
|
54
|
+
};
|
|
55
|
+
const wrapStorageMethod = (method, describeFailure, connectionName, logger) => {
|
|
56
|
+
return async (...args) => {
|
|
57
|
+
try {
|
|
58
|
+
return await method(...args);
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
reportFailure(logger, {
|
|
62
|
+
...describeFailure(...args),
|
|
63
|
+
connectionName,
|
|
64
|
+
error,
|
|
65
|
+
});
|
|
66
|
+
throw error;
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
};
|
|
70
|
+
export const TraceWriteDiagnostics = Object.freeze({
|
|
71
|
+
getSnapshot() {
|
|
72
|
+
return {
|
|
73
|
+
degraded: diagnosticsState.degraded,
|
|
74
|
+
lastErrorMessage: diagnosticsState.lastErrorMessage,
|
|
75
|
+
lastFailureAt: diagnosticsState.lastFailureAt,
|
|
76
|
+
totalFailures: diagnosticsState.totalFailures,
|
|
77
|
+
};
|
|
78
|
+
},
|
|
79
|
+
reset() {
|
|
80
|
+
diagnosticsState.degraded = false;
|
|
81
|
+
diagnosticsState.lastErrorMessage = null;
|
|
82
|
+
diagnosticsState.lastFailureAt = null;
|
|
83
|
+
diagnosticsState.totalFailures = 0;
|
|
84
|
+
diagnosticsState.lastLoggedAtByFingerprint.clear();
|
|
85
|
+
},
|
|
86
|
+
wrapStorage(storage, options) {
|
|
87
|
+
return Object.freeze({
|
|
88
|
+
...storage,
|
|
89
|
+
writeEntry: wrapStorageMethod(storage.writeEntry.bind(storage), (entry) => ({ operation: 'writeEntry', watcherType: entry.type }), options.connectionName, options.logger),
|
|
90
|
+
updateEntry: wrapStorageMethod(storage.updateEntry.bind(storage), (_uuid, _patch) => ({ operation: 'updateEntry' }), options.connectionName, options.logger),
|
|
91
|
+
markFamilyStale: wrapStorageMethod(storage.markFamilyStale.bind(storage), (_familyHash, _exceptUuid) => ({ operation: 'markFamilyStale' }), options.connectionName, options.logger),
|
|
92
|
+
prune: wrapStorageMethod(storage.prune.bind(storage), (_olderThanMs, _keepExceptions) => ({ operation: 'prune' }), options.connectionName, options.logger),
|
|
93
|
+
clear: wrapStorageMethod(storage.clear.bind(storage), () => ({ operation: 'clear' }), options.connectionName, options.logger),
|
|
94
|
+
addMonitoring: wrapStorageMethod(storage.addMonitoring.bind(storage), (_tag) => ({ operation: 'addMonitoring' }), options.connectionName, options.logger),
|
|
95
|
+
removeMonitoring: wrapStorageMethod(storage.removeMonitoring.bind(storage), (_tag) => ({ operation: 'removeMonitoring' }), options.connectionName, options.logger),
|
|
96
|
+
});
|
|
97
|
+
},
|
|
98
|
+
});
|
package/dist/storage/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { TraceStorage } from './TraceStorage';
|
|
1
|
+
export { TraceStorage } from './TraceStorage.js';
|
package/dist/types.d.ts
CHANGED
|
@@ -33,6 +33,7 @@ export interface RequestContent {
|
|
|
33
33
|
payload: Record<string, unknown>;
|
|
34
34
|
responseStatus: number;
|
|
35
35
|
responseHeaders: Record<string, string>;
|
|
36
|
+
responseBody?: unknown;
|
|
36
37
|
duration: number;
|
|
37
38
|
memory: number | null;
|
|
38
39
|
middleware: string[];
|
|
@@ -231,31 +232,47 @@ export interface ITraceWatcher {
|
|
|
231
232
|
register(opts: ITraceWatcherConfig): () => void;
|
|
232
233
|
}
|
|
233
234
|
export type RedactionConfig = {
|
|
235
|
+
keys: string[];
|
|
234
236
|
headers: string[];
|
|
235
237
|
body: string[];
|
|
236
238
|
query: string[];
|
|
237
239
|
};
|
|
240
|
+
export type TraceFilterRule = {
|
|
241
|
+
enabled?: boolean;
|
|
242
|
+
include?: string[];
|
|
243
|
+
exclude?: string[];
|
|
244
|
+
};
|
|
245
|
+
export type TraceRequestWatcherConfig = TraceFilterRule & {
|
|
246
|
+
all?: TraceFilterRule;
|
|
247
|
+
get?: TraceFilterRule;
|
|
248
|
+
post?: TraceFilterRule;
|
|
249
|
+
put?: TraceFilterRule;
|
|
250
|
+
patch?: TraceFilterRule;
|
|
251
|
+
delete?: TraceFilterRule;
|
|
252
|
+
};
|
|
253
|
+
export type TraceWatcherToggle = boolean | TraceFilterRule;
|
|
254
|
+
export type TraceRequestWatcherToggle = boolean | TraceRequestWatcherConfig;
|
|
238
255
|
export type WatcherToggles = {
|
|
239
|
-
request?:
|
|
240
|
-
query?:
|
|
241
|
-
exception?:
|
|
242
|
-
log?:
|
|
243
|
-
job?:
|
|
244
|
-
cache?:
|
|
245
|
-
schedule?:
|
|
246
|
-
mail?:
|
|
247
|
-
auth?:
|
|
248
|
-
event?:
|
|
249
|
-
model?:
|
|
250
|
-
notification?:
|
|
251
|
-
redis?:
|
|
252
|
-
gate?:
|
|
253
|
-
middleware?:
|
|
254
|
-
command?:
|
|
255
|
-
batch?:
|
|
256
|
-
dump?:
|
|
257
|
-
view?:
|
|
258
|
-
clientRequest?:
|
|
256
|
+
request?: TraceRequestWatcherToggle;
|
|
257
|
+
query?: TraceWatcherToggle;
|
|
258
|
+
exception?: TraceWatcherToggle;
|
|
259
|
+
log?: TraceWatcherToggle;
|
|
260
|
+
job?: TraceWatcherToggle;
|
|
261
|
+
cache?: TraceWatcherToggle;
|
|
262
|
+
schedule?: TraceWatcherToggle;
|
|
263
|
+
mail?: TraceWatcherToggle;
|
|
264
|
+
auth?: TraceWatcherToggle;
|
|
265
|
+
event?: TraceWatcherToggle;
|
|
266
|
+
model?: TraceWatcherToggle;
|
|
267
|
+
notification?: TraceWatcherToggle;
|
|
268
|
+
redis?: TraceWatcherToggle;
|
|
269
|
+
gate?: TraceWatcherToggle;
|
|
270
|
+
middleware?: TraceWatcherToggle;
|
|
271
|
+
command?: TraceWatcherToggle;
|
|
272
|
+
batch?: TraceWatcherToggle;
|
|
273
|
+
dump?: TraceWatcherToggle;
|
|
274
|
+
view?: TraceWatcherToggle;
|
|
275
|
+
clientRequest?: TraceWatcherToggle;
|
|
259
276
|
};
|
|
260
277
|
export interface ITraceConfig {
|
|
261
278
|
enabled: boolean;
|
package/dist/ui.js
CHANGED
|
@@ -4,4 +4,4 @@
|
|
|
4
4
|
* Import this subpath when you only need trace dashboard registration
|
|
5
5
|
* without pulling in the package root re-export surface.
|
|
6
6
|
*/
|
|
7
|
-
export { registerTraceDashboard, registerTraceRoutes } from './dashboard/routes';
|
|
7
|
+
export { registerTraceDashboard, registerTraceRoutes } from './dashboard/routes.js';
|
package/dist/utils/authTag.js
CHANGED