@zintrust/trace 0.9.4 → 1.5.0
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 +5 -0
- package/dist/build-manifest.json +12 -24
- package/dist/ingest/TraceIngestGateway.js +95 -16
- package/dist/storage/ProxyTraceStorage.js +8 -1
- package/package.json +2 -2
- package/src/ingest/TraceIngestGateway.ts +126 -29
- package/src/storage/ProxyTraceStorage.ts +9 -1
- package/dist/storage/DebuggerStorage.d.ts +0 -13
- package/dist/storage/DebuggerStorage.js +0 -195
package/README.md
CHANGED
|
@@ -39,6 +39,9 @@ TRACE_PROXY_PATH=/zin/trace/write
|
|
|
39
39
|
TRACE_PROXY_KEY_ID= # optional — falls back to APP_NAME
|
|
40
40
|
TRACE_PROXY_SECRET= # optional — falls back to APP_KEY
|
|
41
41
|
TRACE_PROXY_TIMEOUT_MS=30000
|
|
42
|
+
TRACE_PROXY_MIDDLEWARE= # optional — comma-separated route middleware applied on the receiver
|
|
43
|
+
TRACE_PROXY_RATE_LIMIT_MAX=0 # optional — when > 0, adds rateLimit:<max>:<window> automatically
|
|
44
|
+
TRACE_PROXY_RATE_LIMIT_WINDOW_MINUTES=0
|
|
42
45
|
TRACE_PRUNE_HOURS=24 # how long entries are kept (default: 24)
|
|
43
46
|
TRACE_SLOW_QUERY_MS=100 # slow-query threshold in ms (default: 100)
|
|
44
47
|
TRACE_LOG_LEVEL=info # minimum log level captured (default: info)
|
|
@@ -61,6 +64,8 @@ When `TRACE_CONTENT_QUEUE_DRIVER` is set, trace writes enqueue through that regi
|
|
|
61
64
|
|
|
62
65
|
When `TRACE_PROXY=true`, the local runtime keeps collecting the same trace payload it would normally send to storage, but it sends the write/update/stale-family operations to `TRACE_PROXY_URL + TRACE_PROXY_PATH` instead of writing directly to the local trace database. The receiver can then persist those entries with the standard `TraceStorage` flow.
|
|
63
66
|
|
|
67
|
+
On the receiver, use `TRACE_PROXY_MIDDLEWARE` for any gateway middleware such as `auth,admin`. If you want a dedicated ingest rate limit without encoding `rateLimit:<max>:<window>` by hand, set `TRACE_PROXY_RATE_LIMIT_MAX` and `TRACE_PROXY_RATE_LIMIT_WINDOW_MINUTES`; the gateway appends that parameterized rate-limit middleware automatically.
|
|
68
|
+
|
|
64
69
|
This currently works with any queue driver already registered in ZinTrust. First-class Cloudflare Queue support still requires a dedicated queue driver and queue-runtime registration for that transport.
|
|
65
70
|
|
|
66
71
|
### 2. Enable the plugin in `zintrust.plugins.*`
|
package/dist/build-manifest.json
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zintrust/trace",
|
|
3
|
-
"version": "
|
|
4
|
-
"buildDate": "2026-04-
|
|
3
|
+
"version": "1.5.0",
|
|
4
|
+
"buildDate": "2026-04-24T13:06:46.205Z",
|
|
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": "739aa697",
|
|
12
|
+
"branch": "master"
|
|
13
13
|
},
|
|
14
14
|
"package": {
|
|
15
15
|
"engines": {
|
|
@@ -29,10 +29,6 @@
|
|
|
29
29
|
"size": 4640,
|
|
30
30
|
"sha256": "c51cc312046b6b2bbe1673f1ff9508425cc7140a1d2341907f67aa36069c09f9"
|
|
31
31
|
},
|
|
32
|
-
"build-manifest.json": {
|
|
33
|
-
"size": 14744,
|
|
34
|
-
"sha256": "92fc9e1e76ba84696dbdaf02aa2ed7588c6e1234d586e24bf8bab4f54104ad61"
|
|
35
|
-
},
|
|
36
32
|
"cli-register.d.ts": {
|
|
37
33
|
"size": 255,
|
|
38
34
|
"sha256": "da8d689fe5ef32e97e755f28017e4d3cb1aa63489073a71907ea41ad5761ede9"
|
|
@@ -87,15 +83,15 @@
|
|
|
87
83
|
},
|
|
88
84
|
"index.js": {
|
|
89
85
|
"size": 3421,
|
|
90
|
-
"sha256": "
|
|
86
|
+
"sha256": "596b52f0589be73fccdf8badb2ad90ed1075aba8e71d93d040284315ba9cec01"
|
|
91
87
|
},
|
|
92
88
|
"ingest/TraceIngestGateway.d.ts": {
|
|
93
89
|
"size": 786,
|
|
94
90
|
"sha256": "3a0a46fa5bbf5367047214533ec0ee92a171ee35a60d25c197eea1eed1ff0f65"
|
|
95
91
|
},
|
|
96
92
|
"ingest/TraceIngestGateway.js": {
|
|
97
|
-
"size":
|
|
98
|
-
"sha256": "
|
|
93
|
+
"size": 10936,
|
|
94
|
+
"sha256": "c95f0e42f1294d8ae2e406a5910f8e74044c2b586e82e726876d75ddfafef27e"
|
|
99
95
|
},
|
|
100
96
|
"migrations/20260331000001_create_zin_trace_entries_table.d.ts": {
|
|
101
97
|
"size": 304,
|
|
@@ -153,21 +149,13 @@
|
|
|
153
149
|
"size": 19822,
|
|
154
150
|
"sha256": "c553356d90c812f7de430d5679b1d44468131433e034ae2ea89e2876b1254444"
|
|
155
151
|
},
|
|
156
|
-
"storage/DebuggerStorage.d.ts": {
|
|
157
|
-
"size": 517,
|
|
158
|
-
"sha256": "c9c215aaa414f7b0c1fec6c82b054fc52bdf97af58f96f35c7f96672fb859c31"
|
|
159
|
-
},
|
|
160
|
-
"storage/DebuggerStorage.js": {
|
|
161
|
-
"size": 7442,
|
|
162
|
-
"sha256": "5ecce0fcfcf695df587a7b90a7a5c7efd2e64ad13c9f2d104b392f89f34f0dc4"
|
|
163
|
-
},
|
|
164
152
|
"storage/ProxyTraceStorage.d.ts": {
|
|
165
153
|
"size": 339,
|
|
166
154
|
"sha256": "9c724ff342dfe82da12e7cce95593f6623faa80c40f97593719b8e78e95f266d"
|
|
167
155
|
},
|
|
168
156
|
"storage/ProxyTraceStorage.js": {
|
|
169
|
-
"size":
|
|
170
|
-
"sha256": "
|
|
157
|
+
"size": 4330,
|
|
158
|
+
"sha256": "672c95ed022504b2e5be62f3c2b31bac168ac06158d793dd020f7e38907e8f64"
|
|
171
159
|
},
|
|
172
160
|
"storage/TraceContentBudget.d.ts": {
|
|
173
161
|
"size": 1306,
|
|
@@ -13,8 +13,18 @@ const parseMiddleware = (value) => value
|
|
|
13
13
|
.split(',')
|
|
14
14
|
.map((entry) => entry.trim())
|
|
15
15
|
.filter((entry) => entry.length > 0);
|
|
16
|
+
const isParameterizedRateLimitMiddleware = (value) => {
|
|
17
|
+
return /^rateLimit:\d+:\d+(?:\.\d+)?$/.test(value.trim());
|
|
18
|
+
};
|
|
19
|
+
const trimTrailingSlashes = (value) => {
|
|
20
|
+
let trimmed = value;
|
|
21
|
+
while (trimmed.endsWith('/')) {
|
|
22
|
+
trimmed = trimmed.slice(0, -1);
|
|
23
|
+
}
|
|
24
|
+
return trimmed;
|
|
25
|
+
};
|
|
16
26
|
const appendSuffix = (path, suffix) => {
|
|
17
|
-
const base = normalizePath(path)
|
|
27
|
+
const base = trimTrailingSlashes(normalizePath(path));
|
|
18
28
|
const tail = suffix.startsWith('/') ? suffix : `/${suffix}`;
|
|
19
29
|
return `${base}${tail}`;
|
|
20
30
|
};
|
|
@@ -179,32 +189,101 @@ const resolveStorage = (overrides) => {
|
|
|
179
189
|
}
|
|
180
190
|
return TraceStorage.resolveStorage(db);
|
|
181
191
|
};
|
|
192
|
+
const readConfiguredKeyId = (overrides) => {
|
|
193
|
+
return (overrides?.keyId ?? Env.get('TRACE_PROXY_KEY_ID', '')).trim();
|
|
194
|
+
};
|
|
195
|
+
const readConfiguredSecret = (overrides) => {
|
|
196
|
+
return (overrides?.secret ?? Env.get('TRACE_PROXY_SECRET', '')).trim();
|
|
197
|
+
};
|
|
198
|
+
const resolveKeyId = (overrides) => {
|
|
199
|
+
const configuredKeyId = readConfiguredKeyId(overrides);
|
|
200
|
+
if (configuredKeyId !== '')
|
|
201
|
+
return configuredKeyId;
|
|
202
|
+
return (Env.APP_NAME || 'zintrust').trim();
|
|
203
|
+
};
|
|
204
|
+
const resolveSecret = (overrides) => {
|
|
205
|
+
const configuredSecret = readConfiguredSecret(overrides);
|
|
206
|
+
if (configuredSecret !== '')
|
|
207
|
+
return configuredSecret;
|
|
208
|
+
return Env.APP_KEY;
|
|
209
|
+
};
|
|
210
|
+
const resolveSigningWindowMs = (overrides) => {
|
|
211
|
+
return overrides?.signingWindowMs ?? Env.getInt('TRACE_PROXY_SIGNING_WINDOW_MS', 60000);
|
|
212
|
+
};
|
|
213
|
+
const resolveNonceTtlMs = (overrides) => {
|
|
214
|
+
return overrides?.nonceTtlMs ?? Env.getInt('TRACE_PROXY_NONCE_TTL_MS', 120000);
|
|
215
|
+
};
|
|
216
|
+
const resolveRateLimitMax = () => {
|
|
217
|
+
return Env.getInt('TRACE_PROXY_RATE_LIMIT_MAX', 0);
|
|
218
|
+
};
|
|
219
|
+
const resolveRateLimitWindowMinutes = () => {
|
|
220
|
+
const windowMinutes = Env.getFloat('TRACE_PROXY_RATE_LIMIT_WINDOW_MINUTES', 0);
|
|
221
|
+
return Math.max(windowMinutes, 0);
|
|
222
|
+
};
|
|
223
|
+
const createRateLimitMiddleware = () => {
|
|
224
|
+
const max = resolveRateLimitMax();
|
|
225
|
+
const windowMinutes = resolveRateLimitWindowMinutes();
|
|
226
|
+
if (!Number.isFinite(max) || !Number.isInteger(max)) {
|
|
227
|
+
throw ErrorFactory.createConfigError('TRACE_PROXY_RATE_LIMIT_MAX must be a valid integer', {
|
|
228
|
+
value: max,
|
|
229
|
+
envKey: 'TRACE_PROXY_RATE_LIMIT_MAX',
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
if (!Number.isFinite(windowMinutes)) {
|
|
233
|
+
throw ErrorFactory.createConfigError('TRACE_PROXY_RATE_LIMIT_WINDOW_MINUTES must be a valid number', {
|
|
234
|
+
value: windowMinutes,
|
|
235
|
+
envKey: 'TRACE_PROXY_RATE_LIMIT_WINDOW_MINUTES',
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
if (max <= 0 || windowMinutes <= 0) {
|
|
239
|
+
return undefined;
|
|
240
|
+
}
|
|
241
|
+
return `rateLimit:${max}:${windowMinutes}`;
|
|
242
|
+
};
|
|
243
|
+
const resolveMiddleware = (overrides) => {
|
|
244
|
+
if (overrides?.middleware !== undefined) {
|
|
245
|
+
return overrides.middleware;
|
|
246
|
+
}
|
|
247
|
+
const configured = parseMiddleware(Env.get('TRACE_PROXY_MIDDLEWARE', ''));
|
|
248
|
+
const rateLimitMiddleware = createRateLimitMiddleware();
|
|
249
|
+
if (rateLimitMiddleware === undefined) {
|
|
250
|
+
return configured;
|
|
251
|
+
}
|
|
252
|
+
return [
|
|
253
|
+
...configured.filter((entry) => !isParameterizedRateLimitMiddleware(entry)),
|
|
254
|
+
rateLimitMiddleware,
|
|
255
|
+
];
|
|
256
|
+
};
|
|
182
257
|
const readSettings = (overrides) => {
|
|
183
|
-
const configuredSecret = (overrides?.secret ?? Env.get('TRACE_PROXY_SECRET', '')).trim();
|
|
184
|
-
const configuredKeyId = (overrides?.keyId ?? Env.get('TRACE_PROXY_KEY_ID', '')).trim();
|
|
185
258
|
return {
|
|
186
259
|
basePath: normalizePath(overrides?.basePath ?? Env.get('TRACE_PROXY_PATH', '/zin/trace/write')),
|
|
187
|
-
keyId:
|
|
188
|
-
secret:
|
|
189
|
-
signingWindowMs: overrides
|
|
190
|
-
nonceTtlMs: overrides
|
|
191
|
-
middleware: overrides
|
|
260
|
+
keyId: resolveKeyId(overrides),
|
|
261
|
+
secret: resolveSecret(overrides),
|
|
262
|
+
signingWindowMs: resolveSigningWindowMs(overrides),
|
|
263
|
+
nonceTtlMs: resolveNonceTtlMs(overrides),
|
|
264
|
+
middleware: resolveMiddleware(overrides),
|
|
192
265
|
storage: resolveStorage(overrides),
|
|
193
266
|
};
|
|
194
267
|
};
|
|
268
|
+
const getRouteOptions = (settings) => {
|
|
269
|
+
if (settings.middleware.length === 0)
|
|
270
|
+
return undefined;
|
|
271
|
+
return { middleware: settings.middleware };
|
|
272
|
+
};
|
|
273
|
+
const registerRoutes = (router, settings) => {
|
|
274
|
+
const routeOptions = getRouteOptions(settings);
|
|
275
|
+
const updatePath = appendSuffix(settings.basePath, '/update');
|
|
276
|
+
const markFamilyStalePath = appendSuffix(settings.basePath, '/mark-family-stale');
|
|
277
|
+
Router.post(router, settings.basePath, createWriteHandler(settings, settings.basePath), routeOptions);
|
|
278
|
+
Router.post(router, updatePath, createUpdateHandler(settings, updatePath), routeOptions);
|
|
279
|
+
Router.post(router, markFamilyStalePath, createMarkFamilyStaleHandler(settings, markFamilyStalePath), routeOptions);
|
|
280
|
+
};
|
|
195
281
|
export const TraceIngestGateway = Object.freeze({
|
|
196
282
|
create(overrides) {
|
|
197
283
|
const settings = readSettings(overrides);
|
|
198
|
-
const routeOptions = settings.middleware.length > 0
|
|
199
|
-
? { middleware: settings.middleware }
|
|
200
|
-
: undefined;
|
|
201
|
-
const updatePath = appendSuffix(settings.basePath, '/update');
|
|
202
|
-
const markFamilyStalePath = appendSuffix(settings.basePath, '/mark-family-stale');
|
|
203
284
|
return {
|
|
204
285
|
registerRoutes(router) {
|
|
205
|
-
|
|
206
|
-
Router.post(router, updatePath, createUpdateHandler(settings, updatePath), routeOptions);
|
|
207
|
-
Router.post(router, markFamilyStalePath, createMarkFamilyStaleHandler(settings, markFamilyStalePath), routeOptions);
|
|
286
|
+
registerRoutes(router, settings);
|
|
208
287
|
},
|
|
209
288
|
};
|
|
210
289
|
},
|
|
@@ -13,6 +13,13 @@ const normalizePath = (value) => {
|
|
|
13
13
|
return '/zin/trace/write';
|
|
14
14
|
return trimmed.startsWith('/') ? trimmed : `/${trimmed}`;
|
|
15
15
|
};
|
|
16
|
+
const trimTrailingSlashes = (value) => {
|
|
17
|
+
let trimmed = value;
|
|
18
|
+
while (trimmed.endsWith('/')) {
|
|
19
|
+
trimmed = trimmed.slice(0, -1);
|
|
20
|
+
}
|
|
21
|
+
return trimmed;
|
|
22
|
+
};
|
|
16
23
|
const createUnsupportedReadError = () => ErrorFactory.createConfigError('Trace proxy sender storage does not expose dashboard/query operations. Use the trace server for reads.');
|
|
17
24
|
const buildSettings = (settings) => {
|
|
18
25
|
ensureConfigured(settings);
|
|
@@ -37,7 +44,7 @@ const buildSettings = (settings) => {
|
|
|
37
44
|
};
|
|
38
45
|
};
|
|
39
46
|
const appendSuffix = (path, suffix) => {
|
|
40
|
-
const base = normalizePath(path)
|
|
47
|
+
const base = trimTrailingSlashes(normalizePath(path));
|
|
41
48
|
const tail = suffix.startsWith('/') ? suffix : `/${suffix}`;
|
|
42
49
|
return `${base}${tail}`;
|
|
43
50
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zintrust/trace",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "1.5.0",
|
|
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": "^1.2.0"
|
|
44
44
|
},
|
|
45
45
|
"publishConfig": {
|
|
46
46
|
"access": "public"
|
|
@@ -56,8 +56,20 @@ const parseMiddleware = (value: string): ReadonlyArray<string> =>
|
|
|
56
56
|
.map((entry) => entry.trim())
|
|
57
57
|
.filter((entry) => entry.length > 0);
|
|
58
58
|
|
|
59
|
+
const isParameterizedRateLimitMiddleware = (value: string): boolean => {
|
|
60
|
+
return /^rateLimit:\d+:\d+(?:\.\d+)?$/.test(value.trim());
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const trimTrailingSlashes = (value: string): string => {
|
|
64
|
+
let trimmed = value;
|
|
65
|
+
while (trimmed.endsWith('/')) {
|
|
66
|
+
trimmed = trimmed.slice(0, -1);
|
|
67
|
+
}
|
|
68
|
+
return trimmed;
|
|
69
|
+
};
|
|
70
|
+
|
|
59
71
|
const appendSuffix = (path: string, suffix: string): string => {
|
|
60
|
-
const base = normalizePath(path)
|
|
72
|
+
const base = trimTrailingSlashes(normalizePath(path));
|
|
61
73
|
const tail = suffix.startsWith('/') ? suffix : `/${suffix}`;
|
|
62
74
|
return `${base}${tail}`;
|
|
63
75
|
};
|
|
@@ -259,49 +271,134 @@ const resolveStorage = (overrides?: TraceIngestGatewayOverrides): ITraceStorage
|
|
|
259
271
|
return TraceStorage.resolveStorage(db);
|
|
260
272
|
};
|
|
261
273
|
|
|
262
|
-
const
|
|
263
|
-
|
|
264
|
-
|
|
274
|
+
const readConfiguredKeyId = (overrides?: TraceIngestGatewayOverrides): string => {
|
|
275
|
+
return (overrides?.keyId ?? Env.get('TRACE_PROXY_KEY_ID', '')).trim();
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
const readConfiguredSecret = (overrides?: TraceIngestGatewayOverrides): string => {
|
|
279
|
+
return (overrides?.secret ?? Env.get('TRACE_PROXY_SECRET', '')).trim();
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
const resolveKeyId = (overrides?: TraceIngestGatewayOverrides): string => {
|
|
283
|
+
const configuredKeyId = readConfiguredKeyId(overrides);
|
|
284
|
+
if (configuredKeyId !== '') return configuredKeyId;
|
|
285
|
+
return (Env.APP_NAME || 'zintrust').trim();
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
const resolveSecret = (overrides?: TraceIngestGatewayOverrides): string => {
|
|
289
|
+
const configuredSecret = readConfiguredSecret(overrides);
|
|
290
|
+
if (configuredSecret !== '') return configuredSecret;
|
|
291
|
+
return Env.APP_KEY;
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
const resolveSigningWindowMs = (overrides?: TraceIngestGatewayOverrides): number => {
|
|
295
|
+
return overrides?.signingWindowMs ?? Env.getInt('TRACE_PROXY_SIGNING_WINDOW_MS', 60000);
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
const resolveNonceTtlMs = (overrides?: TraceIngestGatewayOverrides): number => {
|
|
299
|
+
return overrides?.nonceTtlMs ?? Env.getInt('TRACE_PROXY_NONCE_TTL_MS', 120000);
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
const resolveRateLimitMax = (): number => {
|
|
303
|
+
return Env.getInt('TRACE_PROXY_RATE_LIMIT_MAX', 0);
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
const resolveRateLimitWindowMinutes = (): number => {
|
|
307
|
+
const windowMinutes = Env.getFloat('TRACE_PROXY_RATE_LIMIT_WINDOW_MINUTES', 0);
|
|
308
|
+
return Math.max(windowMinutes, 0);
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
const createRateLimitMiddleware = (): string | undefined => {
|
|
312
|
+
const max = resolveRateLimitMax();
|
|
313
|
+
const windowMinutes = resolveRateLimitWindowMinutes();
|
|
265
314
|
|
|
315
|
+
if (!Number.isFinite(max) || !Number.isInteger(max)) {
|
|
316
|
+
throw ErrorFactory.createConfigError('TRACE_PROXY_RATE_LIMIT_MAX must be a valid integer', {
|
|
317
|
+
value: max,
|
|
318
|
+
envKey: 'TRACE_PROXY_RATE_LIMIT_MAX',
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if (!Number.isFinite(windowMinutes)) {
|
|
323
|
+
throw ErrorFactory.createConfigError(
|
|
324
|
+
'TRACE_PROXY_RATE_LIMIT_WINDOW_MINUTES must be a valid number',
|
|
325
|
+
{
|
|
326
|
+
value: windowMinutes,
|
|
327
|
+
envKey: 'TRACE_PROXY_RATE_LIMIT_WINDOW_MINUTES',
|
|
328
|
+
}
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if (max <= 0 || windowMinutes <= 0) {
|
|
333
|
+
return undefined;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
return `rateLimit:${max}:${windowMinutes}`;
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
const resolveMiddleware = (overrides?: TraceIngestGatewayOverrides): ReadonlyArray<string> => {
|
|
340
|
+
if (overrides?.middleware !== undefined) {
|
|
341
|
+
return overrides.middleware;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const configured = parseMiddleware(Env.get('TRACE_PROXY_MIDDLEWARE', ''));
|
|
345
|
+
const rateLimitMiddleware = createRateLimitMiddleware();
|
|
346
|
+
if (rateLimitMiddleware === undefined) {
|
|
347
|
+
return configured;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
return [
|
|
351
|
+
...configured.filter((entry) => !isParameterizedRateLimitMiddleware(entry)),
|
|
352
|
+
rateLimitMiddleware,
|
|
353
|
+
];
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
const readSettings = (overrides?: TraceIngestGatewayOverrides): TraceIngestGatewaySettings => {
|
|
266
357
|
return {
|
|
267
358
|
basePath: normalizePath(overrides?.basePath ?? Env.get('TRACE_PROXY_PATH', '/zin/trace/write')),
|
|
268
|
-
keyId:
|
|
269
|
-
secret:
|
|
270
|
-
signingWindowMs:
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
middleware: overrides?.middleware ?? parseMiddleware(Env.get('TRACE_PROXY_MIDDLEWARE', '')),
|
|
359
|
+
keyId: resolveKeyId(overrides),
|
|
360
|
+
secret: resolveSecret(overrides),
|
|
361
|
+
signingWindowMs: resolveSigningWindowMs(overrides),
|
|
362
|
+
nonceTtlMs: resolveNonceTtlMs(overrides),
|
|
363
|
+
middleware: resolveMiddleware(overrides),
|
|
274
364
|
storage: resolveStorage(overrides),
|
|
275
365
|
};
|
|
276
366
|
};
|
|
277
367
|
|
|
368
|
+
const getRouteOptions = (settings: TraceIngestGatewaySettings): RouteOptions | undefined => {
|
|
369
|
+
if (settings.middleware.length === 0) return undefined;
|
|
370
|
+
return { middleware: settings.middleware } as RouteOptions;
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
const registerRoutes = (router: IRouter, settings: TraceIngestGatewaySettings): void => {
|
|
374
|
+
const routeOptions = getRouteOptions(settings);
|
|
375
|
+
const updatePath = appendSuffix(settings.basePath, '/update');
|
|
376
|
+
const markFamilyStalePath = appendSuffix(settings.basePath, '/mark-family-stale');
|
|
377
|
+
|
|
378
|
+
Router.post(
|
|
379
|
+
router,
|
|
380
|
+
settings.basePath,
|
|
381
|
+
createWriteHandler(settings, settings.basePath),
|
|
382
|
+
routeOptions
|
|
383
|
+
);
|
|
384
|
+
Router.post(router, updatePath, createUpdateHandler(settings, updatePath), routeOptions);
|
|
385
|
+
Router.post(
|
|
386
|
+
router,
|
|
387
|
+
markFamilyStalePath,
|
|
388
|
+
createMarkFamilyStaleHandler(settings, markFamilyStalePath),
|
|
389
|
+
routeOptions
|
|
390
|
+
);
|
|
391
|
+
};
|
|
392
|
+
|
|
278
393
|
export const TraceIngestGateway = Object.freeze({
|
|
279
394
|
create(overrides?: TraceIngestGatewayOverrides): {
|
|
280
395
|
registerRoutes: (router: IRouter) => void;
|
|
281
396
|
} {
|
|
282
397
|
const settings = readSettings(overrides);
|
|
283
|
-
const routeOptions: RouteOptions | undefined =
|
|
284
|
-
settings.middleware.length > 0
|
|
285
|
-
? ({ middleware: settings.middleware } as RouteOptions)
|
|
286
|
-
: undefined;
|
|
287
|
-
const updatePath = appendSuffix(settings.basePath, '/update');
|
|
288
|
-
const markFamilyStalePath = appendSuffix(settings.basePath, '/mark-family-stale');
|
|
289
398
|
|
|
290
399
|
return {
|
|
291
400
|
registerRoutes(router: IRouter): void {
|
|
292
|
-
|
|
293
|
-
router,
|
|
294
|
-
settings.basePath,
|
|
295
|
-
createWriteHandler(settings, settings.basePath),
|
|
296
|
-
routeOptions
|
|
297
|
-
);
|
|
298
|
-
Router.post(router, updatePath, createUpdateHandler(settings, updatePath), routeOptions);
|
|
299
|
-
Router.post(
|
|
300
|
-
router,
|
|
301
|
-
markFamilyStalePath,
|
|
302
|
-
createMarkFamilyStaleHandler(settings, markFamilyStalePath),
|
|
303
|
-
routeOptions
|
|
304
|
-
);
|
|
401
|
+
registerRoutes(router, settings);
|
|
305
402
|
},
|
|
306
403
|
};
|
|
307
404
|
},
|
|
@@ -41,6 +41,14 @@ const normalizePath = (value: string): string => {
|
|
|
41
41
|
return trimmed.startsWith('/') ? trimmed : `/${trimmed}`;
|
|
42
42
|
};
|
|
43
43
|
|
|
44
|
+
const trimTrailingSlashes = (value: string): string => {
|
|
45
|
+
let trimmed = value;
|
|
46
|
+
while (trimmed.endsWith('/')) {
|
|
47
|
+
trimmed = trimmed.slice(0, -1);
|
|
48
|
+
}
|
|
49
|
+
return trimmed;
|
|
50
|
+
};
|
|
51
|
+
|
|
44
52
|
const createUnsupportedReadError = (): Error =>
|
|
45
53
|
ErrorFactory.createConfigError(
|
|
46
54
|
'Trace proxy sender storage does not expose dashboard/query operations. Use the trace server for reads.'
|
|
@@ -90,7 +98,7 @@ const buildSettings = (settings: ProxyTraceStorageSettings): ProxyRequestSetting
|
|
|
90
98
|
};
|
|
91
99
|
|
|
92
100
|
const appendSuffix = (path: string, suffix: string): string => {
|
|
93
|
-
const base = normalizePath(path)
|
|
101
|
+
const base = trimTrailingSlashes(normalizePath(path));
|
|
94
102
|
const tail = suffix.startsWith('/') ? suffix : `/${suffix}`;
|
|
95
103
|
return `${base}${tail}`;
|
|
96
104
|
};
|
|
@@ -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 });
|