@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.
@@ -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(TraceStorage.resolveStorage(storageDb), config),
447
+ TraceEntryFiltering.wrapStorage(
448
+ TraceServiceTag.wrapStorage(resolvedStorage, config),
449
+ config
450
+ ),
492
451
  config.redaction
493
452
  ),
494
453
  config