@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
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Env,
|
|
3
|
+
ErrorFactory,
|
|
4
|
+
Router,
|
|
5
|
+
SignedRequest,
|
|
6
|
+
useDatabase,
|
|
7
|
+
type IRequest,
|
|
8
|
+
type IResponse,
|
|
9
|
+
type IRouter,
|
|
10
|
+
type RouteOptions,
|
|
11
|
+
} from '@zintrust/core';
|
|
12
|
+
import { TraceConfig } from '../config';
|
|
13
|
+
import { TraceStorage } from '../storage';
|
|
14
|
+
import type { ITraceEntry, ITraceStorage } from '../types';
|
|
15
|
+
|
|
16
|
+
type TraceIngestGatewaySettings = {
|
|
17
|
+
basePath: string;
|
|
18
|
+
keyId: string;
|
|
19
|
+
secret: string;
|
|
20
|
+
signingWindowMs: number;
|
|
21
|
+
nonceTtlMs: number;
|
|
22
|
+
middleware: ReadonlyArray<string>;
|
|
23
|
+
storage: ITraceStorage;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
type TraceIngestGatewayOverrides = Partial<
|
|
27
|
+
Omit<TraceIngestGatewaySettings, 'storage'> & { storage: ITraceStorage; connectionName: string }
|
|
28
|
+
>;
|
|
29
|
+
|
|
30
|
+
type TraceGatewayFailure = {
|
|
31
|
+
ok: false;
|
|
32
|
+
error: {
|
|
33
|
+
code: string;
|
|
34
|
+
message: string;
|
|
35
|
+
details?: unknown;
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
type TraceGatewaySuccess = {
|
|
40
|
+
ok: true;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const nonces = new Map<string, number>();
|
|
44
|
+
|
|
45
|
+
const nowMs = (): number => Date.now();
|
|
46
|
+
|
|
47
|
+
const normalizePath = (value: string): string => {
|
|
48
|
+
const trimmed = value.trim();
|
|
49
|
+
if (trimmed === '') return '/zin/trace/write';
|
|
50
|
+
return trimmed.startsWith('/') ? trimmed : `/${trimmed}`;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const parseMiddleware = (value: string): ReadonlyArray<string> =>
|
|
54
|
+
value
|
|
55
|
+
.split(',')
|
|
56
|
+
.map((entry) => entry.trim())
|
|
57
|
+
.filter((entry) => entry.length > 0);
|
|
58
|
+
|
|
59
|
+
const appendSuffix = (path: string, suffix: string): string => {
|
|
60
|
+
const base = normalizePath(path).replace(/\/+$/, '');
|
|
61
|
+
const tail = suffix.startsWith('/') ? suffix : `/${suffix}`;
|
|
62
|
+
return `${base}${tail}`;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const cleanupExpiredNonces = (): void => {
|
|
66
|
+
const current = nowMs();
|
|
67
|
+
for (const [nonceKey, expiresAt] of nonces.entries()) {
|
|
68
|
+
if (expiresAt <= current) {
|
|
69
|
+
nonces.delete(nonceKey);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const storeNonce = async (keyId: string, nonce: string, ttlMs: number): Promise<boolean> => {
|
|
75
|
+
cleanupExpiredNonces();
|
|
76
|
+
const nonceKey = `${keyId}:${nonce}`;
|
|
77
|
+
if (nonces.has(nonceKey)) return false;
|
|
78
|
+
nonces.set(nonceKey, nowMs() + Math.max(ttlMs, 1));
|
|
79
|
+
return true;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const getBodyRecord = (req: IRequest): Record<string, unknown> => {
|
|
83
|
+
const body = req.getBody?.() ?? req.body;
|
|
84
|
+
if (typeof body === 'object' && body !== null && !Array.isArray(body)) {
|
|
85
|
+
return body as Record<string, unknown>;
|
|
86
|
+
}
|
|
87
|
+
return {};
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const getRawBody = (req: IRequest): string => {
|
|
91
|
+
const rawText = req.context['rawBodyText'];
|
|
92
|
+
if (typeof rawText === 'string') return rawText;
|
|
93
|
+
return JSON.stringify(getBodyRecord(req));
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const toIncomingHeaders = (req: IRequest): Record<string, string | undefined> => {
|
|
97
|
+
const headers = req.getHeaders();
|
|
98
|
+
const normalize = (value: string | string[] | undefined): string | undefined => {
|
|
99
|
+
if (Array.isArray(value)) return value.join(',');
|
|
100
|
+
return value;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
'x-zt-key-id': normalize(headers['x-zt-key-id']),
|
|
105
|
+
'x-zt-timestamp': normalize(headers['x-zt-timestamp']),
|
|
106
|
+
'x-zt-nonce': normalize(headers['x-zt-nonce']),
|
|
107
|
+
'x-zt-body-sha256': normalize(headers['x-zt-body-sha256']),
|
|
108
|
+
'x-zt-signature': normalize(headers['x-zt-signature']),
|
|
109
|
+
};
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const sendFailure = (
|
|
113
|
+
res: IResponse,
|
|
114
|
+
status: number,
|
|
115
|
+
code: string,
|
|
116
|
+
message: string,
|
|
117
|
+
details?: unknown
|
|
118
|
+
): void => {
|
|
119
|
+
const payload: TraceGatewayFailure = {
|
|
120
|
+
ok: false,
|
|
121
|
+
error: { code, message, details },
|
|
122
|
+
};
|
|
123
|
+
res.status(status).json(payload);
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const sendSuccess = (res: IResponse): void => {
|
|
127
|
+
const payload: TraceGatewaySuccess = { ok: true };
|
|
128
|
+
res.status(200).json(payload);
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const verifyRequest = async (
|
|
132
|
+
req: IRequest,
|
|
133
|
+
bodyText: string,
|
|
134
|
+
settings: TraceIngestGatewaySettings,
|
|
135
|
+
path: string
|
|
136
|
+
): Promise<{ ok: true } | { ok: false; code: string; status: number; message: string }> => {
|
|
137
|
+
if (settings.keyId.trim() === '' || settings.secret.trim() === '') {
|
|
138
|
+
return {
|
|
139
|
+
ok: false,
|
|
140
|
+
code: 'CONFIG_ERROR',
|
|
141
|
+
status: 500,
|
|
142
|
+
message: 'Trace ingest signing credentials are not configured',
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const verifyResult = await SignedRequest.verify({
|
|
147
|
+
method: req.getMethod(),
|
|
148
|
+
url: new URL(path, 'http://localhost'),
|
|
149
|
+
body: bodyText,
|
|
150
|
+
headers: toIncomingHeaders(req),
|
|
151
|
+
nowMs: nowMs(),
|
|
152
|
+
windowMs: settings.signingWindowMs,
|
|
153
|
+
verifyNonce: async (keyId: string, nonce: string) =>
|
|
154
|
+
storeNonce(keyId, nonce, settings.nonceTtlMs),
|
|
155
|
+
getSecretForKeyId: async (keyId: string) => {
|
|
156
|
+
if (keyId === settings.keyId) return settings.secret;
|
|
157
|
+
return undefined;
|
|
158
|
+
},
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
if (verifyResult.ok === true) return { ok: true };
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
ok: false,
|
|
165
|
+
code: verifyResult.code,
|
|
166
|
+
status: verifyResult.code === 'EXPIRED' || verifyResult.code === 'REPLAYED' ? 401 : 403,
|
|
167
|
+
message: verifyResult.message,
|
|
168
|
+
};
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
const createWriteHandler = (settings: TraceIngestGatewaySettings, path: string) => {
|
|
172
|
+
return async (req: IRequest, res: IResponse): Promise<void> => {
|
|
173
|
+
const body = getBodyRecord(req);
|
|
174
|
+
const auth = await verifyRequest(req, getRawBody(req), settings, path);
|
|
175
|
+
if (auth.ok === false) {
|
|
176
|
+
sendFailure(res, auth.status, auth.code, auth.message);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const entry = body['entry'];
|
|
181
|
+
if (typeof entry !== 'object' || entry === null || Array.isArray(entry)) {
|
|
182
|
+
sendFailure(res, 400, 'VALIDATION_ERROR', 'entry must be an object');
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
await settings.storage.writeEntry(entry as ITraceEntry);
|
|
187
|
+
sendSuccess(res);
|
|
188
|
+
};
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
const createUpdateHandler = (settings: TraceIngestGatewaySettings, path: string) => {
|
|
192
|
+
return async (req: IRequest, res: IResponse): Promise<void> => {
|
|
193
|
+
const body = getBodyRecord(req);
|
|
194
|
+
const auth = await verifyRequest(req, getRawBody(req), settings, path);
|
|
195
|
+
if (auth.ok === false) {
|
|
196
|
+
sendFailure(res, auth.status, auth.code, auth.message);
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const uuid = body['uuid'];
|
|
201
|
+
const patch = body['patch'];
|
|
202
|
+
if (typeof uuid !== 'string' || uuid.trim() === '') {
|
|
203
|
+
sendFailure(res, 400, 'VALIDATION_ERROR', 'uuid is required');
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (typeof patch !== 'object' || patch === null || Array.isArray(patch)) {
|
|
208
|
+
sendFailure(res, 400, 'VALIDATION_ERROR', 'patch must be an object');
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
await settings.storage.updateEntry(
|
|
213
|
+
uuid,
|
|
214
|
+
patch as Partial<Pick<ITraceEntry, 'content' | 'isLatest'>>
|
|
215
|
+
);
|
|
216
|
+
sendSuccess(res);
|
|
217
|
+
};
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
const createMarkFamilyStaleHandler = (settings: TraceIngestGatewaySettings, path: string) => {
|
|
221
|
+
return async (req: IRequest, res: IResponse): Promise<void> => {
|
|
222
|
+
const body = getBodyRecord(req);
|
|
223
|
+
const auth = await verifyRequest(req, getRawBody(req), settings, path);
|
|
224
|
+
if (auth.ok === false) {
|
|
225
|
+
sendFailure(res, auth.status, auth.code, auth.message);
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const familyHash = body['familyHash'];
|
|
230
|
+
const exceptUuid = body['exceptUuid'];
|
|
231
|
+
|
|
232
|
+
if (typeof familyHash !== 'string' || familyHash.trim() === '') {
|
|
233
|
+
sendFailure(res, 400, 'VALIDATION_ERROR', 'familyHash is required');
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (typeof exceptUuid !== 'string' || exceptUuid.trim() === '') {
|
|
238
|
+
sendFailure(res, 400, 'VALIDATION_ERROR', 'exceptUuid is required');
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
await settings.storage.markFamilyStale(familyHash, exceptUuid);
|
|
243
|
+
sendSuccess(res);
|
|
244
|
+
};
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
const resolveStorage = (overrides?: TraceIngestGatewayOverrides): ITraceStorage => {
|
|
248
|
+
if (overrides?.storage !== undefined) return overrides.storage;
|
|
249
|
+
|
|
250
|
+
const connectionName = overrides?.connectionName ?? TraceConfig.merge().connection;
|
|
251
|
+
const db = useDatabase(undefined, connectionName);
|
|
252
|
+
if (db === undefined) {
|
|
253
|
+
throw ErrorFactory.createConfigError('Trace ingest connection is not configured.', {
|
|
254
|
+
connectionName,
|
|
255
|
+
envKey: 'TRACE_DB_CONNECTION',
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return TraceStorage.resolveStorage(db);
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
const readSettings = (overrides?: TraceIngestGatewayOverrides): TraceIngestGatewaySettings => {
|
|
263
|
+
const configuredSecret = (overrides?.secret ?? Env.get('TRACE_PROXY_SECRET', '')).trim();
|
|
264
|
+
const configuredKeyId = (overrides?.keyId ?? Env.get('TRACE_PROXY_KEY_ID', '')).trim();
|
|
265
|
+
|
|
266
|
+
return {
|
|
267
|
+
basePath: normalizePath(overrides?.basePath ?? Env.get('TRACE_PROXY_PATH', '/zin/trace/write')),
|
|
268
|
+
keyId: configuredKeyId === '' ? (Env.APP_NAME || 'zintrust').trim() : configuredKeyId,
|
|
269
|
+
secret: configuredSecret === '' ? Env.APP_KEY : configuredSecret,
|
|
270
|
+
signingWindowMs:
|
|
271
|
+
overrides?.signingWindowMs ?? Env.getInt('TRACE_PROXY_SIGNING_WINDOW_MS', 60000),
|
|
272
|
+
nonceTtlMs: overrides?.nonceTtlMs ?? Env.getInt('TRACE_PROXY_NONCE_TTL_MS', 120000),
|
|
273
|
+
middleware: overrides?.middleware ?? parseMiddleware(Env.get('TRACE_PROXY_MIDDLEWARE', '')),
|
|
274
|
+
storage: resolveStorage(overrides),
|
|
275
|
+
};
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
export const TraceIngestGateway = Object.freeze({
|
|
279
|
+
create(overrides?: TraceIngestGatewayOverrides): {
|
|
280
|
+
registerRoutes: (router: IRouter) => void;
|
|
281
|
+
} {
|
|
282
|
+
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
|
+
|
|
290
|
+
return {
|
|
291
|
+
registerRoutes(router: IRouter): void {
|
|
292
|
+
Router.post(
|
|
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
|
+
);
|
|
305
|
+
},
|
|
306
|
+
};
|
|
307
|
+
},
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
export const registerTraceIngestGateway = (
|
|
311
|
+
router: IRouter,
|
|
312
|
+
overrides?: TraceIngestGatewayOverrides
|
|
313
|
+
): void => {
|
|
314
|
+
TraceIngestGateway.create(overrides).registerRoutes(router);
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
export default TraceIngestGateway;
|
package/src/register.ts
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
*/
|
|
22
22
|
import { TraceConfig } from './config';
|
|
23
23
|
import { TraceContext } from './context';
|
|
24
|
-
import { TraceStorage } from './storage';
|
|
24
|
+
import { ProxyTraceStorage, TraceServiceTag, TraceStorage } from './storage';
|
|
25
25
|
import { TraceContentBudget } from './storage/TraceContentBudget';
|
|
26
26
|
import { TraceContentRedaction } from './storage/TraceContentRedaction';
|
|
27
27
|
import { TraceEntryFiltering } from './storage/TraceEntryFiltering';
|
|
@@ -212,6 +212,15 @@ if (!traceAlreadyInitialized && Env) {
|
|
|
212
212
|
const pruneAfterHoursRaw = Env.get('TRACE_PRUNE_HOURS', '').trim();
|
|
213
213
|
const slowQueryThresholdRaw = Env.get('TRACE_SLOW_QUERY_MS', '').trim();
|
|
214
214
|
const logMinLevelRaw = Env.get('TRACE_LOG_LEVEL', '').trim();
|
|
215
|
+
const traceProxyRaw = Env.get('TRACE_PROXY', '').trim();
|
|
216
|
+
const traceProxyUrlRaw = Env.get('TRACE_PROXY_URL', '').trim();
|
|
217
|
+
const traceProxyPathRaw = Env.get('TRACE_PROXY_PATH', '').trim();
|
|
218
|
+
const traceProxyKeyIdRaw = Env.get('TRACE_PROXY_KEY_ID', '').trim();
|
|
219
|
+
const traceProxySecretRaw = Env.get('TRACE_PROXY_SECRET', '').trim();
|
|
220
|
+
const traceProxyTimeoutRaw = Env.get('TRACE_PROXY_TIMEOUT_MS', '').trim();
|
|
221
|
+
const traceServiceTagRaw = Env.get('TRACE_SERVICE_TAG', '').trim();
|
|
222
|
+
const appNameRaw = Env.get('APP_NAME', '').trim();
|
|
223
|
+
const appKeyRaw = Env.get('APP_KEY', '').trim();
|
|
215
224
|
const captureCachePayloadsRaw = Env.get('TRACE_CACHE_PAYLOADS', '').trim();
|
|
216
225
|
const captureQueryBindingsRaw = Env.get('TRACE_QUERY_BINDINGS', '').trim();
|
|
217
226
|
const contentDispatchDriverRaw = Env.get('TRACE_CONTENT_QUEUE_DRIVER', '').trim();
|
|
@@ -262,6 +271,26 @@ if (!traceAlreadyInitialized && Env) {
|
|
|
262
271
|
parseEnvBool(captureCachePayloadsRaw) ?? startupOverrides?.captureCachePayloads;
|
|
263
272
|
const captureQueryBindings =
|
|
264
273
|
parseEnvBool(captureQueryBindingsRaw) ?? startupOverrides?.captureQueryBindings;
|
|
274
|
+
const traceProxyEnabled = parseEnvBool(traceProxyRaw) ?? startupOverrides?.proxy?.enabled;
|
|
275
|
+
const traceProxyUrl = traceProxyUrlRaw === '' ? startupOverrides?.proxy?.url : traceProxyUrlRaw;
|
|
276
|
+
const traceProxyPath =
|
|
277
|
+
traceProxyPathRaw === '' ? startupOverrides?.proxy?.path : traceProxyPathRaw;
|
|
278
|
+
const traceProxyKeyId =
|
|
279
|
+
traceProxyKeyIdRaw === ''
|
|
280
|
+
? (startupOverrides?.proxy?.keyId ?? appNameRaw)
|
|
281
|
+
: traceProxyKeyIdRaw;
|
|
282
|
+
const traceProxySecret =
|
|
283
|
+
traceProxySecretRaw === ''
|
|
284
|
+
? (startupOverrides?.proxy?.secret ?? appKeyRaw)
|
|
285
|
+
: traceProxySecretRaw;
|
|
286
|
+
const traceProxyTimeout =
|
|
287
|
+
traceProxyTimeoutRaw === ''
|
|
288
|
+
? startupOverrides?.proxy?.timeoutMs
|
|
289
|
+
: Number.parseInt(traceProxyTimeoutRaw, 10);
|
|
290
|
+
const traceServiceTag =
|
|
291
|
+
traceServiceTagRaw === ''
|
|
292
|
+
? (startupOverrides?.serviceTag ?? appNameRaw).trim() || undefined
|
|
293
|
+
: traceServiceTagRaw;
|
|
265
294
|
const contentDispatchDriver =
|
|
266
295
|
contentDispatchDriverRaw === ''
|
|
267
296
|
? startupOverrides?.contentDispatch?.driver
|
|
@@ -305,6 +334,29 @@ if (!traceAlreadyInitialized && Env) {
|
|
|
305
334
|
enabled,
|
|
306
335
|
connection,
|
|
307
336
|
observeConnection,
|
|
337
|
+
...(typeof traceServiceTag === 'string' && traceServiceTag !== ''
|
|
338
|
+
? { serviceTag: traceServiceTag }
|
|
339
|
+
: {}),
|
|
340
|
+
proxy: {
|
|
341
|
+
...TraceConfig.defaults().proxy,
|
|
342
|
+
...startupOverrides?.proxy,
|
|
343
|
+
...(typeof traceProxyEnabled === 'boolean' ? { enabled: traceProxyEnabled } : {}),
|
|
344
|
+
...(typeof traceProxyUrl === 'string' && traceProxyUrl !== ''
|
|
345
|
+
? { url: traceProxyUrl }
|
|
346
|
+
: {}),
|
|
347
|
+
...(typeof traceProxyPath === 'string' && traceProxyPath !== ''
|
|
348
|
+
? { path: traceProxyPath }
|
|
349
|
+
: {}),
|
|
350
|
+
...(typeof traceProxyKeyId === 'string' && traceProxyKeyId !== ''
|
|
351
|
+
? { keyId: traceProxyKeyId }
|
|
352
|
+
: {}),
|
|
353
|
+
...(typeof traceProxySecret === 'string' && traceProxySecret !== ''
|
|
354
|
+
? { secret: traceProxySecret }
|
|
355
|
+
: {}),
|
|
356
|
+
...(typeof traceProxyTimeout === 'number' && Number.isFinite(traceProxyTimeout)
|
|
357
|
+
? { timeoutMs: traceProxyTimeout }
|
|
358
|
+
: {}),
|
|
359
|
+
},
|
|
308
360
|
...(typeof pruneAfterHours === 'number' && Number.isFinite(pruneAfterHours)
|
|
309
361
|
? { pruneAfterHours }
|
|
310
362
|
: {}),
|
|
@@ -379,10 +431,23 @@ if (!traceAlreadyInitialized && Env) {
|
|
|
379
431
|
});
|
|
380
432
|
await assertTraceStorageReady(core, storageDb, resolvedConnectionName);
|
|
381
433
|
|
|
434
|
+
const resolvedStorage = config.proxy.enabled
|
|
435
|
+
? ProxyTraceStorage.create({
|
|
436
|
+
baseUrl: config.proxy.url ?? '',
|
|
437
|
+
path: config.proxy.path,
|
|
438
|
+
keyId: config.proxy.keyId ?? '',
|
|
439
|
+
secret: config.proxy.secret ?? '',
|
|
440
|
+
timeoutMs: config.proxy.timeoutMs,
|
|
441
|
+
})
|
|
442
|
+
: TraceStorage.resolveStorage(storageDb);
|
|
443
|
+
|
|
382
444
|
const storage = TraceWriteDiagnostics.wrapStorage(
|
|
383
445
|
TraceContentBudget.wrapStorage(
|
|
384
446
|
TraceContentRedaction.wrapStorage(
|
|
385
|
-
TraceEntryFiltering.wrapStorage(
|
|
447
|
+
TraceEntryFiltering.wrapStorage(
|
|
448
|
+
TraceServiceTag.wrapStorage(resolvedStorage, config),
|
|
449
|
+
config
|
|
450
|
+
),
|
|
386
451
|
config.redaction
|
|
387
452
|
),
|
|
388
453
|
config
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { ErrorFactory, RemoteSignedJson } from '@zintrust/core';
|
|
2
|
+
import type { ITraceEntry, ITraceStorage } from '../types';
|
|
3
|
+
|
|
4
|
+
type ProxyTraceStorageSettings = {
|
|
5
|
+
baseUrl: string;
|
|
6
|
+
path: string;
|
|
7
|
+
keyId: string;
|
|
8
|
+
secret: string;
|
|
9
|
+
timeoutMs: number;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
type TraceProxyWriteRequest = {
|
|
13
|
+
entry: ITraceEntry;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
type TraceProxyUpdateRequest = {
|
|
17
|
+
uuid: string;
|
|
18
|
+
patch: Partial<Pick<ITraceEntry, 'content' | 'isLatest'>>;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
type TraceProxyMarkFamilyStaleRequest = {
|
|
22
|
+
familyHash: string;
|
|
23
|
+
exceptUuid: string;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const ensureConfigured = (settings: ProxyTraceStorageSettings): void => {
|
|
27
|
+
if (settings.baseUrl.trim() === '') {
|
|
28
|
+
throw ErrorFactory.createConfigError('TRACE_PROXY_URL is required when TRACE_PROXY=true');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (settings.keyId.trim() === '' || settings.secret.trim() === '') {
|
|
32
|
+
throw ErrorFactory.createConfigError(
|
|
33
|
+
'TRACE_PROXY signing credentials are required when TRACE_PROXY=true'
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const normalizePath = (value: string): string => {
|
|
39
|
+
const trimmed = value.trim();
|
|
40
|
+
if (trimmed === '') return '/zin/trace/write';
|
|
41
|
+
return trimmed.startsWith('/') ? trimmed : `/${trimmed}`;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const createUnsupportedReadError = (): Error =>
|
|
45
|
+
ErrorFactory.createConfigError(
|
|
46
|
+
'Trace proxy sender storage does not expose dashboard/query operations. Use the trace server for reads.'
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
type ProxyRequestSettings = {
|
|
50
|
+
baseUrl: string;
|
|
51
|
+
keyId: string;
|
|
52
|
+
secret: string;
|
|
53
|
+
timeoutMs: number;
|
|
54
|
+
signaturePathPrefixToStrip: string;
|
|
55
|
+
missingUrlMessage: string;
|
|
56
|
+
missingCredentialsMessage: string;
|
|
57
|
+
messages: {
|
|
58
|
+
unauthorized: string;
|
|
59
|
+
forbidden: string;
|
|
60
|
+
rateLimited: string;
|
|
61
|
+
rejected: string;
|
|
62
|
+
error: string;
|
|
63
|
+
timedOut: string;
|
|
64
|
+
};
|
|
65
|
+
normalizedPath: string;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const buildSettings = (settings: ProxyTraceStorageSettings): ProxyRequestSettings => {
|
|
69
|
+
ensureConfigured(settings);
|
|
70
|
+
const normalizedPath = normalizePath(settings.path);
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
baseUrl: settings.baseUrl,
|
|
74
|
+
keyId: settings.keyId,
|
|
75
|
+
secret: settings.secret,
|
|
76
|
+
timeoutMs: settings.timeoutMs,
|
|
77
|
+
signaturePathPrefixToStrip: new URL(settings.baseUrl).pathname,
|
|
78
|
+
missingUrlMessage: 'TRACE_PROXY_URL is required when TRACE_PROXY=true',
|
|
79
|
+
missingCredentialsMessage: 'TRACE_PROXY signing credentials are required when TRACE_PROXY=true',
|
|
80
|
+
messages: {
|
|
81
|
+
unauthorized: 'Trace proxy rejected the request credentials',
|
|
82
|
+
forbidden: 'Trace proxy rejected the request signature',
|
|
83
|
+
rateLimited: 'Trace proxy rate-limited the request',
|
|
84
|
+
rejected: 'Trace proxy rejected the request payload',
|
|
85
|
+
error: 'Trace proxy request failed',
|
|
86
|
+
timedOut: 'Trace proxy request timed out',
|
|
87
|
+
},
|
|
88
|
+
normalizedPath,
|
|
89
|
+
};
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const appendSuffix = (path: string, suffix: string): string => {
|
|
93
|
+
const base = normalizePath(path).replace(/\/+$/, '');
|
|
94
|
+
const tail = suffix.startsWith('/') ? suffix : `/${suffix}`;
|
|
95
|
+
return `${base}${tail}`;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const unsupportedQueryEntries: ITraceStorage['queryEntries'] = async () => {
|
|
99
|
+
throw createUnsupportedReadError();
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const unsupportedGetEntry: ITraceStorage['getEntry'] = async () => {
|
|
103
|
+
throw createUnsupportedReadError();
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const unsupportedGetBatch: ITraceStorage['getBatch'] = async () => {
|
|
107
|
+
throw createUnsupportedReadError();
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const unsupportedQueryBatchEntries: ITraceStorage['queryBatchEntries'] = async () => {
|
|
111
|
+
throw createUnsupportedReadError();
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const unsupportedPrune: ITraceStorage['prune'] = async () => {
|
|
115
|
+
throw createUnsupportedReadError();
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const unsupportedClear: ITraceStorage['clear'] = async () => {
|
|
119
|
+
throw createUnsupportedReadError();
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const unsupportedGetMonitoring: ITraceStorage['getMonitoring'] = async () => {
|
|
123
|
+
throw createUnsupportedReadError();
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const unsupportedAddMonitoring: ITraceStorage['addMonitoring'] = async () => {
|
|
127
|
+
throw createUnsupportedReadError();
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const unsupportedRemoveMonitoring: ITraceStorage['removeMonitoring'] = async () => {
|
|
131
|
+
throw createUnsupportedReadError();
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const unsupportedStats: ITraceStorage['stats'] = async () => {
|
|
135
|
+
throw createUnsupportedReadError();
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
export const ProxyTraceStorage = Object.freeze({
|
|
139
|
+
create(settings: ProxyTraceStorageSettings): ITraceStorage {
|
|
140
|
+
const normalized = buildSettings(settings);
|
|
141
|
+
|
|
142
|
+
return Object.freeze({
|
|
143
|
+
async writeEntry(entry: ITraceEntry): Promise<void> {
|
|
144
|
+
await RemoteSignedJson.request<{ ok: true }>(normalized, normalized.normalizedPath, {
|
|
145
|
+
entry,
|
|
146
|
+
} satisfies TraceProxyWriteRequest);
|
|
147
|
+
},
|
|
148
|
+
|
|
149
|
+
async updateEntry(
|
|
150
|
+
uuid: string,
|
|
151
|
+
patch: Partial<Pick<ITraceEntry, 'content' | 'isLatest'>>
|
|
152
|
+
): Promise<void> {
|
|
153
|
+
await RemoteSignedJson.request<{ ok: true }>(
|
|
154
|
+
normalized,
|
|
155
|
+
appendSuffix(normalized.normalizedPath, '/update'),
|
|
156
|
+
{ uuid, patch } satisfies TraceProxyUpdateRequest
|
|
157
|
+
);
|
|
158
|
+
},
|
|
159
|
+
|
|
160
|
+
async markFamilyStale(familyHash: string, exceptUuid: string): Promise<void> {
|
|
161
|
+
await RemoteSignedJson.request<{ ok: true }>(
|
|
162
|
+
normalized,
|
|
163
|
+
appendSuffix(normalized.normalizedPath, '/mark-family-stale'),
|
|
164
|
+
{ familyHash, exceptUuid } satisfies TraceProxyMarkFamilyStaleRequest
|
|
165
|
+
);
|
|
166
|
+
},
|
|
167
|
+
|
|
168
|
+
queryEntries: unsupportedQueryEntries,
|
|
169
|
+
getEntry: unsupportedGetEntry,
|
|
170
|
+
getBatch: unsupportedGetBatch,
|
|
171
|
+
queryBatchEntries: unsupportedQueryBatchEntries,
|
|
172
|
+
prune: unsupportedPrune,
|
|
173
|
+
clear: unsupportedClear,
|
|
174
|
+
getMonitoring: unsupportedGetMonitoring,
|
|
175
|
+
addMonitoring: unsupportedAddMonitoring,
|
|
176
|
+
removeMonitoring: unsupportedRemoveMonitoring,
|
|
177
|
+
stats: unsupportedStats,
|
|
178
|
+
});
|
|
179
|
+
},
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
export default ProxyTraceStorage;
|
|
@@ -285,6 +285,7 @@ const getCoreRuntime = async (): Promise<{
|
|
|
285
285
|
|
|
286
286
|
const getQueueWorkerApi = async (): Promise<QueueWorkerApi | null> => {
|
|
287
287
|
try {
|
|
288
|
+
// @ts-ignore
|
|
288
289
|
const mod = (await import('@zintrust/workers')) as unknown as QueueWorkerApi;
|
|
289
290
|
return typeof mod.createQueueWorker === 'function' ? mod : null;
|
|
290
291
|
} catch {
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { ErrorFactory } from '@zintrust/core';
|
|
2
|
+
import type { ITraceConfig, ITraceEntry, ITraceStorage } from '../types';
|
|
3
|
+
|
|
4
|
+
const appendServiceTag = (entry: ITraceEntry, serviceTag?: string): ITraceEntry => {
|
|
5
|
+
const normalizedTag = serviceTag?.trim() ?? '';
|
|
6
|
+
if (normalizedTag === '' || entry.tags.includes(normalizedTag)) {
|
|
7
|
+
return entry;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
return {
|
|
11
|
+
...entry,
|
|
12
|
+
tags: [...entry.tags, normalizedTag],
|
|
13
|
+
};
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const unsupportedRead = async <T>(): Promise<T> => {
|
|
17
|
+
throw ErrorFactory.createConfigError(
|
|
18
|
+
'Trace proxy mode only supports runtime persistence on the sender. Query the trace server database or dashboard directly.'
|
|
19
|
+
);
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const bindOrUnsupported = <T extends (...args: never[]) => Promise<unknown>>(
|
|
23
|
+
method: T | undefined
|
|
24
|
+
): T => {
|
|
25
|
+
if (method === undefined) {
|
|
26
|
+
return unsupportedRead as unknown as T;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return method;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export const TraceServiceTag = Object.freeze({
|
|
33
|
+
wrapStorage(storage: ITraceStorage, config: ITraceConfig): ITraceStorage {
|
|
34
|
+
const writeEntry = async (entry: ITraceEntry): Promise<void> => {
|
|
35
|
+
await storage.writeEntry(appendServiceTag(entry, config.serviceTag));
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
return Object.freeze({
|
|
39
|
+
writeEntry,
|
|
40
|
+
updateEntry: storage.updateEntry.bind(storage),
|
|
41
|
+
markFamilyStale: storage.markFamilyStale.bind(storage),
|
|
42
|
+
queryEntries: bindOrUnsupported(storage.queryEntries?.bind(storage)),
|
|
43
|
+
getEntry: bindOrUnsupported(storage.getEntry?.bind(storage)),
|
|
44
|
+
getBatch: bindOrUnsupported(storage.getBatch?.bind(storage)),
|
|
45
|
+
queryBatchEntries: bindOrUnsupported(storage.queryBatchEntries?.bind(storage)),
|
|
46
|
+
prune: bindOrUnsupported(storage.prune?.bind(storage)),
|
|
47
|
+
clear: bindOrUnsupported(storage.clear?.bind(storage)),
|
|
48
|
+
getMonitoring: bindOrUnsupported(storage.getMonitoring?.bind(storage)),
|
|
49
|
+
addMonitoring: bindOrUnsupported(storage.addMonitoring?.bind(storage)),
|
|
50
|
+
removeMonitoring: bindOrUnsupported(storage.removeMonitoring?.bind(storage)),
|
|
51
|
+
stats: bindOrUnsupported(storage.stats?.bind(storage)),
|
|
52
|
+
});
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
export default TraceServiceTag;
|
package/src/storage/index.ts
CHANGED
package/src/types.ts
CHANGED
|
@@ -373,6 +373,15 @@ export type TraceContentDispatchConfig = {
|
|
|
373
373
|
worker: TraceContentDispatchWorkerConfig;
|
|
374
374
|
};
|
|
375
375
|
|
|
376
|
+
export type TraceProxyConfig = {
|
|
377
|
+
enabled: boolean;
|
|
378
|
+
url?: string;
|
|
379
|
+
path: string;
|
|
380
|
+
keyId?: string;
|
|
381
|
+
secret?: string;
|
|
382
|
+
timeoutMs: number;
|
|
383
|
+
};
|
|
384
|
+
|
|
376
385
|
export type TraceWatcherToggle = boolean | TraceFilterRule;
|
|
377
386
|
export type TraceRequestWatcherToggle = boolean | TraceRequestWatcherConfig;
|
|
378
387
|
export type TraceClientRequestWatcherToggle = boolean | TraceClientRequestWatcherConfig;
|
|
@@ -404,6 +413,8 @@ export interface ITraceConfig {
|
|
|
404
413
|
enabled: boolean;
|
|
405
414
|
connection?: string;
|
|
406
415
|
observeConnection?: string;
|
|
416
|
+
serviceTag?: string;
|
|
417
|
+
proxy: TraceProxyConfig;
|
|
407
418
|
pruneAfterHours: number;
|
|
408
419
|
ignoreRoutes: string[];
|
|
409
420
|
ignorePaths: string[];
|