@zintrust/trace 0.9.2 → 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/TraceConnection.d.ts +25 -0
- package/dist/TraceConnection.js +98 -0
- package/dist/build-manifest.json +56 -24
- package/dist/config.js +18 -0
- package/dist/dashboard/routes.js +12 -16
- package/dist/dashboard/ui.js +32 -4
- 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 +59 -69
- 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 +3 -3
- package/src/TraceConnection.ts +182 -0
- package/src/config.ts +23 -0
- package/src/dashboard/routes.ts +24 -26
- package/src/dashboard/ui.ts +32 -4
- package/src/index.ts +1 -0
- package/src/ingest/TraceIngestGateway.ts +317 -0
- package/src/register.ts +73 -114
- 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,11 +21,17 @@
|
|
|
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';
|
|
28
28
|
import { TraceWriteDiagnostics } from './storage/TraceWriteDiagnostics';
|
|
29
|
+
import {
|
|
30
|
+
assertTraceConnectionResolved,
|
|
31
|
+
assertTraceStorageReady,
|
|
32
|
+
resolveObservedConnectionName,
|
|
33
|
+
resolveTraceConnectionName,
|
|
34
|
+
} from './TraceConnection';
|
|
29
35
|
import type { ITraceWatcherConfig, TraceConfigOverrides } from './types';
|
|
30
36
|
|
|
31
37
|
export type {}; // side-effect ESM module
|
|
@@ -38,7 +44,6 @@ type GlobalTraceRegisterState = {
|
|
|
38
44
|
};
|
|
39
45
|
|
|
40
46
|
const globalTraceRegisterState = globalThis as unknown as GlobalTraceRegisterState;
|
|
41
|
-
globalTraceRegisterState.__zintrust_system_trace_plugin_requested__ = true;
|
|
42
47
|
const traceAlreadyInitialized =
|
|
43
48
|
globalTraceRegisterState.__zintrust_system_trace_register_initialized__ === true;
|
|
44
49
|
|
|
@@ -80,14 +85,6 @@ type CoreApi = {
|
|
|
80
85
|
};
|
|
81
86
|
};
|
|
82
87
|
|
|
83
|
-
type CoreDatabase = import('@zintrust/core').IDatabase;
|
|
84
|
-
|
|
85
|
-
const TRACE_REQUIRED_TABLES = [
|
|
86
|
-
'zin_trace_entries',
|
|
87
|
-
'zin_trace_entries_tags',
|
|
88
|
-
'zin_trace_monitoring',
|
|
89
|
-
] as const;
|
|
90
|
-
|
|
91
88
|
type GlobalMiddlewareRegistrarState = {
|
|
92
89
|
__zintrust_register_global_middleware__?: ITraceWatcherConfig['registerMiddleware'];
|
|
93
90
|
__zintrust_pending_global_middlewares__?: Array<
|
|
@@ -111,44 +108,6 @@ const resolveRegisterMiddleware = (): NonNullable<ITraceWatcherConfig['registerM
|
|
|
111
108
|
};
|
|
112
109
|
};
|
|
113
110
|
|
|
114
|
-
const resolveTraceConnectionName = (
|
|
115
|
-
env: Pick<NonNullable<CoreApi['Env']>, 'get'> | undefined,
|
|
116
|
-
configuredConnection?: string
|
|
117
|
-
): string => {
|
|
118
|
-
const resolveDefaultConnection = (): string => {
|
|
119
|
-
const defaultConnection = env?.get('DB_CONNECTION', '').trim() ?? '';
|
|
120
|
-
if (defaultConnection === '' || defaultConnection === 'default') return 'default';
|
|
121
|
-
return defaultConnection;
|
|
122
|
-
};
|
|
123
|
-
|
|
124
|
-
const explicitConnection = configuredConnection?.trim();
|
|
125
|
-
if (explicitConnection !== undefined && explicitConnection !== '') {
|
|
126
|
-
return explicitConnection === 'default' ? resolveDefaultConnection() : explicitConnection;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
return resolveDefaultConnection();
|
|
130
|
-
};
|
|
131
|
-
|
|
132
|
-
const resolveObservedConnectionName = (
|
|
133
|
-
env: Pick<NonNullable<CoreApi['Env']>, 'get'> | undefined,
|
|
134
|
-
configuredObservedConnection: string | undefined,
|
|
135
|
-
storageConnectionName: string
|
|
136
|
-
): string => {
|
|
137
|
-
if (
|
|
138
|
-
typeof configuredObservedConnection === 'string' &&
|
|
139
|
-
configuredObservedConnection.trim() !== ''
|
|
140
|
-
) {
|
|
141
|
-
return resolveTraceConnectionName(env, configuredObservedConnection);
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
const defaultConnectionName = resolveTraceConnectionName(env);
|
|
145
|
-
if (storageConnectionName !== defaultConnectionName) {
|
|
146
|
-
return defaultConnectionName;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
return storageConnectionName;
|
|
150
|
-
};
|
|
151
|
-
|
|
152
111
|
const isObjectValue = (value: unknown): value is Record<string, unknown> => {
|
|
153
112
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
154
113
|
};
|
|
@@ -240,71 +199,6 @@ const buildTraceRedactionOverrides = (input: {
|
|
|
240
199
|
: undefined;
|
|
241
200
|
};
|
|
242
201
|
|
|
243
|
-
const createTraceConfigError = (coreApi: CoreApi, message: string, details?: unknown): Error => {
|
|
244
|
-
if (coreApi.ErrorFactory?.createConfigError !== undefined) {
|
|
245
|
-
return coreApi.ErrorFactory.createConfigError(message, details);
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
const error = new globalThis.Error(message) as Error & {
|
|
249
|
-
code?: string;
|
|
250
|
-
details?: unknown;
|
|
251
|
-
name?: string;
|
|
252
|
-
statusCode?: number;
|
|
253
|
-
};
|
|
254
|
-
error.name = 'ConfigError';
|
|
255
|
-
error.code = 'CONFIG_ERROR';
|
|
256
|
-
error.statusCode = 500;
|
|
257
|
-
error.details = details;
|
|
258
|
-
return error;
|
|
259
|
-
};
|
|
260
|
-
|
|
261
|
-
function assertTraceConnectionResolved(
|
|
262
|
-
coreApi: CoreApi,
|
|
263
|
-
db: CoreDatabase | undefined,
|
|
264
|
-
params: { connectionName: string; envKey: 'TRACE_DB_CONNECTION' | 'TRACE_QUERY_CONNECTION' }
|
|
265
|
-
): asserts db is CoreDatabase {
|
|
266
|
-
if (db !== undefined) {
|
|
267
|
-
return;
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
throw createTraceConfigError(
|
|
271
|
-
coreApi,
|
|
272
|
-
`Trace connection "${params.connectionName}" could not be resolved.`,
|
|
273
|
-
{
|
|
274
|
-
connectionName: params.connectionName,
|
|
275
|
-
envKey: params.envKey,
|
|
276
|
-
hint:
|
|
277
|
-
params.envKey === 'TRACE_DB_CONNECTION'
|
|
278
|
-
? 'Configure TRACE_DB_CONNECTION to an existing database connection before enabling TRACE_ENABLED.'
|
|
279
|
-
: 'Configure TRACE_QUERY_CONNECTION, or ensure DB_CONNECTION resolves to an existing database connection.',
|
|
280
|
-
}
|
|
281
|
-
);
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
const assertTraceStorageReady = async (
|
|
285
|
-
coreApi: CoreApi,
|
|
286
|
-
db: CoreDatabase,
|
|
287
|
-
connectionName: string
|
|
288
|
-
): Promise<void> => {
|
|
289
|
-
try {
|
|
290
|
-
await Promise.all(
|
|
291
|
-
TRACE_REQUIRED_TABLES.map(async (table) => {
|
|
292
|
-
await db.queryOne(`SELECT 1 AS ok FROM ${table} LIMIT 1`, []);
|
|
293
|
-
})
|
|
294
|
-
);
|
|
295
|
-
} catch (error) {
|
|
296
|
-
throw createTraceConfigError(
|
|
297
|
-
coreApi,
|
|
298
|
-
`Trace storage connection "${connectionName}" is not ready. Create the database if needed and run \`zin migrate:trace\` before enabling TRACE_ENABLED.`,
|
|
299
|
-
{
|
|
300
|
-
connectionName,
|
|
301
|
-
error,
|
|
302
|
-
requiredTables: [...TRACE_REQUIRED_TABLES],
|
|
303
|
-
}
|
|
304
|
-
);
|
|
305
|
-
}
|
|
306
|
-
};
|
|
307
|
-
|
|
308
202
|
const core = (await importCore()) as CoreApi;
|
|
309
203
|
const Env = core.Env;
|
|
310
204
|
const startupOverrides = await resolveTraceStartupOverrides(core);
|
|
@@ -318,6 +212,15 @@ if (!traceAlreadyInitialized && Env) {
|
|
|
318
212
|
const pruneAfterHoursRaw = Env.get('TRACE_PRUNE_HOURS', '').trim();
|
|
319
213
|
const slowQueryThresholdRaw = Env.get('TRACE_SLOW_QUERY_MS', '').trim();
|
|
320
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();
|
|
321
224
|
const captureCachePayloadsRaw = Env.get('TRACE_CACHE_PAYLOADS', '').trim();
|
|
322
225
|
const captureQueryBindingsRaw = Env.get('TRACE_QUERY_BINDINGS', '').trim();
|
|
323
226
|
const contentDispatchDriverRaw = Env.get('TRACE_CONTENT_QUEUE_DRIVER', '').trim();
|
|
@@ -368,6 +271,26 @@ if (!traceAlreadyInitialized && Env) {
|
|
|
368
271
|
parseEnvBool(captureCachePayloadsRaw) ?? startupOverrides?.captureCachePayloads;
|
|
369
272
|
const captureQueryBindings =
|
|
370
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;
|
|
371
294
|
const contentDispatchDriver =
|
|
372
295
|
contentDispatchDriverRaw === ''
|
|
373
296
|
? startupOverrides?.contentDispatch?.driver
|
|
@@ -411,6 +334,29 @@ if (!traceAlreadyInitialized && Env) {
|
|
|
411
334
|
enabled,
|
|
412
335
|
connection,
|
|
413
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
|
+
},
|
|
414
360
|
...(typeof pruneAfterHours === 'number' && Number.isFinite(pruneAfterHours)
|
|
415
361
|
? { pruneAfterHours }
|
|
416
362
|
: {}),
|
|
@@ -485,10 +431,23 @@ if (!traceAlreadyInitialized && Env) {
|
|
|
485
431
|
});
|
|
486
432
|
await assertTraceStorageReady(core, storageDb, resolvedConnectionName);
|
|
487
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
|
+
|
|
488
444
|
const storage = TraceWriteDiagnostics.wrapStorage(
|
|
489
445
|
TraceContentBudget.wrapStorage(
|
|
490
446
|
TraceContentRedaction.wrapStorage(
|
|
491
|
-
TraceEntryFiltering.wrapStorage(
|
|
447
|
+
TraceEntryFiltering.wrapStorage(
|
|
448
|
+
TraceServiceTag.wrapStorage(resolvedStorage, config),
|
|
449
|
+
config
|
|
450
|
+
),
|
|
492
451
|
config.redaction
|
|
493
452
|
),
|
|
494
453
|
config
|