@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.
@@ -0,0 +1,195 @@
1
+ import { familyHash } from '../utils/familyHash.js';
2
+ const TABLE_ENTRIES = 'zin_trace_entries';
3
+ const TABLE_TAGS = 'zin_trace_entries_tags';
4
+ const TABLE_MONITORING = 'zin_trace_monitoring';
5
+ const generateUuid = () => crypto.randomUUID();
6
+ const rowToEntry = (row, tags) => ({
7
+ uuid: row.uuid,
8
+ batchId: row.batch_id,
9
+ familyHash: row.family_hash ?? undefined,
10
+ type: row.type,
11
+ content: JSON.parse(row.content),
12
+ tags,
13
+ isLatest: Boolean(row.is_latest),
14
+ createdAt: row.created_at,
15
+ });
16
+ const insertTags = async (db, uuid, tags) => {
17
+ if (tags.length === 0)
18
+ return;
19
+ await Promise.all(tags.map(async (tag) => {
20
+ await db.execute(`INSERT OR IGNORE INTO ${TABLE_TAGS} (entry_uuid, tag) VALUES (?, ?)`, [
21
+ uuid,
22
+ tag,
23
+ ]);
24
+ }));
25
+ };
26
+ const buildEntryFilters = (opts) => {
27
+ const conditions = [];
28
+ const params = [];
29
+ if (opts.type) {
30
+ conditions.push('e.type = ?');
31
+ params.push(opts.type);
32
+ }
33
+ if (opts.batchId) {
34
+ conditions.push('e.batch_id = ?');
35
+ params.push(opts.batchId);
36
+ }
37
+ if (opts.from) {
38
+ conditions.push('e.created_at >= ?');
39
+ params.push(opts.from);
40
+ }
41
+ if (opts.to) {
42
+ conditions.push('e.created_at <= ?');
43
+ params.push(opts.to);
44
+ }
45
+ let joinClause = '';
46
+ if (opts.tag) {
47
+ joinClause = `INNER JOIN ${TABLE_TAGS} t ON t.entry_uuid = e.uuid AND t.tag = ?`;
48
+ params.unshift(opts.tag);
49
+ }
50
+ const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
51
+ const countParams = opts.tag ? [opts.tag, ...params.slice(1)] : [...params];
52
+ return { joinClause, whereClause, params, countParams };
53
+ };
54
+ const loadTagsByUuid = async (db, uuids) => {
55
+ const tagsByUuid = new Map();
56
+ if (uuids.length === 0)
57
+ return tagsByUuid;
58
+ const tagRows = (await db.query(`SELECT entry_uuid, tag FROM ${TABLE_TAGS} WHERE entry_uuid IN (${uuids.map(() => '?').join(',')})`, uuids));
59
+ for (const tagRow of tagRows) {
60
+ const tags = tagsByUuid.get(tagRow.entry_uuid) ?? [];
61
+ tags.push(tagRow.tag);
62
+ tagsByUuid.set(tagRow.entry_uuid, tags);
63
+ }
64
+ return tagsByUuid;
65
+ };
66
+ // The storage facade intentionally groups related DB operations in one factory.
67
+ // eslint-disable-next-line max-lines-per-function
68
+ const createStorage = (db) => {
69
+ const writeEntry = async (entry) => {
70
+ const uuid = entry.uuid || generateUuid();
71
+ await db.execute(`INSERT INTO ${TABLE_ENTRIES} (uuid, batch_id, family_hash, type, content, is_latest, created_at)
72
+ VALUES (?, ?, ?, ?, ?, ?, ?)`, [
73
+ uuid,
74
+ entry.batchId,
75
+ entry.familyHash ?? null,
76
+ entry.type,
77
+ JSON.stringify(entry.content),
78
+ entry.isLatest ? 1 : 0,
79
+ entry.createdAt,
80
+ ]);
81
+ await insertTags(db, uuid, entry.tags);
82
+ };
83
+ const updateEntry = async (uuid, patch) => {
84
+ const sets = [];
85
+ const params = [];
86
+ if (patch.content !== undefined) {
87
+ sets.push('content = ?');
88
+ params.push(JSON.stringify(patch.content));
89
+ }
90
+ if (patch.isLatest !== undefined) {
91
+ sets.push('is_latest = ?');
92
+ params.push(patch.isLatest ? 1 : 0);
93
+ }
94
+ if (sets.length === 0)
95
+ return;
96
+ params.push(uuid);
97
+ await db.execute(`UPDATE ${TABLE_ENTRIES} SET ${sets.join(', ')} WHERE uuid = ?`, params);
98
+ };
99
+ const markFamilyStale = async (hash, exceptUuid) => {
100
+ await db.execute(`UPDATE ${TABLE_ENTRIES} SET is_latest = 0
101
+ WHERE family_hash = ? AND uuid != ? AND is_latest = 1`, [hash, exceptUuid]);
102
+ };
103
+ const queryEntries = async (opts) => {
104
+ const page = opts.page ?? 1;
105
+ const perPage = opts.perPage ?? 50;
106
+ const offset = (page - 1) * perPage;
107
+ const { joinClause, whereClause, params, countParams } = buildEntryFilters(opts);
108
+ const countResult = (await db.queryOne(`SELECT COUNT(*) as cnt FROM ${TABLE_ENTRIES} e ${joinClause} ${whereClause}`, countParams));
109
+ const total = countResult?.cnt ?? 0;
110
+ const rows = (await db.query(`SELECT e.id, e.uuid, e.batch_id, e.family_hash, e.type, e.content, e.is_latest, e.created_at
111
+ FROM ${TABLE_ENTRIES} e ${joinClause} ${whereClause}
112
+ ORDER BY e.created_at DESC, e.id DESC
113
+ LIMIT ? OFFSET ?`, [...params, perPage, offset]));
114
+ const tagsByUuid = await loadTagsByUuid(db, rows.map((row) => row.uuid));
115
+ return {
116
+ data: rows.map((row) => rowToEntry(row, tagsByUuid.get(row.uuid) ?? [])),
117
+ total,
118
+ };
119
+ };
120
+ const getEntry = async (uuid) => {
121
+ const row = (await db.queryOne(`SELECT id, uuid, batch_id, family_hash, type, content, is_latest, created_at
122
+ FROM ${TABLE_ENTRIES}
123
+ WHERE uuid = ?`, [uuid]));
124
+ if (!row)
125
+ return null;
126
+ const tags = (await db.query(`SELECT tag FROM ${TABLE_TAGS} WHERE entry_uuid = ?`, [
127
+ uuid,
128
+ ]));
129
+ return rowToEntry(row, tags.map((tag) => tag.tag));
130
+ };
131
+ const getBatch = async (batchId) => {
132
+ const rows = (await db.query(`SELECT id, uuid, batch_id, family_hash, type, content, is_latest, created_at
133
+ FROM ${TABLE_ENTRIES}
134
+ WHERE batch_id = ?
135
+ ORDER BY created_at ASC, id ASC`, [batchId]));
136
+ if (rows.length === 0)
137
+ return [];
138
+ const tagsByUuid = await loadTagsByUuid(db, rows.map((row) => row.uuid));
139
+ return rows.map((row) => rowToEntry(row, tagsByUuid.get(row.uuid) ?? []));
140
+ };
141
+ const prune = async (olderThanMs, keepExceptions = false) => {
142
+ const countResult = (await db.queryOne(`SELECT COUNT(*) as cnt FROM ${TABLE_ENTRIES}
143
+ WHERE created_at < ?
144
+ ${keepExceptions ? "AND type != 'exception'" : ''}`, [olderThanMs]));
145
+ const deleted = countResult?.cnt ?? 0;
146
+ if (deleted === 0)
147
+ return 0;
148
+ await db.execute(`DELETE FROM ${TABLE_ENTRIES}
149
+ WHERE created_at < ?
150
+ ${keepExceptions ? "AND type != 'exception'" : ''}`, [olderThanMs]);
151
+ return deleted;
152
+ };
153
+ const clear = async () => {
154
+ await db.execute(`DELETE FROM ${TABLE_ENTRIES}`, []);
155
+ };
156
+ const getMonitoring = async () => {
157
+ const rows = (await db.query(`SELECT tag FROM ${TABLE_MONITORING}`, []));
158
+ return rows.map((row) => row.tag);
159
+ };
160
+ const addMonitoring = async (tag) => {
161
+ await db.execute(`INSERT OR IGNORE INTO ${TABLE_MONITORING} (tag) VALUES (?)`, [tag]);
162
+ };
163
+ const removeMonitoring = async (tag) => {
164
+ await db.execute(`DELETE FROM ${TABLE_MONITORING} WHERE tag = ?`, [tag]);
165
+ };
166
+ const stats = async () => {
167
+ const rows = (await db.query(`SELECT type, COUNT(*) as cnt FROM ${TABLE_ENTRIES} GROUP BY type`, []));
168
+ const output = {};
169
+ for (const row of rows) {
170
+ output[row.type] = row.cnt;
171
+ }
172
+ return output;
173
+ };
174
+ return {
175
+ writeEntry,
176
+ updateEntry,
177
+ markFamilyStale,
178
+ queryEntries,
179
+ getEntry,
180
+ getBatch,
181
+ prune,
182
+ clear,
183
+ getMonitoring,
184
+ addMonitoring,
185
+ removeMonitoring,
186
+ stats,
187
+ };
188
+ };
189
+ const resolveStorage = (db) => {
190
+ return createStorage(db);
191
+ };
192
+ const reset = () => {
193
+ return;
194
+ };
195
+ export const TraceStorage = Object.freeze({ resolveStorage, reset, familyHash });
@@ -0,0 +1,12 @@
1
+ import type { ITraceStorage } from '../types';
2
+ type ProxyTraceStorageSettings = {
3
+ baseUrl: string;
4
+ path: string;
5
+ keyId: string;
6
+ secret: string;
7
+ timeoutMs: number;
8
+ };
9
+ export declare const ProxyTraceStorage: Readonly<{
10
+ create(settings: ProxyTraceStorageSettings): ITraceStorage;
11
+ }>;
12
+ export default ProxyTraceStorage;
@@ -0,0 +1,102 @@
1
+ import { ErrorFactory, RemoteSignedJson } from '@zintrust/core';
2
+ const ensureConfigured = (settings) => {
3
+ if (settings.baseUrl.trim() === '') {
4
+ throw ErrorFactory.createConfigError('TRACE_PROXY_URL is required when TRACE_PROXY=true');
5
+ }
6
+ if (settings.keyId.trim() === '' || settings.secret.trim() === '') {
7
+ throw ErrorFactory.createConfigError('TRACE_PROXY signing credentials are required when TRACE_PROXY=true');
8
+ }
9
+ };
10
+ const normalizePath = (value) => {
11
+ const trimmed = value.trim();
12
+ if (trimmed === '')
13
+ return '/zin/trace/write';
14
+ return trimmed.startsWith('/') ? trimmed : `/${trimmed}`;
15
+ };
16
+ const createUnsupportedReadError = () => ErrorFactory.createConfigError('Trace proxy sender storage does not expose dashboard/query operations. Use the trace server for reads.');
17
+ const buildSettings = (settings) => {
18
+ ensureConfigured(settings);
19
+ const normalizedPath = normalizePath(settings.path);
20
+ return {
21
+ baseUrl: settings.baseUrl,
22
+ keyId: settings.keyId,
23
+ secret: settings.secret,
24
+ timeoutMs: settings.timeoutMs,
25
+ signaturePathPrefixToStrip: new URL(settings.baseUrl).pathname,
26
+ missingUrlMessage: 'TRACE_PROXY_URL is required when TRACE_PROXY=true',
27
+ missingCredentialsMessage: 'TRACE_PROXY signing credentials are required when TRACE_PROXY=true',
28
+ messages: {
29
+ unauthorized: 'Trace proxy rejected the request credentials',
30
+ forbidden: 'Trace proxy rejected the request signature',
31
+ rateLimited: 'Trace proxy rate-limited the request',
32
+ rejected: 'Trace proxy rejected the request payload',
33
+ error: 'Trace proxy request failed',
34
+ timedOut: 'Trace proxy request timed out',
35
+ },
36
+ normalizedPath,
37
+ };
38
+ };
39
+ const appendSuffix = (path, suffix) => {
40
+ const base = normalizePath(path).replace(/\/+$/, '');
41
+ const tail = suffix.startsWith('/') ? suffix : `/${suffix}`;
42
+ return `${base}${tail}`;
43
+ };
44
+ const unsupportedQueryEntries = async () => {
45
+ throw createUnsupportedReadError();
46
+ };
47
+ const unsupportedGetEntry = async () => {
48
+ throw createUnsupportedReadError();
49
+ };
50
+ const unsupportedGetBatch = async () => {
51
+ throw createUnsupportedReadError();
52
+ };
53
+ const unsupportedQueryBatchEntries = async () => {
54
+ throw createUnsupportedReadError();
55
+ };
56
+ const unsupportedPrune = async () => {
57
+ throw createUnsupportedReadError();
58
+ };
59
+ const unsupportedClear = async () => {
60
+ throw createUnsupportedReadError();
61
+ };
62
+ const unsupportedGetMonitoring = async () => {
63
+ throw createUnsupportedReadError();
64
+ };
65
+ const unsupportedAddMonitoring = async () => {
66
+ throw createUnsupportedReadError();
67
+ };
68
+ const unsupportedRemoveMonitoring = async () => {
69
+ throw createUnsupportedReadError();
70
+ };
71
+ const unsupportedStats = async () => {
72
+ throw createUnsupportedReadError();
73
+ };
74
+ export const ProxyTraceStorage = Object.freeze({
75
+ create(settings) {
76
+ const normalized = buildSettings(settings);
77
+ return Object.freeze({
78
+ async writeEntry(entry) {
79
+ await RemoteSignedJson.request(normalized, normalized.normalizedPath, {
80
+ entry,
81
+ });
82
+ },
83
+ async updateEntry(uuid, patch) {
84
+ await RemoteSignedJson.request(normalized, appendSuffix(normalized.normalizedPath, '/update'), { uuid, patch });
85
+ },
86
+ async markFamilyStale(familyHash, exceptUuid) {
87
+ await RemoteSignedJson.request(normalized, appendSuffix(normalized.normalizedPath, '/mark-family-stale'), { familyHash, exceptUuid });
88
+ },
89
+ queryEntries: unsupportedQueryEntries,
90
+ getEntry: unsupportedGetEntry,
91
+ getBatch: unsupportedGetBatch,
92
+ queryBatchEntries: unsupportedQueryBatchEntries,
93
+ prune: unsupportedPrune,
94
+ clear: unsupportedClear,
95
+ getMonitoring: unsupportedGetMonitoring,
96
+ addMonitoring: unsupportedAddMonitoring,
97
+ removeMonitoring: unsupportedRemoveMonitoring,
98
+ stats: unsupportedStats,
99
+ });
100
+ },
101
+ });
102
+ export default ProxyTraceStorage;
@@ -179,6 +179,7 @@ const getCoreRuntime = async () => {
179
179
  };
