@zintrust/trace 0.4.76 → 0.4.79
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 +101 -15
- package/dist/build-manifest.json +78 -38
- package/dist/config.d.ts +1 -0
- package/dist/config.js +123 -4
- package/dist/dashboard/ui.js +88 -29
- package/dist/index.d.ts +7 -0
- package/dist/index.js +5 -0
- package/dist/migrations/20260331000001_create_zin_trace_entries_table.js +1 -1
- package/dist/migrations/20260407193000_widen_trace_created_at_for_sql.d.ts +10 -0
- package/dist/migrations/20260407193000_widen_trace_created_at_for_sql.js +34 -0
- package/dist/migrations/index.js +2 -1
- package/dist/register.js +107 -9
- package/dist/storage/TraceContentRedaction.d.ts +4 -0
- package/dist/storage/TraceContentRedaction.js +33 -0
- package/dist/storage/TraceEntryFiltering.d.ts +4 -0
- package/dist/storage/TraceEntryFiltering.js +13 -0
- package/dist/storage/TraceStorage.js +35 -5
- package/dist/storage/TraceWriteDiagnostics.d.ts +19 -0
- package/dist/storage/TraceWriteDiagnostics.js +98 -0
- package/dist/types.d.ts +38 -21
- package/dist/utils/entryFilter.d.ts +4 -0
- package/dist/utils/entryFilter.js +95 -0
- package/dist/utils/redact.d.ts +1 -0
- package/dist/utils/redact.js +43 -9
- package/dist/watchers/CommandWatcher.js +1 -1
- package/dist/watchers/ExceptionWatcher.d.ts +8 -1
- package/dist/watchers/ExceptionWatcher.js +12 -7
- package/dist/watchers/HttpClientWatcher.js +1 -1
- package/dist/watchers/HttpWatcher.js +112 -21
- package/package.json +2 -2
- package/src/config.ts +152 -5
- package/src/dashboard/routes.ts +6 -2
- package/src/dashboard/ui.ts +88 -29
- package/src/index.ts +10 -0
- package/src/register.ts +137 -10
- package/src/storage/TraceContentRedaction.ts +44 -0
- package/src/storage/TraceEntryFiltering.ts +14 -0
- package/src/storage/TraceStorage.ts +52 -5
- package/src/storage/TraceWriteDiagnostics.ts +174 -0
- package/src/types.ts +41 -21
- package/src/utils/entryFilter.ts +108 -0
- package/src/utils/redact.ts +57 -9
- package/src/watchers/CommandWatcher.ts +1 -1
- package/src/watchers/ExceptionWatcher.ts +21 -8
- package/src/watchers/HttpClientWatcher.ts +1 -1
- package/src/watchers/HttpWatcher.ts +142 -23
- package/src/watchers/LogWatcher.ts +26 -28
package/src/dashboard/ui.ts
CHANGED
|
@@ -77,7 +77,7 @@ const DASHBOARD_DOCUMENT = `<!DOCTYPE html>
|
|
|
77
77
|
.section-head{display:flex;justify-content:space-between;align-items:flex-start;gap:12px;padding:22px 24px 16px}.section-head h3{margin:0;font-size:1.04rem}.section-head p{margin:6px 0 0;color:var(--muted);font-size:.92rem}.toolbar{display:flex;flex-wrap:wrap;gap:10px;padding:0 24px 18px}.control,.toolbar input,.toolbar select{height:44px;border-radius:13px;border:1px solid var(--line);background:var(--surface-strong);color:var(--text);padding:0 14px;min-width:0}.toolbar input,.toolbar select{flex:1 1 180px}.toolbar input::placeholder{color:var(--muted)}.btn{height:44px;border:none;border-radius:13px;padding:0 16px;cursor:pointer;font-weight:800}.btn-primary{background:linear-gradient(135deg,var(--accent-strong),var(--accent));color:#fff}.btn-danger{background:rgba(239,68,68,.12);color:var(--danger);border:1px solid rgba(239,68,68,.18)}.btn-ghost{background:var(--surface-soft);color:var(--text);border:1px solid var(--line)}
|
|
78
78
|
.table-wrap{overflow:auto;padding:0 12px 12px}table{width:100%;border-collapse:separate;border-spacing:0;min-width:880px}th{padding:14px;color:var(--muted);font-size:.74rem;font-weight:800;letter-spacing:.12em;text-transform:uppercase;text-align:left;border-bottom:1px solid var(--line)}td{padding:15px 14px;border-bottom:1px solid var(--line);vertical-align:top}.row-button{cursor:pointer}.row-button:hover td{background:rgba(56,189,248,.05)}.summary{font-size:.93rem;font-weight:700;line-height:1.4;color:var(--text)}.summary-sub{margin-top:6px;color:var(--muted);font-size:.82rem;line-height:1.4}.mono{font-family:var(--mono)}.empty{padding:44px 24px;color:var(--muted);line-height:1.65;text-align:center}.pagination{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:0 24px 24px;color:var(--muted);flex-wrap:wrap}.pagination-controls{display:flex;gap:8px}.pagination button{height:40px;min-width:92px;padding:0 14px;border-radius:12px;border:1px solid var(--line);background:var(--surface-strong);color:var(--text);cursor:pointer}.pagination button:disabled{opacity:.45;cursor:not-allowed}
|
|
79
79
|
.activity-list{list-style:none;margin:0;padding:0 24px 24px}.activity-item{padding:14px 0;border-top:1px solid var(--line)}.activity-item:first-child{border-top:none}.activity-head{display:flex;align-items:center;gap:10px;flex-wrap:wrap}.activity-time{color:var(--muted);font-size:.85rem}.activity-summary{margin-top:8px;color:var(--text);line-height:1.48}.back-link{display:inline-flex;align-items:center;gap:8px;margin:0 0 14px;color:var(--accent);font-weight:800;cursor:pointer}.detail-card{padding:24px}.detail-meta{display:flex;flex-wrap:wrap;gap:10px;margin:14px 0 20px;color:var(--muted);font-size:.9rem}.detail-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:14px}.detail-stack{display:grid;gap:16px;margin-top:18px}.detail-box{padding:16px;border-radius:16px;background:var(--surface-soft);border:1px solid var(--line)}.detail-box h4{margin:0 0 10px;font-size:.92rem}.detail-box dl{margin:0;display:grid;gap:8px}.detail-box dt{font-size:.76rem;letter-spacing:.08em;text-transform:uppercase;color:var(--muted);font-weight:800}.detail-box dd{margin:0;color:var(--text);line-height:1.45}.trace-tabs{display:flex;gap:10px;flex-wrap:wrap;margin:20px 0 16px}.trace-tab{border:none;border-radius:12px;padding:10px 12px;background:transparent;color:var(--muted);cursor:pointer;box-shadow:inset 0 0 0 1px var(--line);font-weight:800}.trace-tab.active{background:rgba(56,189,248,.12);color:var(--text);box-shadow:inset 0 0 0 1px rgba(56,189,248,.28)}.trace-panel{display:grid;gap:14px}.trace-item{padding:18px;border-radius:16px;background:var(--surface-soft);border:1px solid var(--line)}.trace-item-head{display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap}.trace-item-summary{margin-top:10px;display:grid;gap:10px}.trace-note{color:var(--muted);line-height:1.6}
|
|
80
|
-
.tag{display:inline-flex;align-items:center;gap:6px;padding:4px 10px;border-radius:999px;background:rgba(56,189,248,.12);color:#bae6fd;font-size:.78rem;font-weight:800;margin:0 6px 6px 0;border:1px solid rgba(56,189,248,.18)}button.tag{cursor:pointer}html[data-theme='light'] .tag{color:#075985}.tag.failed{background:rgba(239,68,68,.14);color:#fecaca;border-color:rgba(239,68,68,.2)}html[data-theme='light'] .tag.failed{color:#b91c1c}.tag.slow{background:rgba(245,158,11,.12);color:#fde68a;border-color:rgba(245,158,11,.18)}html[data-theme='light'] .tag.slow{color:#92400e}.type-pill{display:inline-flex;align-items:center;gap:6px;padding:6px 10px;border-radius:999px;font-size:.74rem;font-weight:900;text-transform:uppercase;letter-spacing:.08em;border:1px solid transparent}.pill-request{background:rgba(56,189,248,.14);color:#93c5fd}.pill-query{background:rgba(34,197,94,.12);color:#86efac}.pill-exception{background:rgba(239,68,68,.14);color:#fecaca}.pill-log{background:rgba(168,85,247,.14);color:#ddd6fe}.pill-job,.pill-batch{background:rgba(245,158,11,.14);color:#fde68a}.pill-cache{background:rgba(20,184,166,.12);color:#99f6e4}.pill-schedule,.pill-command{background:rgba(14,165,233,.14);color:#bae6fd}.pill-mail,.pill-notification{background:rgba(236,72,153,.14);color:#fbcfe8}.pill-auth{background:rgba(148,163,184,.16);color:#e2e8f0}.pill-event,.pill-model{background:rgba(74,222,128,.14);color:#bbf7d0}.pill-redis{background:rgba(239,68,68,.12);color:#fecaca}.pill-gate{background:rgba(99,102,241,.14);color:#c7d2fe}.pill-middleware{background:rgba(45,212,191,.12);color:#ccfbf1}.pill-dump,.pill-view{background:rgba(148,163,184,.14);color:#e2e8f0}.pill-client-request{background:rgba(59,130,246,.14);color:#bfdbfe}html[data-theme='light'] .pill-request{color:#1d4ed8}html[data-theme='light'] .pill-query{color:#166534}html[data-theme='light'] .pill-exception{color:#b91c1c}html[data-theme='light'] .pill-log{color:#6d28d9}html[data-theme='light'] .pill-job,html[data-theme='light'] .pill-batch{color:#92400e}html[data-theme='light'] .pill-cache{color:#115e59}html[data-theme='light'] .pill-schedule,html[data-theme='light'] .pill-command{color:#0c4a6e}html[data-theme='light'] .pill-mail,html[data-theme='light'] .pill-notification{color:#9d174d}html[data-theme='light'] .pill-auth,html[data-theme='light'] .pill-dump,html[data-theme='light'] .pill-view{color:#334155}html[data-theme='light'] .pill-event,html[data-theme='light'] .pill-model{color:#166534}html[data-theme='light'] .pill-redis{color:#991b1b}html[data-theme='light'] .pill-gate{color:#3730a3}html[data-theme='light'] .pill-middleware{color:#155e75}html[data-theme='light'] .pill-client-request{color:#1d4ed8}
|
|
80
|
+
.tag{display:inline-flex;align-items:center;gap:6px;padding:4px 10px;border-radius:999px;background:rgba(56,189,248,.12);color:#bae6fd;font-size:.78rem;font-weight:800;margin:0 6px 6px 0;border:1px solid rgba(56,189,248,.18);text-decoration:none}button.tag{cursor:pointer}html[data-theme='light'] .tag{color:#075985}.tag.failed{background:rgba(239,68,68,.14);color:#fecaca;border-color:rgba(239,68,68,.2)}html[data-theme='light'] .tag.failed{color:#b91c1c}.tag.slow{background:rgba(245,158,11,.12);color:#fde68a;border-color:rgba(245,158,11,.18)}html[data-theme='light'] .tag.slow{color:#92400e}.type-pill{display:inline-flex;align-items:center;gap:6px;padding:6px 10px;border-radius:999px;font-size:.74rem;font-weight:900;text-transform:uppercase;letter-spacing:.08em;border:1px solid transparent}.pill-request{background:rgba(56,189,248,.14);color:#93c5fd}.pill-request.method-get{background:rgba(34,197,94,.16);color:#bbf7d0}.pill-request.method-post{background:rgba(59,130,246,.16);color:#bfdbfe}.pill-request.method-other{background:rgba(245,158,11,.16);color:#fde68a}.pill-query{background:rgba(34,197,94,.12);color:#86efac}.pill-exception{background:rgba(239,68,68,.14);color:#fecaca}.pill-log{background:rgba(168,85,247,.14);color:#ddd6fe}.pill-job,.pill-batch{background:rgba(245,158,11,.14);color:#fde68a}.pill-cache{background:rgba(20,184,166,.12);color:#99f6e4}.pill-schedule,.pill-command{background:rgba(14,165,233,.14);color:#bae6fd}.pill-mail,.pill-notification{background:rgba(236,72,153,.14);color:#fbcfe8}.pill-auth{background:rgba(148,163,184,.16);color:#e2e8f0}.pill-event,.pill-model{background:rgba(74,222,128,.14);color:#bbf7d0}.pill-redis{background:rgba(239,68,68,.12);color:#fecaca}.pill-gate{background:rgba(99,102,241,.14);color:#c7d2fe}.pill-middleware{background:rgba(45,212,191,.12);color:#ccfbf1}.pill-dump,.pill-view{background:rgba(148,163,184,.14);color:#e2e8f0}.pill-client-request{background:rgba(59,130,246,.14);color:#bfdbfe}html[data-theme='light'] .pill-request{color:#1d4ed8}html[data-theme='light'] .pill-request.method-get{color:#166534}html[data-theme='light'] .pill-request.method-post{color:#1d4ed8}html[data-theme='light'] .pill-request.method-other{color:#92400e}html[data-theme='light'] .pill-query{color:#166534}html[data-theme='light'] .pill-exception{color:#b91c1c}html[data-theme='light'] .pill-log{color:#6d28d9}html[data-theme='light'] .pill-job,html[data-theme='light'] .pill-batch{color:#92400e}html[data-theme='light'] .pill-cache{color:#115e59}html[data-theme='light'] .pill-schedule,html[data-theme='light'] .pill-command{color:#0c4a6e}html[data-theme='light'] .pill-mail,html[data-theme='light'] .pill-notification{color:#9d174d}html[data-theme='light'] .pill-auth,html[data-theme='light'] .pill-dump,html[data-theme='light'] .pill-view{color:#334155}html[data-theme='light'] .pill-event,html[data-theme='light'] .pill-model{color:#166534}html[data-theme='light'] .pill-redis{color:#991b1b}html[data-theme='light'] .pill-gate{color:#3730a3}html[data-theme='light'] .pill-middleware{color:#155e75}html[data-theme='light'] .pill-client-request{color:#1d4ed8}
|
|
81
81
|
.monitoring-wrap{padding:0 24px 24px}.tag-list{display:flex;flex-wrap:wrap;gap:10px;margin-bottom:18px}.tag-item{display:inline-flex;align-items:center;gap:10px;padding:10px 14px;border-radius:999px;border:1px solid var(--line);background:var(--surface-strong)}.tag-remove{border:none;background:rgba(239,68,68,.14);color:var(--danger);border-radius:999px;width:24px;height:24px;cursor:pointer;font-size:1rem;line-height:1}.helper-text{color:var(--muted);line-height:1.6}
|
|
82
82
|
.duration-chip{display:inline-flex;align-items:center;padding:5px 9px;border-radius:999px;border:1px solid transparent;font-size:.8rem;font-weight:700;color:var(--text);white-space:nowrap}.duration-chip.vfast{background:rgba(34,197,94,.14);border-color:rgba(34,197,94,.28);color:#bbf7d0}.duration-chip.fast{background:rgba(56,189,248,.12);border-color:rgba(56,189,248,.24);color:#bae6fd}.duration-chip.slow{background:rgba(245,158,11,.12);border-color:rgba(245,158,11,.22);color:#fde68a}.duration-chip.vslow{background:rgba(239,68,68,.14);border-color:rgba(239,68,68,.24);color:#fecaca}html[data-theme='light'] .duration-chip.vfast{color:#166534}html[data-theme='light'] .duration-chip.fast{color:#1d4ed8}html[data-theme='light'] .duration-chip.slow{color:#92400e}html[data-theme='light'] .duration-chip.vslow{color:#b91c1c}
|
|
83
83
|
.code-card{border-radius:16px;border:1px solid var(--code-border);background:var(--surface-soft);overflow:hidden}.code-toolbar{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:12px 14px;border-bottom:1px solid var(--line)}.code-label{font-size:.76rem;letter-spacing:.12em;text-transform:uppercase;color:var(--muted);font-weight:800}.copy-button{display:inline-flex;align-items:center;justify-content:center;gap:8px;width:38px;height:38px;border-radius:12px;border:1px solid var(--line);background:var(--surface-strong);color:var(--text);cursor:pointer;transition:border-color .16s ease,color .16s ease}.copy-button:hover{border-color:rgba(56,189,248,.35);color:var(--accent)}.copy-button[data-copied='true']{color:var(--success);border-color:rgba(34,197,94,.28)}.copy-button svg{width:16px;height:16px;display:block}.code-block{margin:0;padding:18px 20px;background:var(--code-bg);color:#dbeafe;border:0;overflow:auto;white-space:pre;line-height:1.72;font-family:var(--mono);font-size:.92rem}.code-block code{font-family:inherit}.tok-key{color:#93c5fd}.tok-string{color:#86efac}.tok-number{color:#f9a8d4}.tok-boolean{color:#facc15}.tok-null{color:#fb7185}.tok-punctuation{color:#94a3b8}.tok-sql-keyword{color:#f472b6;font-weight:700}.tok-sql-identifier{color:#93c5fd}.tok-sql-string{color:#86efac}.tok-sql-number{color:#facc15}.tok-sql-comment{color:#64748b;font-style:italic}html[data-theme='light'] .code-block{color:#0f172a}html[data-theme='light'] .tok-key{color:#1d4ed8}html[data-theme='light'] .tok-string{color:#15803d}html[data-theme='light'] .tok-number{color:#c026d3}html[data-theme='light'] .tok-boolean{color:#b45309}html[data-theme='light'] .tok-null{color:#dc2626}html[data-theme='light'] .tok-punctuation{color:#64748b}html[data-theme='light'] .tok-sql-keyword{color:#db2777}html[data-theme='light'] .tok-sql-identifier{color:#2563eb}html[data-theme='light'] .tok-sql-string{color:#15803d}html[data-theme='light'] .tok-sql-number{color:#b45309}html[data-theme='light'] .tok-sql-comment{color:#6b7280}
|
|
@@ -146,15 +146,27 @@ const DASHBOARD_DOCUMENT = `<!DOCTYPE html>
|
|
|
146
146
|
monitoring: { title: 'Monitoring tags', subtitle: 'Pinned tags for trace pivots.' }
|
|
147
147
|
};
|
|
148
148
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
149
|
+
const createInitialState = () => {
|
|
150
|
+
const search = new URLSearchParams(window.location.search);
|
|
151
|
+
const page = search.get('page');
|
|
152
|
+
const entriesPage = Number.parseInt(search.get('entriesPage') || '1', 10);
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
page: page && Object.prototype.hasOwnProperty.call(PAGE_COPY, page) ? page : 'overview',
|
|
156
|
+
entriesPage: Number.isFinite(entriesPage) && entriesPage > 0 ? entriesPage : 1,
|
|
157
|
+
entriesFilter: {
|
|
158
|
+
type: search.get('type') || '',
|
|
159
|
+
tag: search.get('tag') || '',
|
|
160
|
+
batchId: search.get('batchId') || ''
|
|
161
|
+
},
|
|
162
|
+
detail: null,
|
|
163
|
+
detailBatch: null,
|
|
164
|
+
detailTab: 'summary'
|
|
165
|
+
};
|
|
156
166
|
};
|
|
157
167
|
|
|
168
|
+
let state = createInitialState();
|
|
169
|
+
|
|
158
170
|
let copySequence = 0;
|
|
159
171
|
const copyPayloads = new Map();
|
|
160
172
|
|
|
@@ -199,7 +211,19 @@ const DASHBOARD_DOCUMENT = `<!DOCTYPE html>
|
|
|
199
211
|
return response.json();
|
|
200
212
|
};
|
|
201
213
|
|
|
202
|
-
const
|
|
214
|
+
const requestMethodClass = (entry) => {
|
|
215
|
+
if (!entry || entry.type !== 'request') return '';
|
|
216
|
+
const method = String(entry.content && entry.content.method || '').toUpperCase();
|
|
217
|
+
if (method === 'GET') return ' method-get';
|
|
218
|
+
if (method === 'POST') return ' method-post';
|
|
219
|
+
return ' method-other';
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
const typeClass = (entryOrType, maybeEntry) => {
|
|
223
|
+
const entry = maybeEntry || (typeof entryOrType === 'object' ? entryOrType : null);
|
|
224
|
+
const type = entry && entry.type ? entry.type : entryOrType;
|
|
225
|
+
return 'type-pill pill-' + String(type || '').replace(/_/g, '-') + requestMethodClass(entry);
|
|
226
|
+
};
|
|
203
227
|
|
|
204
228
|
const timeSince = (value) => {
|
|
205
229
|
const createdAt = Number(value);
|
|
@@ -251,11 +275,34 @@ const DASHBOARD_DOCUMENT = `<!DOCTYPE html>
|
|
|
251
275
|
return '<span class="duration-chip ' + tone + '" title="' + toneLabel + '">' + escapeHtml(formatDuration(duration)) + '</span>';
|
|
252
276
|
};
|
|
253
277
|
|
|
278
|
+
const buildEntriesHref = (params) => {
|
|
279
|
+
const search = new URLSearchParams();
|
|
280
|
+
search.set('page', 'entries');
|
|
281
|
+
if (params.type) search.set('type', String(params.type));
|
|
282
|
+
if (params.tag) search.set('tag', String(params.tag));
|
|
283
|
+
if (params.batchId) search.set('batchId', String(params.batchId));
|
|
284
|
+
return BASE + '?' + search.toString();
|
|
285
|
+
};
|
|
286
|
+
|
|
254
287
|
const tagsHtml = (tags) => (tags || []).map((tag) => {
|
|
255
288
|
const css = tag === 'failed' ? 'tag failed' : tag === 'slow' ? 'tag slow' : 'tag';
|
|
256
|
-
|
|
289
|
+
const href = buildEntriesHref({ tag });
|
|
290
|
+
return '<a class="' + css + '" data-action="filter-tag" data-tag="' + escapeHtml(tag) + '" href="' + escapeHtml(href) + '">' + escapeHtml(tag) + '</a>';
|
|
257
291
|
}).join('');
|
|
258
292
|
|
|
293
|
+
const syncUrl = () => {
|
|
294
|
+
const search = new URLSearchParams();
|
|
295
|
+
if (state.page !== 'overview') search.set('page', state.page);
|
|
296
|
+
if (state.page === 'entries' || state.entriesFilter.type || state.entriesFilter.tag || state.entriesFilter.batchId) {
|
|
297
|
+
if (state.entriesFilter.type) search.set('type', state.entriesFilter.type);
|
|
298
|
+
if (state.entriesFilter.tag) search.set('tag', state.entriesFilter.tag);
|
|
299
|
+
if (state.entriesFilter.batchId) search.set('batchId', state.entriesFilter.batchId);
|
|
300
|
+
if (state.entriesPage > 1) search.set('entriesPage', String(state.entriesPage));
|
|
301
|
+
}
|
|
302
|
+
const nextUrl = search.toString() === '' ? BASE : BASE + '?' + search.toString();
|
|
303
|
+
window.history.replaceState(null, '', nextUrl);
|
|
304
|
+
};
|
|
305
|
+
|
|
259
306
|
const batchSnippet = (batchId) => {
|
|
260
307
|
const raw = String(batchId || '');
|
|
261
308
|
return raw === '' ? '-' : escapeHtml(raw.slice(0, 8));
|
|
@@ -294,7 +341,7 @@ const DASHBOARD_DOCUMENT = `<!DOCTYPE html>
|
|
|
294
341
|
].join('');
|
|
295
342
|
};
|
|
296
343
|
|
|
297
|
-
const highlightJson = (value) => {
|
|
344
|
+
const highlightJson = (value, label = 'JSON') => {
|
|
298
345
|
const source = prettyJson(value);
|
|
299
346
|
let output = '';
|
|
300
347
|
let lastIndex = 0;
|
|
@@ -313,7 +360,7 @@ const DASHBOARD_DOCUMENT = `<!DOCTYPE html>
|
|
|
313
360
|
}
|
|
314
361
|
|
|
315
362
|
output += escapeHtml(source.slice(lastIndex));
|
|
316
|
-
return renderCodeCard(
|
|
363
|
+
return renderCodeCard(label, source, output, 'language-json');
|
|
317
364
|
};
|
|
318
365
|
|
|
319
366
|
const highlightSql = (sql) => {
|
|
@@ -343,11 +390,11 @@ const DASHBOARD_DOCUMENT = `<!DOCTYPE html>
|
|
|
343
390
|
return renderCodeCard('SQL', source, output, 'language-sql');
|
|
344
391
|
};
|
|
345
392
|
|
|
346
|
-
const detailJson = (value) => highlightJson(value ?? {});
|
|
393
|
+
const detailJson = (value, label = 'JSON') => highlightJson(value ?? {}, label);
|
|
347
394
|
|
|
348
395
|
const entrySummaryText = (entry) => {
|
|
349
396
|
const content = entry && entry.content ? entry.content : {};
|
|
350
|
-
if (entry.type === 'request') return [content.method || '', content.uri || ''].filter(Boolean).join(' ');
|
|
397
|
+
if (entry.type === 'request') return [content.responseStatus || '', content.method || '', content.uri || ''].filter(Boolean).join(' ');
|
|
351
398
|
if (entry.type === 'query') return String(content.sql || '').slice(0, 160);
|
|
352
399
|
if (entry.type === 'exception') return [content.class || '', content.message || ''].filter(Boolean).join(': ');
|
|
353
400
|
if (entry.type === 'log') return '[' + String(content.level || 'log') + '] ' + String(content.message || '').slice(0, 160);
|
|
@@ -475,7 +522,7 @@ const DASHBOARD_DOCUMENT = `<!DOCTYPE html>
|
|
|
475
522
|
'<section class="trace-item">',
|
|
476
523
|
'<div class="trace-item-head">',
|
|
477
524
|
'<div>',
|
|
478
|
-
'<span class="' + typeClass(entry
|
|
525
|
+
'<span class="' + typeClass(entry) + '">' + escapeHtml(entry.type) + '</span>',
|
|
479
526
|
'</div>',
|
|
480
527
|
'<div class="activity-head">' + durationHtml(entry) + '<span class="activity-time">' + escapeHtml(timeSince(entry.createdAt)) + '</span></div>',
|
|
481
528
|
'</div>',
|
|
@@ -501,10 +548,11 @@ const DASHBOARD_DOCUMENT = `<!DOCTYPE html>
|
|
|
501
548
|
{ id: 'logs', label: 'Logs', count: batchEntriesByType('log').length },
|
|
502
549
|
{ id: 'exceptions', label: 'Exceptions', count: batchEntriesByType('exception').length },
|
|
503
550
|
{ id: 'http', label: 'HTTP', count: batchEntriesByType('client_request').length },
|
|
504
|
-
{ id: '
|
|
551
|
+
{ id: 'cache', label: 'Cache', count: batchEntriesByType('cache').length },
|
|
552
|
+
{ id: 'other', label: 'Other', count: batchEntries().filter((item) => !['request','query','log','exception','client_request','cache'].includes(item.type)).length }
|
|
505
553
|
];
|
|
506
554
|
const currentTab = traceTabs.some((tab) => tab.id === state.detailTab) ? state.detailTab : 'summary';
|
|
507
|
-
const otherEntries = batchEntries().filter((item) => !['request','query','log','exception','client_request'].includes(item.type));
|
|
555
|
+
const otherEntries = batchEntries().filter((item) => !['request','query','log','exception','client_request','cache'].includes(item.type));
|
|
508
556
|
const panels = {
|
|
509
557
|
summary: [
|
|
510
558
|
'<div class="detail-grid">',
|
|
@@ -525,20 +573,23 @@ const DASHBOARD_DOCUMENT = `<!DOCTYPE html>
|
|
|
525
573
|
]),
|
|
526
574
|
'</div>'
|
|
527
575
|
].join(''),
|
|
528
|
-
payload: detailJson(content.payload || {}),
|
|
529
|
-
headers: '<div class="detail-stack">' + detailJson(content.headers || {}) + detailJson(content.responseHeaders || {}) + '</div>',
|
|
530
|
-
response: '<div class="detail-stack"><div class="detail-grid">' + renderMetricBox('Status', [{ label: 'Response status', value: escapeHtml(content.responseStatus || '') }, { label: 'Duration', value: escapeHtml(formatDuration(getEntryDuration(entry))) }]) + '</div
|
|
576
|
+
payload: detailJson(content.payload || {}, 'Payload Json'),
|
|
577
|
+
headers: '<div class="detail-stack">' + detailJson(content.headers || {}, 'Request Header Json') + detailJson(content.responseHeaders || {}, 'Response Header Json') + '</div>',
|
|
578
|
+
response: '<div class="detail-stack"><div class="detail-grid">' + renderMetricBox('Status', [{ label: 'Response status', value: escapeHtml(content.responseStatus || '') }, { label: 'Duration', value: escapeHtml(formatDuration(getEntryDuration(entry))) }]) + '</div>' + (content.responseBody === undefined ? '<p class="trace-note">No response body was captured for this request.</p>' : detailJson(content.responseBody, 'Response Body Json')) + detailJson(content.responseHeaders || {}, 'Response Header Json') + '</div>',
|
|
531
579
|
queries: renderTraceItems(batchEntriesByType('query')),
|
|
532
580
|
logs: renderTraceItems(batchEntriesByType('log')),
|
|
533
581
|
exceptions: renderTraceItems(batchEntriesByType('exception')),
|
|
534
582
|
http: renderTraceItems(batchEntriesByType('client_request')),
|
|
583
|
+
cache: renderTraceItems(batchEntriesByType('cache')),
|
|
535
584
|
other: renderTraceItems(otherEntries)
|
|
536
585
|
};
|
|
537
586
|
|
|
538
587
|
main.innerHTML = [
|
|
539
588
|
'<span class="back-link" data-action="close-detail"><- Back to entries</span>',
|
|
540
589
|
'<section class="panel detail-card">',
|
|
541
|
-
'<div
|
|
590
|
+
'<div>' + (entry.type === 'request'
|
|
591
|
+
? '<span class="' + typeClass(entry) + '">' + escapeHtml(entry.type) + '</span> <span class="' + typeClass(entry) + '">' + escapeHtml(content.responseStatus || '') + '</span> <span class="mono">' + escapeHtml(content.uri || '') + '</span> ' + tagsHtml(entry.tags)
|
|
592
|
+
: '<span class="' + typeClass(entry) + '">' + escapeHtml(entry.type) + '</span> ' + tagsHtml(entry.tags)) + '</div>',
|
|
542
593
|
'<div class="detail-meta"><span>UUID <span class="mono">' + escapeHtml(entry.uuid) + '</span></span><span>Batch <span class="mono">' + escapeHtml(entry.batchId || '-') + '</span></span><span>' + durationHtml(entry) + '</span><span>' + escapeHtml(new Date(Number(entry.createdAt)).toISOString()) + '</span></div>',
|
|
543
594
|
'<div class="trace-tabs">',
|
|
544
595
|
traceTabs.map((tab) => '<button type="button" class="trace-tab' + (tab.id === currentTab ? ' active' : '') + '" data-action="detail-tab" data-tab="' + escapeHtml(tab.id) + '">' + escapeHtml(tab.label) + (tab.count !== undefined ? ' (' + escapeHtml(tab.count) + ')' : '') + '</button>').join(''),
|
|
@@ -564,10 +615,10 @@ const DASHBOARD_DOCUMENT = `<!DOCTYPE html>
|
|
|
564
615
|
const recentRows = recent.data || [];
|
|
565
616
|
const recentTable = recentRows.length === 0
|
|
566
617
|
? '<div class="empty">No trace entries recorded.</div>'
|
|
567
|
-
: '<div class="table-wrap"><table><thead><tr><th>Type</th><th>Summary</th><th>Tags</th><th>Duration</th><th>Happened</th></tr></thead><tbody>' + recentRows.map((entry) => '<tr class="row-button" data-action="show-detail" data-uuid="' + escapeHtml(entry.uuid) + '"><td><span class="' + typeClass(entry
|
|
618
|
+
: '<div class="table-wrap"><table><thead><tr><th>Type</th><th>Summary</th><th>Tags</th><th>Duration</th><th>Happened</th></tr></thead><tbody>' + recentRows.map((entry) => '<tr class="row-button" data-action="show-detail" data-uuid="' + escapeHtml(entry.uuid) + '"><td><span class="' + typeClass(entry) + '">' + escapeHtml(entry.type) + '</span></td><td>' + entrySummaryHtml(entry) + '</td><td>' + tagsHtml(entry.tags) + '</td><td>' + durationHtml(entry) + '</td><td class="activity-time">' + escapeHtml(timeSince(entry.createdAt)) + '</td></tr>').join('') + '</tbody></table></div>';
|
|
568
619
|
const activityList = recentRows.length === 0
|
|
569
620
|
? '<div class="empty">No recent activity.</div>'
|
|
570
|
-
: '<ul class="activity-list">' + recentRows.slice(0, 5).map((entry) => '<li class="activity-item"><div class="activity-head"><span class="' + typeClass(entry
|
|
621
|
+
: '<ul class="activity-list">' + recentRows.slice(0, 5).map((entry) => '<li class="activity-item"><div class="activity-head"><span class="' + typeClass(entry) + '">' + escapeHtml(entry.type) + '</span>' + durationHtml(entry) + '<span class="activity-time">' + escapeHtml(timeSince(entry.createdAt)) + '</span></div><div class="activity-summary">' + escapeHtml(entrySummaryText(entry)) + '</div></li>').join('') + '</ul>';
|
|
571
622
|
|
|
572
623
|
main.innerHTML = [
|
|
573
624
|
statsCardsHtml(stats),
|
|
@@ -611,7 +662,7 @@ const DASHBOARD_DOCUMENT = `<!DOCTYPE html>
|
|
|
611
662
|
const total = Number(response.total || 0);
|
|
612
663
|
const perPage = Number(response.perPage || 50);
|
|
613
664
|
const totalPages = Math.max(1, Math.ceil(total / perPage));
|
|
614
|
-
const rows = data.map((entry) => '<tr class="row-button" data-action="show-detail" data-uuid="' + escapeHtml(entry.uuid) + '"><td><span class="' + typeClass(entry
|
|
665
|
+
const rows = data.map((entry) => '<tr class="row-button" data-action="show-detail" data-uuid="' + escapeHtml(entry.uuid) + '"><td><span class="' + typeClass(entry) + '">' + escapeHtml(entry.type) + '</span></td><td>' + entrySummaryHtml(entry) + '</td><td>' + tagsHtml(entry.tags) + '</td><td>' + durationHtml(entry) + '</td><td class="mono">' + batchSnippet(entry.batchId) + '</td><td class="activity-time">' + escapeHtml(timeSince(entry.createdAt)) + '</td></tr>').join('');
|
|
615
666
|
|
|
616
667
|
main.innerHTML = [
|
|
617
668
|
'<section class="panel">',
|
|
@@ -647,7 +698,7 @@ const DASHBOARD_DOCUMENT = `<!DOCTYPE html>
|
|
|
647
698
|
main.innerHTML = [
|
|
648
699
|
'<span class="back-link" data-action="close-detail"><- Back to entries</span>',
|
|
649
700
|
'<section class="panel detail-card">',
|
|
650
|
-
'<div><span class="' + typeClass(entry
|
|
701
|
+
'<div><span class="' + typeClass(entry) + '">' + escapeHtml(entry.type) + '</span> ' + tagsHtml(entry.tags) + '</div>',
|
|
651
702
|
'<div class="detail-meta"><span>UUID <span class="mono">' + escapeHtml(entry.uuid) + '</span></span><span>Batch <span class="mono">' + escapeHtml(entry.batchId || '-') + '</span></span><span>' + durationHtml(entry) + '</span><span>' + escapeHtml(new Date(Number(entry.createdAt)).toISOString()) + '</span></div>',
|
|
652
703
|
'<div class="detail-stack">',
|
|
653
704
|
renderEntryBody(entry),
|
|
@@ -663,13 +714,14 @@ const DASHBOARD_DOCUMENT = `<!DOCTYPE html>
|
|
|
663
714
|
const tags = result.tags || [];
|
|
664
715
|
main.innerHTML = [
|
|
665
716
|
'<section class="panel">',
|
|
666
|
-
'<div class="section-head"><div><h3>Monitoring tags</h3><p>
|
|
717
|
+
'<div class="section-head"><div><h3>Monitoring tags</h3><p>Save the tags you filter by often so they stay one click away.</p></div></div>',
|
|
667
718
|
'<div class="monitoring-wrap">',
|
|
668
719
|
'<div class="tag-list">',
|
|
669
|
-
tags.length === 0 ? '<span class="helper-text">No tags monitored.</span>' : tags.map((tag) => '<span class="tag-item"><
|
|
720
|
+
tags.length === 0 ? '<span class="helper-text">No tags monitored yet. Add tags like auth, checkout, queue:emails, or nightly-sync to pin them here.</span>' : tags.map((tag) => '<span class="tag-item"><a class="tag mono" data-action="filter-tag" data-tag="' + escapeHtml(tag) + '" href="' + escapeHtml(buildEntriesHref({ tag })) + '">' + escapeHtml(tag) + '</a><button type="button" class="tag-remove" data-action="remove-tag" data-tag="' + escapeHtml(tag) + '">x</button></span>').join(''),
|
|
670
721
|
'</div>',
|
|
722
|
+
'<p class="helper-text">Click a saved tag to filter the entries list by that exact tag. Because each tag is a real link, you can also open it in a new tab. Removing a saved tag only removes the shortcut here; it does not delete any trace entries.</p>',
|
|
671
723
|
'<div class="toolbar" style="padding:0;margin-top:8px">',
|
|
672
|
-
'<input id="new-tag" class="control" type="text" placeholder="Add tag">',
|
|
724
|
+
'<input id="new-tag" class="control" type="text" placeholder="Add tag, for example checkout">',
|
|
673
725
|
'<button type="button" class="btn btn-primary" data-action="add-tag">Add tag</button>',
|
|
674
726
|
'</div>',
|
|
675
727
|
'</div>',
|
|
@@ -694,6 +746,8 @@ const DASHBOARD_DOCUMENT = `<!DOCTYPE html>
|
|
|
694
746
|
button.classList.toggle('active', button.getAttribute('data-type') === activeShortcut);
|
|
695
747
|
});
|
|
696
748
|
|
|
749
|
+
syncUrl();
|
|
750
|
+
|
|
697
751
|
if (state.page === 'overview') await renderOverview(main);
|
|
698
752
|
if (state.page === 'entries') await renderEntries(main);
|
|
699
753
|
if (state.page === 'monitoring') await renderMonitoring(main);
|
|
@@ -828,7 +882,12 @@ const DASHBOARD_DOCUMENT = `<!DOCTYPE html>
|
|
|
828
882
|
const action = target.getAttribute('data-action');
|
|
829
883
|
if (action === 'go-page') { setPage(String(target.getAttribute('data-page') || 'overview')); return; }
|
|
830
884
|
if (action === 'type-shortcut') { setTypeShortcut(String(target.getAttribute('data-type') || '')); return; }
|
|
831
|
-
if (action === 'filter-tag') {
|
|
885
|
+
if (action === 'filter-tag') {
|
|
886
|
+
if (target instanceof HTMLAnchorElement && (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey || event.button !== 0)) return;
|
|
887
|
+
event.preventDefault();
|
|
888
|
+
filterByTag(String(target.getAttribute('data-tag') || ''));
|
|
889
|
+
return;
|
|
890
|
+
}
|
|
832
891
|
if (action === 'detail-tab') { state = { ...state, detailTab: String(target.getAttribute('data-tab') || 'summary') }; render(); return; }
|
|
833
892
|
if (action === 'clear-all') { clearAll(); return; }
|
|
834
893
|
if (action === 'show-detail') { showDetail(String(target.getAttribute('data-uuid') || '')); return; }
|
package/src/index.ts
CHANGED
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
* import '@zintrust/trace/register';
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
import { ExceptionWatcher as ExceptionWatcherApi } from './watchers/ExceptionWatcher';
|
|
10
|
+
|
|
9
11
|
// ---------------------------------------------------------------------------
|
|
10
12
|
// Config
|
|
11
13
|
// ---------------------------------------------------------------------------
|
|
@@ -16,6 +18,7 @@ export { TraceConfig } from './config';
|
|
|
16
18
|
// ---------------------------------------------------------------------------
|
|
17
19
|
export { TraceStorage } from './storage';
|
|
18
20
|
export type { ITraceStorage } from './storage';
|
|
21
|
+
export { TraceContentRedaction } from './storage/TraceContentRedaction';
|
|
19
22
|
|
|
20
23
|
// ---------------------------------------------------------------------------
|
|
21
24
|
// Context
|
|
@@ -52,6 +55,13 @@ export { RedisWatcher } from './watchers/RedisWatcher';
|
|
|
52
55
|
export { ScheduleWatcher } from './watchers/ScheduleWatcher';
|
|
53
56
|
export { ViewWatcher } from './watchers/ViewWatcher';
|
|
54
57
|
|
|
58
|
+
export const captureTraceException = (
|
|
59
|
+
error: unknown,
|
|
60
|
+
context?: { batchId?: string; hostname?: string; path?: string; userId?: string }
|
|
61
|
+
): void => {
|
|
62
|
+
ExceptionWatcherApi.capture(error, context);
|
|
63
|
+
};
|
|
64
|
+
|
|
55
65
|
// ---------------------------------------------------------------------------
|
|
56
66
|
// Types
|
|
57
67
|
// ---------------------------------------------------------------------------
|
package/src/register.ts
CHANGED
|
@@ -22,7 +22,10 @@
|
|
|
22
22
|
import { TraceConfig } from './config';
|
|
23
23
|
import { TraceContext } from './context';
|
|
24
24
|
import { TraceStorage } from './storage';
|
|
25
|
-
import
|
|
25
|
+
import { TraceContentRedaction } from './storage/TraceContentRedaction';
|
|
26
|
+
import { TraceEntryFiltering } from './storage/TraceEntryFiltering';
|
|
27
|
+
import { TraceWriteDiagnostics } from './storage/TraceWriteDiagnostics';
|
|
28
|
+
import type { ITraceWatcherConfig, TraceConfigOverrides } from './types';
|
|
26
29
|
|
|
27
30
|
export type {}; // side-effect ESM module
|
|
28
31
|
|
|
@@ -58,6 +61,15 @@ type CoreApi = {
|
|
|
58
61
|
RequestContext?: {
|
|
59
62
|
current(): unknown;
|
|
60
63
|
};
|
|
64
|
+
Logger?: {
|
|
65
|
+
warn(message: string, context?: Record<string, unknown>): void;
|
|
66
|
+
};
|
|
67
|
+
StartupConfigFile?: {
|
|
68
|
+
Trace?: string;
|
|
69
|
+
};
|
|
70
|
+
StartupConfigFileRegistry?: {
|
|
71
|
+
get<T>(file: string): T | undefined;
|
|
72
|
+
};
|
|
61
73
|
};
|
|
62
74
|
|
|
63
75
|
type GlobalMiddlewareRegistrarState = {
|
|
@@ -101,35 +113,150 @@ const resolveTraceConnectionName = (
|
|
|
101
113
|
return resolveDefaultConnection();
|
|
102
114
|
};
|
|
103
115
|
|
|
116
|
+
const isObjectValue = (value: unknown): value is Record<string, unknown> => {
|
|
117
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const parseEnvList = (rawValue: string): string[] | undefined => {
|
|
121
|
+
const value = rawValue.trim();
|
|
122
|
+
if (value === '') return undefined;
|
|
123
|
+
|
|
124
|
+
if (value.startsWith('[')) {
|
|
125
|
+
try {
|
|
126
|
+
const parsed = JSON.parse(value) as unknown;
|
|
127
|
+
if (Array.isArray(parsed)) {
|
|
128
|
+
return parsed
|
|
129
|
+
.filter((entry): entry is string => typeof entry === 'string')
|
|
130
|
+
.map((entry) => entry.trim())
|
|
131
|
+
.filter((entry) => entry !== '');
|
|
132
|
+
}
|
|
133
|
+
} catch {
|
|
134
|
+
// fall through to CSV parsing
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return value
|
|
139
|
+
.split(',')
|
|
140
|
+
.map((entry) => entry.trim())
|
|
141
|
+
.filter((entry) => entry !== '');
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const resolveTraceStartupOverrides = (core: CoreApi): TraceConfigOverrides | undefined => {
|
|
145
|
+
const traceConfigFile = core.StartupConfigFile?.Trace;
|
|
146
|
+
if (typeof traceConfigFile !== 'string' || traceConfigFile.trim() === '') return undefined;
|
|
147
|
+
|
|
148
|
+
const overrides = core.StartupConfigFileRegistry?.get<unknown>(traceConfigFile);
|
|
149
|
+
return isObjectValue(overrides) ? (overrides as TraceConfigOverrides) : undefined;
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const buildTraceRedactionOverrides = (input: {
|
|
153
|
+
startupOverrides?: TraceConfigOverrides;
|
|
154
|
+
redactionBody?: string[];
|
|
155
|
+
redactionHeaders?: string[];
|
|
156
|
+
redactionKeys?: string[];
|
|
157
|
+
redactionQuery?: string[];
|
|
158
|
+
}): TraceConfigOverrides['redaction'] | undefined => {
|
|
159
|
+
const redaction: Partial<NonNullable<TraceConfigOverrides['redaction']>> = {
|
|
160
|
+
...(isObjectValue(input.startupOverrides?.redaction) ? input.startupOverrides?.redaction : {}),
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
if (input.redactionKeys === undefined) {
|
|
164
|
+
// no-op
|
|
165
|
+
} else {
|
|
166
|
+
redaction.keys = input.redactionKeys;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (input.redactionHeaders === undefined) {
|
|
170
|
+
// no-op
|
|
171
|
+
} else {
|
|
172
|
+
redaction.headers = input.redactionHeaders;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (input.redactionBody === undefined) {
|
|
176
|
+
// no-op
|
|
177
|
+
} else {
|
|
178
|
+
redaction.body = input.redactionBody;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (input.redactionQuery === undefined) {
|
|
182
|
+
// no-op
|
|
183
|
+
} else {
|
|
184
|
+
redaction.query = input.redactionQuery;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return Object.keys(redaction).length > 0
|
|
188
|
+
? (redaction as NonNullable<TraceConfigOverrides['redaction']>)
|
|
189
|
+
: undefined;
|
|
190
|
+
};
|
|
191
|
+
|
|
104
192
|
const core = (await importCore()) as CoreApi;
|
|
105
193
|
const Env = core.Env;
|
|
194
|
+
const startupOverrides = resolveTraceStartupOverrides(core);
|
|
106
195
|
|
|
107
196
|
if (!traceAlreadyInitialized && Env) {
|
|
108
|
-
const enabled = Env.getBool('TRACE_ENABLED', false);
|
|
197
|
+
const enabled = startupOverrides?.enabled === true || Env.getBool('TRACE_ENABLED', false);
|
|
109
198
|
|
|
110
199
|
if (enabled) {
|
|
111
|
-
const
|
|
112
|
-
const
|
|
113
|
-
const
|
|
114
|
-
const
|
|
200
|
+
const connectionRaw = Env.get('TRACE_DB_CONNECTION', '').trim();
|
|
201
|
+
const pruneAfterHoursRaw = Env.get('TRACE_PRUNE_HOURS', '').trim();
|
|
202
|
+
const slowQueryThresholdRaw = Env.get('TRACE_SLOW_QUERY_MS', '').trim();
|
|
203
|
+
const logMinLevelRaw = Env.get('TRACE_LOG_LEVEL', '').trim();
|
|
204
|
+
const redactionKeys = parseEnvList(Env.get('TRACE_REDACT_KEYS', ''));
|
|
205
|
+
const redactionHeaders = parseEnvList(Env.get('TRACE_REDACT_HEADERS', ''));
|
|
206
|
+
const redactionBody = parseEnvList(Env.get('TRACE_REDACT_BODY', ''));
|
|
207
|
+
const redactionQuery = parseEnvList(Env.get('TRACE_REDACT_QUERY', ''));
|
|
208
|
+
|
|
209
|
+
const connection = connectionRaw === '' ? startupOverrides?.connection : connectionRaw;
|
|
210
|
+
const pruneAfterHours =
|
|
211
|
+
pruneAfterHoursRaw === ''
|
|
212
|
+
? startupOverrides?.pruneAfterHours
|
|
213
|
+
: Number.parseInt(pruneAfterHoursRaw, 10);
|
|
214
|
+
const slowQueryThreshold =
|
|
215
|
+
slowQueryThresholdRaw === ''
|
|
216
|
+
? startupOverrides?.slowQueryThreshold
|
|
217
|
+
: Number.parseInt(slowQueryThresholdRaw, 10);
|
|
218
|
+
const logMinLevel = (logMinLevelRaw === '' ? startupOverrides?.logMinLevel : logMinLevelRaw) as
|
|
115
219
|
| 'debug'
|
|
116
220
|
| 'info'
|
|
117
221
|
| 'warn'
|
|
118
222
|
| 'error'
|
|
119
223
|
| 'fatal';
|
|
224
|
+
const redaction = buildTraceRedactionOverrides({
|
|
225
|
+
startupOverrides,
|
|
226
|
+
redactionBody,
|
|
227
|
+
redactionHeaders,
|
|
228
|
+
redactionKeys,
|
|
229
|
+
redactionQuery,
|
|
230
|
+
});
|
|
120
231
|
|
|
121
232
|
const config = TraceConfig.merge({
|
|
233
|
+
...startupOverrides,
|
|
122
234
|
enabled,
|
|
123
235
|
connection,
|
|
124
|
-
pruneAfterHours
|
|
125
|
-
|
|
236
|
+
...(typeof pruneAfterHours === 'number' && Number.isFinite(pruneAfterHours)
|
|
237
|
+
? { pruneAfterHours }
|
|
238
|
+
: {}),
|
|
239
|
+
...(typeof slowQueryThreshold === 'number' && Number.isFinite(slowQueryThreshold)
|
|
240
|
+
? { slowQueryThreshold }
|
|
241
|
+
: {}),
|
|
126
242
|
logMinLevel,
|
|
243
|
+
...(redaction === undefined ? {} : { redaction }),
|
|
127
244
|
});
|
|
128
245
|
|
|
129
|
-
const
|
|
246
|
+
const resolvedConnectionName = resolveTraceConnectionName(Env, config.connection);
|
|
247
|
+
const db = core.useDatabase?.(undefined, resolvedConnectionName);
|
|
130
248
|
|
|
131
249
|
if (db) {
|
|
132
|
-
const storage =
|
|
250
|
+
const storage = TraceWriteDiagnostics.wrapStorage(
|
|
251
|
+
TraceContentRedaction.wrapStorage(
|
|
252
|
+
TraceEntryFiltering.wrapStorage(TraceStorage.resolveStorage(db), config),
|
|
253
|
+
config.redaction
|
|
254
|
+
),
|
|
255
|
+
{
|
|
256
|
+
connectionName: resolvedConnectionName,
|
|
257
|
+
logger: core.Logger,
|
|
258
|
+
}
|
|
259
|
+
);
|
|
133
260
|
|
|
134
261
|
if (core.RequestContext) {
|
|
135
262
|
TraceContext.setRequestContextImpl(
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { ITraceEntry, ITraceStorage, RedactionConfig } from '../types';
|
|
2
|
+
import { redactUnknown } from '../utils/redact';
|
|
3
|
+
|
|
4
|
+
const collectRedactionFields = (redaction: RedactionConfig): string[] => {
|
|
5
|
+
return [
|
|
6
|
+
...new Set([...redaction.keys, ...redaction.headers, ...redaction.body, ...redaction.query]),
|
|
7
|
+
];
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const redactTraceEntry = (entry: ITraceEntry, redaction: RedactionConfig): ITraceEntry => {
|
|
11
|
+
return {
|
|
12
|
+
...entry,
|
|
13
|
+
content: redactUnknown(entry.content, collectRedactionFields(redaction)),
|
|
14
|
+
};
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const redactTracePatch = (
|
|
18
|
+
patch: Partial<Pick<ITraceEntry, 'content' | 'isLatest'>>,
|
|
19
|
+
redaction: RedactionConfig
|
|
20
|
+
): Partial<Pick<ITraceEntry, 'content' | 'isLatest'>> => {
|
|
21
|
+
if (patch.content === undefined) return patch;
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
...patch,
|
|
25
|
+
content: redactUnknown(patch.content, collectRedactionFields(redaction)),
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export const TraceContentRedaction = Object.freeze({
|
|
30
|
+
wrapStorage(storage: ITraceStorage, redaction: RedactionConfig): ITraceStorage {
|
|
31
|
+
return Object.freeze({
|
|
32
|
+
...storage,
|
|
33
|
+
writeEntry: async (entry: ITraceEntry): Promise<void> => {
|
|
34
|
+
await storage.writeEntry(redactTraceEntry(entry, redaction));
|
|
35
|
+
},
|
|
36
|
+
updateEntry: async (
|
|
37
|
+
uuid: string,
|
|
38
|
+
patch: Partial<Pick<ITraceEntry, 'content' | 'isLatest'>>
|
|
39
|
+
): Promise<void> => {
|
|
40
|
+
await storage.updateEntry(uuid, redactTracePatch(patch, redaction));
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
},
|
|
44
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ITraceConfig, ITraceEntry, ITraceStorage } from '../types';
|
|
2
|
+
import { TraceEntryFilter } from '../utils/entryFilter';
|
|
3
|
+
|
|
4
|
+
export const TraceEntryFiltering = Object.freeze({
|
|
5
|
+
wrapStorage(storage: ITraceStorage, config: ITraceConfig): ITraceStorage {
|
|
6
|
+
return Object.freeze({
|
|
7
|
+
...storage,
|
|
8
|
+
async writeEntry(entry: ITraceEntry) {
|
|
9
|
+
if (!TraceEntryFilter.shouldCapture(entry, config)) return;
|
|
10
|
+
await storage.writeEntry(entry);
|
|
11
|
+
},
|
|
12
|
+
});
|
|
13
|
+
},
|
|
14
|
+
});
|
|
@@ -26,6 +26,54 @@ type EntryRow = {
|
|
|
26
26
|
|
|
27
27
|
type TagRow = { entry_uuid: string; tag: string };
|
|
28
28
|
|
|
29
|
+
type DatabaseWithDriver = IDatabase & {
|
|
30
|
+
getType?: () => string;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const buildIgnoreInsert = (
|
|
34
|
+
db: IDatabase,
|
|
35
|
+
table: string,
|
|
36
|
+
columns: string[],
|
|
37
|
+
conflictColumns: string[]
|
|
38
|
+
): string => {
|
|
39
|
+
const columnList = columns.join(', ');
|
|
40
|
+
const placeholders = columns.map(() => '?').join(', ');
|
|
41
|
+
const driver = (db as DatabaseWithDriver).getType?.() ?? 'sqlite';
|
|
42
|
+
|
|
43
|
+
if (driver === 'sqlite' || driver === 'd1' || driver === 'd1-remote') {
|
|
44
|
+
return `INSERT OR IGNORE INTO ${table} (${columnList}) VALUES (${placeholders})`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (driver === 'mysql') {
|
|
48
|
+
return `INSERT IGNORE INTO ${table} (${columnList}) VALUES (${placeholders})`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (driver === 'postgresql') {
|
|
52
|
+
return `INSERT INTO ${table} (${columnList}) VALUES (${placeholders}) ON CONFLICT (${conflictColumns.join(', ')}) DO NOTHING`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (driver === 'sqlserver') {
|
|
56
|
+
const sourceColumns = columns.map((_, index) => `v${index + 1}`);
|
|
57
|
+
const selectClause = sourceColumns.map((name) => `? AS ${name}`).join(', ');
|
|
58
|
+
const conflictClause = conflictColumns
|
|
59
|
+
.map((column) => `target.${column} = source.${column}`)
|
|
60
|
+
.join(' AND ');
|
|
61
|
+
const insertValues = columns.map((column) => `source.${column}`).join(', ');
|
|
62
|
+
const sourceProjection = columns
|
|
63
|
+
.map((column, index) => `${sourceColumns[index]} AS ${column}`)
|
|
64
|
+
.join(', ');
|
|
65
|
+
|
|
66
|
+
return [
|
|
67
|
+
`MERGE INTO ${table} WITH (HOLDLOCK) AS target`,
|
|
68
|
+
`USING (SELECT ${sourceProjection} FROM (SELECT ${selectClause}) seed) AS source`,
|
|
69
|
+
`ON ${conflictClause}`,
|
|
70
|
+
`WHEN NOT MATCHED THEN INSERT (${columnList}) VALUES (${insertValues});`,
|
|
71
|
+
].join(' ');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return `INSERT INTO ${table} (${columnList}) VALUES (${placeholders})`;
|
|
75
|
+
};
|
|
76
|
+
|
|
29
77
|
const rowToEntry = (row: EntryRow, tags: string[]): ITraceEntry => ({
|
|
30
78
|
uuid: row.uuid,
|
|
31
79
|
batchId: row.batch_id,
|
|
@@ -40,12 +88,11 @@ const rowToEntry = (row: EntryRow, tags: string[]): ITraceEntry => ({
|
|
|
40
88
|
const insertTags = async (db: IDatabase, uuid: string, tags: string[]): Promise<void> => {
|
|
41
89
|
if (tags.length === 0) return;
|
|
42
90
|
|
|
91
|
+
const sql = buildIgnoreInsert(db, TABLE_TAGS, ['entry_uuid', 'tag'], ['entry_uuid', 'tag']);
|
|
92
|
+
|
|
43
93
|
await Promise.all(
|
|
44
94
|
tags.map(async (tag) => {
|
|
45
|
-
await db.execute(
|
|
46
|
-
uuid,
|
|
47
|
-
tag,
|
|
48
|
-
]);
|
|
95
|
+
await db.execute(sql, [uuid, tag]);
|
|
49
96
|
})
|
|
50
97
|
);
|
|
51
98
|
};
|
|
@@ -258,7 +305,7 @@ const createStorage = (db: IDatabase): ITraceStorage => {
|
|
|
258
305
|
};
|
|
259
306
|
|
|
260
307
|
const addMonitoring = async (tag: string): Promise<void> => {
|
|
261
|
-
await db.execute(
|
|
308
|
+
await db.execute(buildIgnoreInsert(db, TABLE_MONITORING, ['tag'], ['tag']), [tag]);
|
|
262
309
|
};
|
|
263
310
|
|
|
264
311
|
const removeMonitoring = async (tag: string): Promise<void> => {
|