@zintrust/trace 0.9.3 → 0.9.4
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 +31 -0
- package/dist/build-manifest.json +59 -23
- package/dist/config.js +18 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/ingest/TraceIngestGateway.d.ts +22 -0
- package/dist/ingest/TraceIngestGateway.js +215 -0
- package/dist/register.js +58 -2
- package/dist/storage/DebuggerStorage.d.ts +13 -0
- package/dist/storage/DebuggerStorage.js +195 -0
- package/dist/storage/ProxyTraceStorage.d.ts +12 -0
- package/dist/storage/ProxyTraceStorage.js +102 -0
- package/dist/storage/TraceContentBudget.js +1 -0
- package/dist/storage/TraceServiceTag.d.ts +5 -0
- package/dist/storage/TraceServiceTag.js +43 -0
- package/dist/storage/index.d.ts +2 -0
- package/dist/storage/index.js +2 -0
- package/dist/types.d.ts +10 -0
- package/package.json +2 -2
- package/src/config.ts +23 -0
- package/src/index.ts +1 -0
- package/src/ingest/TraceIngestGateway.ts +317 -0
- package/src/register.ts +67 -2
- package/src/storage/ProxyTraceStorage.ts +182 -0
- package/src/storage/TraceContentBudget.ts +1 -0
- package/src/storage/TraceServiceTag.ts +56 -0
- package/src/storage/index.ts +2 -0
- package/src/types.ts +11 -0
package/README.md
CHANGED
|
@@ -32,6 +32,13 @@ You can still import the package migrations manually if you prefer to keep them
|
|
|
32
32
|
TRACE_ENABLED=true
|
|
33
33
|
TRACE_DB_CONNECTION=d1 # optional — omit to inherit DB_CONNECTION
|
|
34
34
|
TRACE_QUERY_CONNECTION=main # optional — app DB to observe for SQL traces
|
|
35
|
+
TRACE_SERVICE_TAG= # optional — global trace tag, falls back to APP_NAME when empty
|
|
36
|
+
TRACE_PROXY=false # optional — send writes to a remote trace server instead of local DB
|
|
37
|
+
TRACE_PROXY_URL= # required when TRACE_PROXY=true
|
|
38
|
+
TRACE_PROXY_PATH=/zin/trace/write
|
|
39
|
+
TRACE_PROXY_KEY_ID= # optional — falls back to APP_NAME
|
|
40
|
+
TRACE_PROXY_SECRET= # optional — falls back to APP_KEY
|
|
41
|
+
TRACE_PROXY_TIMEOUT_MS=30000
|
|
35
42
|
TRACE_PRUNE_HOURS=24 # how long entries are kept (default: 24)
|
|
36
43
|
TRACE_SLOW_QUERY_MS=100 # slow-query threshold in ms (default: 100)
|
|
37
44
|
TRACE_LOG_LEVEL=info # minimum log level captured (default: info)
|
|
@@ -52,6 +59,8 @@ TRACE_REDACT_QUERY=
|
|
|
52
59
|
|
|
53
60
|
When `TRACE_CONTENT_QUEUE_DRIVER` is set, trace writes enqueue through that registered queue driver and an internal trace worker drains them outside the live request path. When it is unset, oversized content is replaced with `Trace content exceeded budget and was replaced.` before persistence instead of running the heavy compaction loop inline.
|
|
54
61
|
|
|
62
|
+
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
|
+
|
|
55
64
|
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.
|
|
56
65
|
|
|
57
66
|
### 2. Enable the plugin in `zintrust.plugins.*`
|
|
@@ -154,6 +163,28 @@ registerTraceDashboard(router, {
|
|
|
154
163
|
});
|
|
155
164
|
```
|
|
156
165
|
|
|
166
|
+
### 3b. Optional remote trace ingest gateway
|
|
167
|
+
|
|
168
|
+
If you want one project to send trace writes to another project, mount the signed ingest gateway on the trace server:
|
|
169
|
+
|
|
170
|
+
```ts
|
|
171
|
+
// routes/api.ts
|
|
172
|
+
import { registerTraceIngestGateway } from '@zintrust/trace';
|
|
173
|
+
|
|
174
|
+
registerTraceIngestGateway(router, {
|
|
175
|
+
basePath: '/zin/trace/write',
|
|
176
|
+
middleware: ['admin'], // optional
|
|
177
|
+
});
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
The sender runtime uses `TRACE_PROXY_URL`, `TRACE_PROXY_PATH`, `TRACE_PROXY_KEY_ID`, and `TRACE_PROXY_SECRET` to sign write requests. On the receiver side, `TRACE_PROXY_KEY_ID` and `TRACE_PROXY_SECRET` must match the sender pair or fall back to the same `APP_NAME` and `APP_KEY` values.
|
|
181
|
+
|
|
182
|
+
The ingest gateway exposes exactly these signed POST endpoints:
|
|
183
|
+
|
|
184
|
+
- `TRACE_PROXY_PATH` for new trace entries
|
|
185
|
+
- `TRACE_PROXY_PATH + /update` for trace content updates
|
|
186
|
+
- `TRACE_PROXY_PATH + /mark-family-stale` for latest-entry rotation
|
|
187
|
+
|
|
157
188
|
The dashboard SPA will be available at `GET /trace` (or your chosen `basePath`).
|
|
158
189
|
|
|
159
190
|
If you need custom storage wiring, keep using the low-level route registrar:
|
package/dist/build-manifest.json
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zintrust/trace",
|
|
3
|
-
"version": "0.9.
|
|
4
|
-
"buildDate": "2026-04-
|
|
3
|
+
"version": "0.9.4",
|
|
4
|
+
"buildDate": "2026-04-23T11:29:28.904Z",
|
|
5
5
|
"buildEnvironment": {
|
|
6
|
-
"node": "
|
|
7
|
-
"platform": "
|
|
8
|
-
"arch": "
|
|
6
|
+
"node": "v22.22.1",
|
|
7
|
+
"platform": "darwin",
|
|
8
|
+
"arch": "arm64"
|
|
9
9
|
},
|
|
10
10
|
"git": {
|
|
11
|
-
"commit": "
|
|
12
|
-
"branch": "
|
|
11
|
+
"commit": "eebcc3ad",
|
|
12
|
+
"branch": "release"
|
|
13
13
|
},
|
|
14
14
|
"package": {
|
|
15
15
|
"engines": {
|
|
@@ -29,6 +29,10 @@
|
|
|
29
29
|
"size": 4640,
|
|
30
30
|
"sha256": "c51cc312046b6b2bbe1673f1ff9508425cc7140a1d2341907f67aa36069c09f9"
|
|
31
31
|
},
|
|
32
|
+
"build-manifest.json": {
|
|
33
|
+
"size": 14744,
|
|
34
|
+
"sha256": "92fc9e1e76ba84696dbdaf02aa2ed7588c6e1234d586e24bf8bab4f54104ad61"
|
|
35
|
+
},
|
|
32
36
|
"cli-register.d.ts": {
|
|
33
37
|
"size": 255,
|
|
34
38
|
"sha256": "da8d689fe5ef32e97e755f28017e4d3cb1aa63489073a71907ea41ad5761ede9"
|
|
@@ -42,8 +46,8 @@
|
|
|
42
46
|
"sha256": "b034cbef0c71fb868071363624ef7a9f8d7acc20f8be8c895dd5db5a75e81f37"
|
|
43
47
|
},
|
|
44
48
|
"config.js": {
|
|
45
|
-
"size":
|
|
46
|
-
"sha256": "
|
|
49
|
+
"size": 10503,
|
|
50
|
+
"sha256": "4514e95067194c7afacb1202a8f1caffc64a400dd995cf809301b1a51a67da38"
|
|
47
51
|
},
|
|
48
52
|
"context.d.ts": {
|
|
49
53
|
"size": 596,
|
|
@@ -78,12 +82,20 @@
|
|
|
78
82
|
"sha256": "b260cd17012cd5bf5f24f24eb8958d0cf79bed1d976412def3706f960b3aefa6"
|
|
79
83
|
},
|
|
80
84
|
"index.d.ts": {
|
|
81
|
-
"size":
|
|
82
|
-
"sha256": "
|
|
85
|
+
"size": 2693,
|
|
86
|
+
"sha256": "901aaefe88fb2cc1e7771cd541f41ebcfe6443390bd66905eacbda51f1a6f73d"
|
|
83
87
|
},
|
|
84
88
|
"index.js": {
|
|
85
|
-
"size":
|
|
86
|
-
"sha256": "
|
|
89
|
+
"size": 3421,
|
|
90
|
+
"sha256": "478b80c6aa6c2eea2d109aa2e6fe090d77469d186b3e6a19ababb7d1f6d0be7e"
|
|
91
|
+
},
|
|
92
|
+
"ingest/TraceIngestGateway.d.ts": {
|
|
93
|
+
"size": 786,
|
|
94
|
+
"sha256": "3a0a46fa5bbf5367047214533ec0ee92a171ee35a60d25c197eea1eed1ff0f65"
|
|
95
|
+
},
|
|
96
|
+
"ingest/TraceIngestGateway.js": {
|
|
97
|
+
"size": 8456,
|
|
98
|
+
"sha256": "56df56cecda7110d1096c3beef1ef62c386f4a0e85120ebfb4cdfc5ab3b758d7"
|
|
87
99
|
},
|
|
88
100
|
"migrations/20260331000001_create_zin_trace_entries_table.d.ts": {
|
|
89
101
|
"size": 304,
|
|
@@ -138,16 +150,32 @@
|
|
|
138
150
|
"sha256": "71d366165dd36f1675aa253a76262b226fb6c62e5ab632746b8aea61c0c625fc"
|
|
139
151
|
},
|
|
140
152
|
"register.js": {
|
|
141
|
-
"size":
|
|
142
|
-
"sha256": "
|
|
153
|
+
"size": 19822,
|
|
154
|
+
"sha256": "c553356d90c812f7de430d5679b1d44468131433e034ae2ea89e2876b1254444"
|
|
155
|
+
},
|
|
156
|
+
"storage/DebuggerStorage.d.ts": {
|
|
157
|
+
"size": 517,
|
|
158
|
+
"sha256": "c9c215aaa414f7b0c1fec6c82b054fc52bdf97af58f96f35c7f96672fb859c31"
|
|
159
|
+
},
|
|
160
|
+
"storage/DebuggerStorage.js": {
|
|
161
|
+
"size": 7442,
|
|
162
|
+
"sha256": "5ecce0fcfcf695df587a7b90a7a5c7efd2e64ad13c9f2d104b392f89f34f0dc4"
|
|
163
|
+
},
|
|
164
|
+
"storage/ProxyTraceStorage.d.ts": {
|
|
165
|
+
"size": 339,
|
|
166
|
+
"sha256": "9c724ff342dfe82da12e7cce95593f6623faa80c40f97593719b8e78e95f266d"
|
|
167
|
+
},
|
|
168
|
+
"storage/ProxyTraceStorage.js": {
|
|
169
|
+
"size": 4158,
|
|
170
|
+
"sha256": "097d82e94c6ef38661fb57a20d378ba890dbacd03a22666aaf4aba4dfdedf735"
|
|
143
171
|
},
|
|
144
172
|
"storage/TraceContentBudget.d.ts": {
|
|
145
173
|
"size": 1306,
|
|
146
174
|
"sha256": "606a37af0a4aef4866c22cc727f67f485c43181b40eb831e1920b8b90fdaf503"
|
|
147
175
|
},
|
|
148
176
|
"storage/TraceContentBudget.js": {
|
|
149
|
-
"size":
|
|
150
|
-
"sha256": "
|
|
177
|
+
"size": 11434,
|
|
178
|
+
"sha256": "f53229e7233cf13b095780451b68c912a95dc3f44e17c49289446219231e7e06"
|
|
151
179
|
},
|
|
152
180
|
"storage/TraceContentRedaction.d.ts": {
|
|
153
181
|
"size": 207,
|
|
@@ -165,6 +193,14 @@
|
|
|
165
193
|
"size": 2465,
|
|
166
194
|
"sha256": "ace7e492373d2dccdbefe20040d30fffd7ad9f8e71113b7a044bddf92dcdf6fb"
|
|
167
195
|
},
|
|
196
|
+
"storage/TraceServiceTag.d.ts": {
|
|
197
|
+
"size": 224,
|
|
198
|
+
"sha256": "35d93c82d6db80071946adaa8e40d012408b469949f1002f85ae3fd20b0a70c8"
|
|
199
|
+
},
|
|
200
|
+
"storage/TraceServiceTag.js": {
|
|
201
|
+
"size": 1916,
|
|
202
|
+
"sha256": "1ddad82563c98ddbb4033e9b8fb31901d635cffa7024d499a896f0d7a6d00900"
|
|
203
|
+
},
|
|
168
204
|
"storage/TraceStorage.d.ts": {
|
|
169
205
|
"size": 517,
|
|
170
206
|
"sha256": "c9c215aaa414f7b0c1fec6c82b054fc52bdf97af58f96f35c7f96672fb859c31"
|
|
@@ -182,16 +218,16 @@
|
|
|
182
218
|
"sha256": "6fc34e6e52a9b463db0967dba4aec4c8c706b6978c496f568637ffc15327279a"
|
|
183
219
|
},
|
|
184
220
|
"storage/index.d.ts": {
|
|
185
|
-
"size":
|
|
186
|
-
"sha256": "
|
|
221
|
+
"size": 210,
|
|
222
|
+
"sha256": "bb4c3a0c73eb3e2629dd1a1dbc29eecedd1910a37b221357cc7981ced320bdeb"
|
|
187
223
|
},
|
|
188
224
|
"storage/index.js": {
|
|
189
|
-
"size":
|
|
190
|
-
"sha256": "
|
|
225
|
+
"size": 166,
|
|
226
|
+
"sha256": "ed5e83e108cd15f9a3976be71e966bdf0c44d8e0266d1d2dbad189f0885ad501"
|
|
191
227
|
},
|
|
192
228
|
"types.d.ts": {
|
|
193
|
-
"size":
|
|
194
|
-
"sha256": "
|
|
229
|
+
"size": 10037,
|
|
230
|
+
"sha256": "68a2de953af2fce1a12447078177ff886f10e853c760378e13478b2558648868"
|
|
195
231
|
},
|
|
196
232
|
"types.js": {
|
|
197
233
|
"size": 696,
|
package/dist/config.js
CHANGED
|
@@ -163,10 +163,27 @@ const mergeContentDispatch = (base, override) => {
|
|
|
163
163
|
},
|
|
164
164
|
};
|
|
165
165
|
};
|
|
166
|
+
const mergeProxyConfig = (base, override) => {
|
|
167
|
+
if (override === undefined)
|
|
168
|
+
return base;
|
|
169
|
+
return {
|
|
170
|
+
...base,
|
|
171
|
+
...override,
|
|
172
|
+
};
|
|
173
|
+
};
|
|
166
174
|
const DEFAULTS = Object.freeze({
|
|
167
175
|
enabled: false,
|
|
168
176
|
connection: undefined,
|
|
169
177
|
observeConnection: undefined,
|
|
178
|
+
serviceTag: undefined,
|
|
179
|
+
proxy: {
|
|
180
|
+
enabled: false,
|
|
181
|
+
url: undefined,
|
|
182
|
+
path: '/zin/trace/write',
|
|
183
|
+
keyId: undefined,
|
|
184
|
+
secret: undefined,
|
|
185
|
+
timeoutMs: 30000,
|
|
186
|
+
},
|
|
170
187
|
pruneAfterHours: 24,
|
|
171
188
|
ignoreRoutes: ['/trace', '/health', '/ping'],
|
|
172
189
|
ignorePaths: [],
|
|
@@ -247,6 +264,7 @@ export const TraceConfig = Object.freeze({
|
|
|
247
264
|
return Object.freeze({
|
|
248
265
|
...DEFAULTS,
|
|
249
266
|
...overrides,
|
|
267
|
+
proxy: mergeProxyConfig(DEFAULTS.proxy, overrides.proxy),
|
|
250
268
|
contentDispatch: mergeContentDispatch(DEFAULTS.contentDispatch, overrides.contentDispatch),
|
|
251
269
|
watchers: mergeWatchers(DEFAULTS.watchers, overrides.watchers),
|
|
252
270
|
redaction: {
|
package/dist/index.d.ts
CHANGED
|
@@ -13,6 +13,7 @@ export { TraceContentRedaction } from './storage/TraceContentRedaction';
|
|
|
13
13
|
export { TraceContext } from './context';
|
|
14
14
|
export { registerTraceDashboard, registerTraceRoutes } from './dashboard/routes';
|
|
15
15
|
export type { TraceDashboardOptions, TraceDashboardRegistrationOptions } from './dashboard/routes';
|
|
16
|
+
export { registerTraceIngestGateway, TraceIngestGateway } from './ingest/TraceIngestGateway';
|
|
16
17
|
export { AuthWatcher } from './watchers/AuthWatcher';
|
|
17
18
|
export { BatchWatcher } from './watchers/BatchWatcher';
|
|
18
19
|
export { CacheWatcher } from './watchers/CacheWatcher';
|
package/dist/index.js
CHANGED
|
@@ -24,6 +24,7 @@ export { TraceContext } from './context.js';
|
|
|
24
24
|
// Dashboard
|
|
25
25
|
// ---------------------------------------------------------------------------
|
|
26
26
|
export { registerTraceDashboard, registerTraceRoutes } from './dashboard/routes.js';
|
|
27
|
+
export { registerTraceIngestGateway, TraceIngestGateway } from './ingest/TraceIngestGateway.js';
|
|
27
28
|
// ---------------------------------------------------------------------------
|
|
28
29
|
// Watchers (named re-exports for use with custom wiring)
|
|
29
30
|
// ---------------------------------------------------------------------------
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type IRouter } from '@zintrust/core';
|
|
2
|
+
import type { ITraceStorage } from '../types';
|
|
3
|
+
type TraceIngestGatewaySettings = {
|
|
4
|
+
basePath: string;
|
|
5
|
+
keyId: string;
|
|
6
|
+
secret: string;
|
|
7
|
+
signingWindowMs: number;
|
|
8
|
+
nonceTtlMs: number;
|
|
9
|
+
middleware: ReadonlyArray<string>;
|
|
10
|
+
storage: ITraceStorage;
|
|
11
|
+
};
|
|
12
|
+
type TraceIngestGatewayOverrides = Partial<Omit<TraceIngestGatewaySettings, 'storage'> & {
|
|
13
|
+
storage: ITraceStorage;
|
|
14
|
+
connectionName: string;
|
|
15
|
+
}>;
|
|
16
|
+
export declare const TraceIngestGateway: Readonly<{
|
|
17
|
+
create(overrides?: TraceIngestGatewayOverrides): {
|
|
18
|
+
registerRoutes: (router: IRouter) => void;
|
|
19
|
+
};
|
|
20
|
+
}>;
|
|
21
|
+
export declare const registerTraceIngestGateway: (router: IRouter, overrides?: TraceIngestGatewayOverrides) => void;
|
|
22
|
+
export default TraceIngestGateway;
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import { Env, ErrorFactory, Router, SignedRequest, useDatabase, } from '@zintrust/core';
|
|
2
|
+
import { TraceConfig } from '../config.js';
|
|
3
|
+
import { TraceStorage } from '../storage/index.js';
|
|
4
|
+
const nonces = new Map();
|
|
5
|
+
const nowMs = () => Date.now();
|
|
6
|
+
const normalizePath = (value) => {
|
|
7
|
+
const trimmed = value.trim();
|
|
8
|
+
if (trimmed === '')
|
|
9
|
+
return '/zin/trace/write';
|
|
10
|
+
return trimmed.startsWith('/') ? trimmed : `/${trimmed}`;
|
|
11
|
+
};
|
|
12
|
+
const parseMiddleware = (value) => value
|
|
13
|
+
.split(',')
|
|
14
|
+
.map((entry) => entry.trim())
|
|
15
|
+
.filter((entry) => entry.length > 0);
|
|
16
|
+
const appendSuffix = (path, suffix) => {
|
|
17
|
+
const base = normalizePath(path).replace(/\/+$/, '');
|
|
18
|
+
const tail = suffix.startsWith('/') ? suffix : `/${suffix}`;
|
|
19
|
+
return `${base}${tail}`;
|
|
20
|
+
};
|
|
21
|
+
const cleanupExpiredNonces = () => {
|
|
22
|
+
const current = nowMs();
|
|
23
|
+
for (const [nonceKey, expiresAt] of nonces.entries()) {
|
|
24
|
+
if (expiresAt <= current) {
|
|
25
|
+
nonces.delete(nonceKey);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
const storeNonce = async (keyId, nonce, ttlMs) => {
|
|
30
|
+
cleanupExpiredNonces();
|
|
31
|
+
const nonceKey = `${keyId}:${nonce}`;
|
|
32
|
+
if (nonces.has(nonceKey))
|
|
33
|
+
return false;
|
|
34
|
+
nonces.set(nonceKey, nowMs() + Math.max(ttlMs, 1));
|
|
35
|
+
return true;
|
|
36
|
+
};
|
|
37
|
+
const getBodyRecord = (req) => {
|
|
38
|
+
const body = req.getBody?.() ?? req.body;
|
|
39
|
+
if (typeof body === 'object' && body !== null && !Array.isArray(body)) {
|
|
40
|
+
return body;
|
|
41
|
+
}
|
|
42
|
+
return {};
|
|
43
|
+
};
|
|
44
|
+
const getRawBody = (req) => {
|
|
45
|
+
const rawText = req.context['rawBodyText'];
|
|
46
|
+
if (typeof rawText === 'string')
|
|
47
|
+
return rawText;
|
|
48
|
+
return JSON.stringify(getBodyRecord(req));
|
|
49
|
+
};
|
|
50
|
+
const toIncomingHeaders = (req) => {
|
|
51
|
+
const headers = req.getHeaders();
|
|
52
|
+
const normalize = (value) => {
|
|
53
|
+
if (Array.isArray(value))
|
|
54
|
+
return value.join(',');
|
|
55
|
+
return value;
|
|
56
|
+
};
|
|
57
|
+
return {
|
|
58
|
+
'x-zt-key-id': normalize(headers['x-zt-key-id']),
|
|
59
|
+
'x-zt-timestamp': normalize(headers['x-zt-timestamp']),
|
|
60
|
+
'x-zt-nonce': normalize(headers['x-zt-nonce']),
|
|
61
|
+
'x-zt-body-sha256': normalize(headers['x-zt-body-sha256']),
|
|
62
|
+
'x-zt-signature': normalize(headers['x-zt-signature']),
|
|
63
|
+
};
|
|
64
|
+
};
|
|
65
|
+
const sendFailure = (res, status, code, message, details) => {
|
|
66
|
+
const payload = {
|
|
67
|
+
ok: false,
|
|
68
|
+
error: { code, message, details },
|
|
69
|
+
};
|
|
70
|
+
res.status(status).json(payload);
|
|
71
|
+
};
|
|
72
|
+
const sendSuccess = (res) => {
|
|
73
|
+
const payload = { ok: true };
|
|
74
|
+
res.status(200).json(payload);
|
|
75
|
+
};
|
|
76
|
+
const verifyRequest = async (req, bodyText, settings, path) => {
|
|
77
|
+
if (settings.keyId.trim() === '' || settings.secret.trim() === '') {
|
|
78
|
+
return {
|
|
79
|
+
ok: false,
|
|
80
|
+
code: 'CONFIG_ERROR',
|
|
81
|
+
status: 500,
|
|
82
|
+
message: 'Trace ingest signing credentials are not configured',
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
const verifyResult = await SignedRequest.verify({
|
|
86
|
+
method: req.getMethod(),
|
|
87
|
+
url: new URL(path, 'http://localhost'),
|
|
88
|
+
body: bodyText,
|
|
89
|
+
headers: toIncomingHeaders(req),
|
|
90
|
+
nowMs: nowMs(),
|
|
91
|
+
windowMs: settings.signingWindowMs,
|
|
92
|
+
verifyNonce: async (keyId, nonce) => storeNonce(keyId, nonce, settings.nonceTtlMs),
|
|
93
|
+
getSecretForKeyId: async (keyId) => {
|
|
94
|
+
if (keyId === settings.keyId)
|
|
95
|
+
return settings.secret;
|
|
96
|
+
return undefined;
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
if (verifyResult.ok === true)
|
|
100
|
+
return { ok: true };
|
|
101
|
+
return {
|
|
102
|
+
ok: false,
|
|
103
|
+
code: verifyResult.code,
|
|
104
|
+
status: verifyResult.code === 'EXPIRED' || verifyResult.code === 'REPLAYED' ? 401 : 403,
|
|
105
|
+
message: verifyResult.message,
|
|
106
|
+
};
|
|
107
|
+
};
|
|
108
|
+
const createWriteHandler = (settings, path) => {
|
|
109
|
+
return async (req, res) => {
|
|
110
|
+
const body = getBodyRecord(req);
|
|
111
|
+
const auth = await verifyRequest(req, getRawBody(req), settings, path);
|
|
112
|
+
if (auth.ok === false) {
|
|
113
|
+
sendFailure(res, auth.status, auth.code, auth.message);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
const entry = body['entry'];
|
|
117
|
+
if (typeof entry !== 'object' || entry === null || Array.isArray(entry)) {
|
|
118
|
+
sendFailure(res, 400, 'VALIDATION_ERROR', 'entry must be an object');
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
await settings.storage.writeEntry(entry);
|
|
122
|
+
sendSuccess(res);
|
|
123
|
+
};
|
|
124
|
+
};
|
|
125
|
+
const createUpdateHandler = (settings, path) => {
|
|
126
|
+
return async (req, res) => {
|
|
127
|
+
const body = getBodyRecord(req);
|
|
128
|
+
const auth = await verifyRequest(req, getRawBody(req), settings, path);
|
|
129
|
+
if (auth.ok === false) {
|
|
130
|
+
sendFailure(res, auth.status, auth.code, auth.message);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
const uuid = body['uuid'];
|
|
134
|
+
const patch = body['patch'];
|
|
135
|
+
if (typeof uuid !== 'string' || uuid.trim() === '') {
|
|
136
|
+
sendFailure(res, 400, 'VALIDATION_ERROR', 'uuid is required');
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
if (typeof patch !== 'object' || patch === null || Array.isArray(patch)) {
|
|
140
|
+
sendFailure(res, 400, 'VALIDATION_ERROR', 'patch must be an object');
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
await settings.storage.updateEntry(uuid, patch);
|
|
144
|
+
sendSuccess(res);
|
|
145
|
+
};
|
|
146
|
+
};
|
|
147
|
+
const createMarkFamilyStaleHandler = (settings, path) => {
|
|
148
|
+
return async (req, res) => {
|
|
149
|
+
const body = getBodyRecord(req);
|
|
150
|
+
const auth = await verifyRequest(req, getRawBody(req), settings, path);
|
|
151
|
+
if (auth.ok === false) {
|
|
152
|
+
sendFailure(res, auth.status, auth.code, auth.message);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
const familyHash = body['familyHash'];
|
|
156
|
+
const exceptUuid = body['exceptUuid'];
|
|
157
|
+
if (typeof familyHash !== 'string' || familyHash.trim() === '') {
|
|
158
|
+
sendFailure(res, 400, 'VALIDATION_ERROR', 'familyHash is required');
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
if (typeof exceptUuid !== 'string' || exceptUuid.trim() === '') {
|
|
162
|
+
sendFailure(res, 400, 'VALIDATION_ERROR', 'exceptUuid is required');
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
await settings.storage.markFamilyStale(familyHash, exceptUuid);
|
|
166
|
+
sendSuccess(res);
|
|
167
|
+
};
|
|
168
|
+
};
|
|
169
|
+
const resolveStorage = (overrides) => {
|
|
170
|
+
if (overrides?.storage !== undefined)
|
|
171
|
+
return overrides.storage;
|
|
172
|
+
const connectionName = overrides?.connectionName ?? TraceConfig.merge().connection;
|
|
173
|
+
const db = useDatabase(undefined, connectionName);
|
|
174
|
+
if (db === undefined) {
|
|
175
|
+
throw ErrorFactory.createConfigError('Trace ingest connection is not configured.', {
|
|
176
|
+
connectionName,
|
|
177
|
+
envKey: 'TRACE_DB_CONNECTION',
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
return TraceStorage.resolveStorage(db);
|
|
181
|
+
};
|
|
182
|
+
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
|
+
return {
|
|
186
|
+
basePath: normalizePath(overrides?.basePath ?? Env.get('TRACE_PROXY_PATH', '/zin/trace/write')),
|
|
187
|
+
keyId: configuredKeyId === '' ? (Env.APP_NAME || 'zintrust').trim() : configuredKeyId,
|
|
188
|
+
secret: configuredSecret === '' ? Env.APP_KEY : configuredSecret,
|
|
189
|
+
signingWindowMs: overrides?.signingWindowMs ?? Env.getInt('TRACE_PROXY_SIGNING_WINDOW_MS', 60000),
|
|
190
|
+
nonceTtlMs: overrides?.nonceTtlMs ?? Env.getInt('TRACE_PROXY_NONCE_TTL_MS', 120000),
|
|
191
|
+
middleware: overrides?.middleware ?? parseMiddleware(Env.get('TRACE_PROXY_MIDDLEWARE', '')),
|
|
192
|
+
storage: resolveStorage(overrides),
|
|
193
|
+
};
|
|
194
|
+
};
|
|
195
|
+
export const TraceIngestGateway = Object.freeze({
|
|
196
|
+
create(overrides) {
|
|
197
|
+
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
|
+
return {
|
|
204
|
+
registerRoutes(router) {
|
|
205
|
+
Router.post(router, settings.basePath, createWriteHandler(settings, settings.basePath), routeOptions);
|
|
206
|
+
Router.post(router, updatePath, createUpdateHandler(settings, updatePath), routeOptions);
|
|
207
|
+
Router.post(router, markFamilyStalePath, createMarkFamilyStaleHandler(settings, markFamilyStalePath), routeOptions);
|
|
208
|
+
},
|
|
209
|
+
};
|
|
210
|
+
},
|
|
211
|
+
});
|
|
212
|
+
export const registerTraceIngestGateway = (router, overrides) => {
|
|
213
|
+
TraceIngestGateway.create(overrides).registerRoutes(router);
|
|
214
|
+
};
|
|
215
|
+
export default TraceIngestGateway;
|
package/dist/register.js
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
*/
|
|
22
22
|
import { TraceConfig } from './config.js';
|
|
23
23
|
import { TraceContext } from './context.js';
|
|
24
|
-
import { TraceStorage } from './storage/index.js';
|
|
24
|
+
import { ProxyTraceStorage, TraceServiceTag, TraceStorage } from './storage/index.js';
|
|
25
25
|
import { TraceContentBudget } from './storage/TraceContentBudget.js';
|
|
26
26
|
import { TraceContentRedaction } from './storage/TraceContentRedaction.js';
|
|
27
27
|
import { TraceEntryFiltering } from './storage/TraceEntryFiltering.js';
|
|
@@ -142,6 +142,15 @@ if (!traceAlreadyInitialized && Env) {
|
|
|
142
142
|
const pruneAfterHoursRaw = Env.get('TRACE_PRUNE_HOURS', '').trim();
|
|
143
143
|
const slowQueryThresholdRaw = Env.get('TRACE_SLOW_QUERY_MS', '').trim();
|
|
144
144
|
const logMinLevelRaw = Env.get('TRACE_LOG_LEVEL', '').trim();
|
|
145
|
+
const traceProxyRaw = Env.get('TRACE_PROXY', '').trim();
|
|
146
|
+
const traceProxyUrlRaw = Env.get('TRACE_PROXY_URL', '').trim();
|
|
147
|
+
const traceProxyPathRaw = Env.get('TRACE_PROXY_PATH', '').trim();
|
|
148
|
+
const traceProxyKeyIdRaw = Env.get('TRACE_PROXY_KEY_ID', '').trim();
|
|
149
|
+
const traceProxySecretRaw = Env.get('TRACE_PROXY_SECRET', '').trim();
|
|
150
|
+
const traceProxyTimeoutRaw = Env.get('TRACE_PROXY_TIMEOUT_MS', '').trim();
|
|
151
|
+
const traceServiceTagRaw = Env.get('TRACE_SERVICE_TAG', '').trim();
|
|
152
|
+
const appNameRaw = Env.get('APP_NAME', '').trim();
|
|
153
|
+
const appKeyRaw = Env.get('APP_KEY', '').trim();
|
|
145
154
|
const captureCachePayloadsRaw = Env.get('TRACE_CACHE_PAYLOADS', '').trim();
|
|
146
155
|
const captureQueryBindingsRaw = Env.get('TRACE_QUERY_BINDINGS', '').trim();
|
|
147
156
|
const contentDispatchDriverRaw = Env.get('TRACE_CONTENT_QUEUE_DRIVER', '').trim();
|
|
@@ -166,6 +175,21 @@ if (!traceAlreadyInitialized && Env) {
|
|
|
166
175
|
const logMinLevel = (logMinLevelRaw === '' ? startupOverrides?.logMinLevel : logMinLevelRaw);
|
|
167
176
|
const captureCachePayloads = parseEnvBool(captureCachePayloadsRaw) ?? startupOverrides?.captureCachePayloads;
|
|
168
177
|
const captureQueryBindings = parseEnvBool(captureQueryBindingsRaw) ?? startupOverrides?.captureQueryBindings;
|
|
178
|
+
const traceProxyEnabled = parseEnvBool(traceProxyRaw) ?? startupOverrides?.proxy?.enabled;
|
|
179
|
+
const traceProxyUrl = traceProxyUrlRaw === '' ? startupOverrides?.proxy?.url : traceProxyUrlRaw;
|
|
180
|
+
const traceProxyPath = traceProxyPathRaw === '' ? startupOverrides?.proxy?.path : traceProxyPathRaw;
|
|
181
|
+
const traceProxyKeyId = traceProxyKeyIdRaw === ''
|
|
182
|
+
? (startupOverrides?.proxy?.keyId ?? appNameRaw)
|
|
183
|
+
: traceProxyKeyIdRaw;
|
|
184
|
+
const traceProxySecret = traceProxySecretRaw === ''
|
|
185
|
+
? (startupOverrides?.proxy?.secret ?? appKeyRaw)
|
|
186
|
+
: traceProxySecretRaw;
|
|
187
|
+
const traceProxyTimeout = traceProxyTimeoutRaw === ''
|
|
188
|
+
? startupOverrides?.proxy?.timeoutMs
|
|
189
|
+
: Number.parseInt(traceProxyTimeoutRaw, 10);
|
|
190
|
+
const traceServiceTag = traceServiceTagRaw === ''
|
|
191
|
+
? (startupOverrides?.serviceTag ?? appNameRaw).trim() || undefined
|
|
192
|
+
: traceServiceTagRaw;
|
|
169
193
|
const contentDispatchDriver = contentDispatchDriverRaw === ''
|
|
170
194
|
? startupOverrides?.contentDispatch?.driver
|
|
171
195
|
: contentDispatchDriverRaw;
|
|
@@ -201,6 +225,29 @@ if (!traceAlreadyInitialized && Env) {
|
|
|
201
225
|
enabled,
|
|
202
226
|
connection,
|
|
203
227
|
observeConnection,
|
|
228
|
+
...(typeof traceServiceTag === 'string' && traceServiceTag !== ''
|
|
229
|
+
? { serviceTag: traceServiceTag }
|
|
230
|
+
: {}),
|
|
231
|
+
proxy: {
|
|
232
|
+
...TraceConfig.defaults().proxy,
|
|
233
|
+
...startupOverrides?.proxy,
|
|
234
|
+
...(typeof traceProxyEnabled === 'boolean' ? { enabled: traceProxyEnabled } : {}),
|
|
235
|
+
...(typeof traceProxyUrl === 'string' && traceProxyUrl !== ''
|
|
236
|
+
? { url: traceProxyUrl }
|
|
237
|
+
: {}),
|
|
238
|
+
...(typeof traceProxyPath === 'string' && traceProxyPath !== ''
|
|
239
|
+
? { path: traceProxyPath }
|
|
240
|
+
: {}),
|
|
241
|
+
...(typeof traceProxyKeyId === 'string' && traceProxyKeyId !== ''
|
|
242
|
+
? { keyId: traceProxyKeyId }
|
|
243
|
+
: {}),
|
|
244
|
+
...(typeof traceProxySecret === 'string' && traceProxySecret !== ''
|
|
245
|
+
? { secret: traceProxySecret }
|
|
246
|
+
: {}),
|
|
247
|
+
...(typeof traceProxyTimeout === 'number' && Number.isFinite(traceProxyTimeout)
|
|
248
|
+
? { timeoutMs: traceProxyTimeout }
|
|
249
|
+
: {}),
|
|
250
|
+
},
|
|
204
251
|
...(typeof pruneAfterHours === 'number' && Number.isFinite(pruneAfterHours)
|
|
205
252
|
? { pruneAfterHours }
|
|
206
253
|
: {}),
|
|
@@ -264,7 +311,16 @@ if (!traceAlreadyInitialized && Env) {
|
|
|
264
311
|
envKey: 'TRACE_QUERY_CONNECTION',
|
|
265
312
|
});
|
|
266
313
|
await assertTraceStorageReady(core, storageDb, resolvedConnectionName);
|
|
267
|
-
const
|
|
314
|
+
const resolvedStorage = config.proxy.enabled
|
|
315
|
+
? ProxyTraceStorage.create({
|
|
316
|
+
baseUrl: config.proxy.url ?? '',
|
|
317
|
+
path: config.proxy.path,
|
|
318
|
+
keyId: config.proxy.keyId ?? '',
|
|
319
|
+
secret: config.proxy.secret ?? '',
|
|
320
|
+
timeoutMs: config.proxy.timeoutMs,
|
|
321
|
+
})
|
|
322
|
+
: TraceStorage.resolveStorage(storageDb);
|
|
323
|
+
const storage = TraceWriteDiagnostics.wrapStorage(TraceContentBudget.wrapStorage(TraceContentRedaction.wrapStorage(TraceEntryFiltering.wrapStorage(TraceServiceTag.wrapStorage(resolvedStorage, config), config), config.redaction), config), {
|
|
268
324
|
connectionName: resolvedConnectionName,
|
|
269
325
|
logger: core.Logger,
|
|
270
326
|
});
|
|
@@ -0,0 +1,13 @@
|
|
|
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';
|