180
180
  const getQueueWorkerApi = async () => {
181
181
  try {
182
+ // @ts-ignore
182
183
  const mod = (await import('@zintrust/workers'));
183
184
  return typeof mod.createQueueWorker === 'function' ? mod : null;
184
185
  }
@@ -0,0 +1,5 @@
1
+ import type { ITraceConfig, ITraceStorage } from '../types';
2
+ export declare const TraceServiceTag: Readonly<{
3
+ wrapStorage(storage: ITraceStorage, config: ITraceConfig): ITraceStorage;
4
+ }>;
5
+ export default TraceServiceTag;
@@ -0,0 +1,43 @@
1
+ import { ErrorFactory } from '@zintrust/core';
2
+ const appendServiceTag = (entry, serviceTag) => {
3
+ const normalizedTag = serviceTag?.trim() ?? '';
4
+ if (normalizedTag === '' || entry.tags.includes(normalizedTag)) {
5
+ return entry;
6
+ }
7
+ return {
8
+ ...entry,
9
+ tags: [...entry.tags, normalizedTag],
10
+ };
11
+ };
12
+ const unsupportedRead = async () => {
13
+ throw ErrorFactory.createConfigError('Trace proxy mode only supports runtime persistence on the sender. Query the trace server database or dashboard directly.');
14
+ };
15
+ const bindOrUnsupported = (method) => {
16
+ if (method === undefined) {
17
+ return unsupportedRead;
18
+ }
19
+ return method;
20
+ };
21
+ export const TraceServiceTag = Object.freeze({
22
+ wrapStorage(storage, config) {
23
+ const writeEntry = async (entry) => {
24
+ await storage.writeEntry(appendServiceTag(entry, config.serviceTag));
25
+ };
26
+ return Object.freeze({
27
+ writeEntry,
28
+ updateEntry: storage.updateEntry.bind(storage),
29
+ markFamilyStale: storage.markFamilyStale.bind(storage),
30
+ queryEntries: bindOrUnsupported(storage.queryEntries?.bind(storage)),
31
+ getEntry: bindOrUnsupported(storage.getEntry?.bind(storage)),
32
+ getBatch: bindOrUnsupported(storage.getBatch?.bind(storage)),
33
+ queryBatchEntries: bindOrUnsupported(storage.queryBatchEntries?.bind(storage)),
34
+ prune: bindOrUnsupported(storage.prune?.bind(storage)),
35
+ clear: bindOrUnsupported(storage.clear?.bind(storage)),
36
+ getMonitoring: bindOrUnsupported(storage.getMonitoring?.bind(storage)),
37
+ addMonitoring: bindOrUnsupported(storage.addMonitoring?.bind(storage)),
38
+ removeMonitoring: bindOrUnsupported(storage.removeMonitoring?.bind(storage)),
39
+ stats: bindOrUnsupported(storage.stats?.bind(storage)),
40
+ });
41
+ },
42
+ });
43
+ export default TraceServiceTag;
@@ -1,2 +1,4 @@
1
1
  export { TraceStorage } from './TraceStorage';
2
2
  export type { ITraceStorage } from './TraceStorage';
3
+ export { ProxyTraceStorage } from './ProxyTraceStorage';
4
+ export { TraceServiceTag } from './TraceServiceTag';
@@ -1 +1,3 @@
1
1
  export { TraceStorage } from './TraceStorage.js';
2
+ export { ProxyTraceStorage } from './ProxyTraceStorage.js';
3
+ export { TraceServiceTag } from './TraceServiceTag.js';
package/dist/types.d.ts CHANGED
@@ -316,6 +316,14 @@ export type TraceContentDispatchConfig = {
316
316
  enqueueTimeoutMs: number;
317
317
  worker: TraceContentDispatchWorkerConfig;
318
318
  };
319
+ export type TraceProxyConfig = {
320
+ enabled: boolean;
321
+ url?: string;
322
+ path: string;
323
+ keyId?: string;
324
+ secret?: string;
325
+ timeoutMs: number;
326
+ };
319
327
  export type TraceWatcherToggle = boolean | TraceFilterRule;
320
328
  export type TraceRequestWatcherToggle = boolean | TraceRequestWatcherConfig;
321
329
  export type TraceClientRequestWatcherToggle = boolean | TraceClientRequestWatcherConfig;
@@ -345,6 +353,8 @@ export interface ITraceConfig {
345
353
  enabled: boolean;
346
354
  connection?: string;
347
355
  observeConnection?: string;
356
+ serviceTag?: string;
357
+ proxy: TraceProxyConfig;
348
358
  pruneAfterHours: number;
349
359
  ignoreRoutes: string[];
350
360
  ignorePaths: string[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zintrust/trace",
3
- "version": "0.9.3",
3
+ "version": "0.9.4",
4
4
  "description": "Trace assistant for ZinTrust: logs requests, queries, exceptions, jobs, and more.",
5
5
  "private": false,
6
6
  "type": "module",
@@ -40,7 +40,7 @@
40
40
  "node": ">=20.0.0"
41
41
  },
42
42
  "peerDependencies": {
43
- "@zintrust/core": "^0.9.2"
43
+ "@zintrust/core": "^0.9.4"
44
44
  },
45
45
  "publishConfig": {
46
46
  "access": "public"
package/src/config.ts CHANGED
@@ -8,6 +8,7 @@ import type {
8
8
  TraceConfigOverrides,
9
9
  TraceContentDispatchConfig,
10
10
  TraceFilterRule,
11
+ TraceProxyConfig,
11
12
  TraceRequestWatcherConfig,
12
13
  TraceWatcherToggle,
13
14
  } from './types';
@@ -253,10 +254,31 @@ const mergeContentDispatch = (
253
254
  };
254
255
  };
255
256
 
257
+ const mergeProxyConfig = (
258
+ base: TraceProxyConfig,
259
+ override?: TraceConfigOverrides['proxy']
260
+ ): TraceProxyConfig => {
261
+ if (override === undefined) return base;
262
+
263
+ return {
264
+ ...base,
265
+ ...override,
266
+ };
267
+ };
268
+
256
269
  const DEFAULTS: ITraceConfig = Object.freeze({
257
270
  enabled: false,
258
271
  connection: undefined,
259
272
  observeConnection: undefined,
273
+ serviceTag: undefined,
274
+ proxy: {
275
+ enabled: false,
276
+ url: undefined,
277
+ path: '/zin/trace/write',
278
+ keyId: undefined,
279
+ secret: undefined,
280
+ timeoutMs: 30000,
281
+ },
260
282
  pruneAfterHours: 24,
261
283
  ignoreRoutes: ['/trace', '/health', '/ping'],
262
284
  ignorePaths: [],
@@ -342,6 +364,7 @@ export const TraceConfig = Object.freeze({
342
364
  return Object.freeze({
343
365
  ...DEFAULTS,
344
366
  ...overrides,
367
+ proxy: mergeProxyConfig(DEFAULTS.proxy, overrides.proxy),
345
368
  contentDispatch: mergeContentDispatch(DEFAULTS.contentDispatch, overrides.contentDispatch),
346
369
  watchers: mergeWatchers(DEFAULTS.watchers, overrides.watchers),
347
370
  redaction: {
package/src/index.ts CHANGED
@@ -31,6 +31,7 @@ export { TraceContext } from './context';
31
31
  // ---------------------------------------------------------------------------
32
32
  export { registerTraceDashboard, registerTraceRoutes } from './dashboard/routes';
33
33
  export type { TraceDashboardOptions, TraceDashboardRegistrationOptions } from './dashboard/routes';
34
+ export { registerTraceIngestGateway, TraceIngestGateway } from './ingest/TraceIngestGateway';
34
35
 
35
36
  // ---------------------------------------------------------------------------
36
37
  // Watchers (named re-exports for use with custom wiring)