@zintrust/trace 0.4.92 → 0.4.95
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/dist/build-manifest.json +41 -21
- package/dist/dashboard/ui.js +33 -13
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/register.js +2 -1
- package/dist/storage/DebuggerStorage.d.ts +13 -0
- package/dist/storage/DebuggerStorage.js +195 -0
- package/dist/storage/TraceContentBudget.d.ts +4 -0
- package/dist/storage/TraceContentBudget.js +114 -0
- package/dist/watchers/HttpWatcher.js +7 -1
- package/dist/watchers/MiddlewareWatcher.js +5 -0
- package/dist/watchers/ModelWatcher.js +5 -0
- package/package.json +2 -2
- package/src/dashboard/ui.ts +33 -13
- package/src/index.ts +1 -0
- package/src/register.ts +6 -3
- package/src/storage/TraceContentBudget.ts +145 -0
- package/src/watchers/HttpWatcher.ts +8 -1
- package/src/watchers/MiddlewareWatcher.ts +9 -0
- package/src/watchers/ModelWatcher.ts +9 -0
package/dist/build-manifest.json
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zintrust/trace",
|
|
3
|
-
"version": "0.4.
|
|
4
|
-
"buildDate": "2026-04-
|
|
3
|
+
"version": "0.4.95",
|
|
4
|
+
"buildDate": "2026-04-11T17:50:25.235Z",
|
|
5
5
|
"buildEnvironment": {
|
|
6
|
-
"node": "
|
|
7
|
-
"platform": "
|
|
8
|
-
"arch": "
|
|
6
|
+
"node": "v22.22.1",
|
|
7
|
+
"platform": "darwin",
|
|
8
|
+
"arch": "arm64"
|
|
9
9
|
},
|
|
10
10
|
"git": {
|
|
11
|
-
"commit": "
|
|
12
|
-
"branch": "
|
|
11
|
+
"commit": "8f4c9f2f",
|
|
12
|
+
"branch": "release"
|
|
13
13
|
},
|
|
14
14
|
"package": {
|
|
15
15
|
"engines": {
|
|
@@ -21,6 +21,10 @@
|
|
|
21
21
|
]
|
|
22
22
|
},
|
|
23
23
|
"files": {
|
|
24
|
+
"build-manifest.json": {
|
|
25
|
+
"size": 14440,
|
|
26
|
+
"sha256": "cbd454ac88211f4242ab1d62b641b064452d5532792e5ecae7cbe2a49f4d652a"
|
|
27
|
+
},
|
|
24
28
|
"cli-register.d.ts": {
|
|
25
29
|
"size": 255,
|
|
26
30
|
"sha256": "da8d689fe5ef32e97e755f28017e4d3cb1aa63489073a71907ea41ad5761ede9"
|
|
@@ -66,16 +70,16 @@
|
|
|
66
70
|
"sha256": "4862b41e0477f01afa0dbb446d4553b65c22ed774cd1e2db3489059ced392f94"
|
|
67
71
|
},
|
|
68
72
|
"dashboard/ui.js": {
|
|
69
|
-
"size":
|
|
70
|
-
"sha256": "
|
|
73
|
+
"size": 74630,
|
|
74
|
+
"sha256": "925ab75a6176dc4639d0772eda904f1401d73e866f0f0882450d9fd4e253ad89"
|
|
71
75
|
},
|
|
72
76
|
"index.d.ts": {
|
|
73
|
-
"size":
|
|
74
|
-
"sha256": "
|
|
77
|
+
"size": 2537,
|
|
78
|
+
"sha256": "1707d26322dbad17f6bf85938ae6fe2477e84c7fed3333760ce8d6eadfaaffd2"
|
|
75
79
|
},
|
|
76
80
|
"index.js": {
|
|
77
|
-
"size":
|
|
78
|
-
"sha256": "
|
|
81
|
+
"size": 3325,
|
|
82
|
+
"sha256": "3715aa0dca22638b15c2ec534e2463d7c82268e7648f75571c591d07edc9a0de"
|
|
79
83
|
},
|
|
80
84
|
"migrations/20260331000001_create_zin_trace_entries_table.d.ts": {
|
|
81
85
|
"size": 304,
|
|
@@ -130,8 +134,24 @@
|
|
|
130
134
|
"sha256": "71d366165dd36f1675aa253a76262b226fb6c62e5ab632746b8aea61c0c625fc"
|
|
131
135
|
},
|
|
132
136
|
"register.js": {
|
|
133
|
-
"size":
|
|
134
|
-
"sha256": "
|
|
137
|
+
"size": 14327,
|
|
138
|
+
"sha256": "efc9bb131b9eef7e81a6f85ea3338fd8ac74257f794aa0fc7c76efed636f99d8"
|
|
139
|
+
},
|
|
140
|
+
"storage/DebuggerStorage.d.ts": {
|
|
141
|
+
"size": 517,
|
|
142
|
+
"sha256": "c9c215aaa414f7b0c1fec6c82b054fc52bdf97af58f96f35c7f96672fb859c31"
|
|
143
|
+
},
|
|
144
|
+
"storage/DebuggerStorage.js": {
|
|
145
|
+
"size": 7442,
|
|
146
|
+
"sha256": "5ecce0fcfcf695df587a7b90a7a5c7efd2e64ad13c9f2d104b392f89f34f0dc4"
|
|
147
|
+
},
|
|
148
|
+
"storage/TraceContentBudget.d.ts": {
|
|
149
|
+
"size": 159,
|
|
150
|
+
"sha256": "d899a615e6cf2a5eea51f6200347cb81fbaae11979b801859f57135083aaf85b"
|
|
151
|
+
},
|
|
152
|
+
"storage/TraceContentBudget.js": {
|
|
153
|
+
"size": 4022,
|
|
154
|
+
"sha256": "4b1d4f0ad7da15caeaa1f9fe8b4edcd3000b0000c5bdc270601cd6db2eedce2e"
|
|
135
155
|
},
|
|
136
156
|
"storage/TraceContentRedaction.d.ts": {
|
|
137
157
|
"size": 207,
|
|
@@ -314,8 +334,8 @@
|
|
|
314
334
|
"sha256": "ce9a95a670f755193fd74ce721dbfa4b30f20c879a6566ebb35229b3b2435429"
|
|
315
335
|
},
|
|
316
336
|
"watchers/HttpWatcher.js": {
|
|
317
|
-
"size":
|
|
318
|
-
"sha256": "
|
|
337
|
+
"size": 6165,
|
|
338
|
+
"sha256": "aa18bc520f5f17a73aa12b71e57d825f5fc75cddb909ebdb222c0ce03291685d"
|
|
319
339
|
},
|
|
320
340
|
"watchers/JobWatcher.d.ts": {
|
|
321
341
|
"size": 441,
|
|
@@ -346,16 +366,16 @@
|
|
|
346
366
|
"sha256": "5a28b472c835bd0f79ec9d3670516544bc5b6da9f1f1bed5159be0cba39304a1"
|
|
347
367
|
},
|
|
348
368
|
"watchers/MiddlewareWatcher.js": {
|
|
349
|
-
"size":
|
|
350
|
-
"sha256": "
|
|
369
|
+
"size": 1399,
|
|
370
|
+
"sha256": "54c7c2c7b1cbeaf3987a7e888b3494c12e8848bfb9d07be8be35934bd6a7c63d"
|
|
351
371
|
},
|
|
352
372
|
"watchers/ModelWatcher.d.ts": {
|
|
353
373
|
"size": 285,
|
|
354
374
|
"sha256": "4c05112af855a92b3f3f97c6b61bf1b07444bcb7d5a17f3e0ae19da4a85faf1c"
|
|
355
375
|
},
|
|
356
376
|
"watchers/ModelWatcher.js": {
|
|
357
|
-
"size":
|
|
358
|
-
"sha256": "
|
|
377
|
+
"size": 1381,
|
|
378
|
+
"sha256": "0ead883fad311762f8310a3748d4859d2913ad92c8fd0db48b7070fe4b5e9d70"
|
|
359
379
|
},
|
|
360
380
|
"watchers/NotificationWatcher.d.ts": {
|
|
361
381
|
"size": 274,
|
package/dist/dashboard/ui.js
CHANGED
|
@@ -215,6 +215,19 @@ const DASHBOARD_DOCUMENT = String.raw `<!DOCTYPE html>
|
|
|
215
215
|
return ' method-other';
|
|
216
216
|
};
|
|
217
217
|
|
|
218
|
+
const normalizeMethodLabel = (value) => {
|
|
219
|
+
const method = String(value || '').trim().toUpperCase();
|
|
220
|
+
if (method === '') return 'Request';
|
|
221
|
+
return method.charAt(0) + method.slice(1).toLowerCase();
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
const entryTypeLabel = (entry) => {
|
|
225
|
+
if (entry && entry.type === 'request') {
|
|
226
|
+
return normalizeMethodLabel(entry.content && entry.content.method);
|
|
227
|
+
}
|
|
228
|
+
return String(entry && entry.type || '');
|
|
229
|
+
};
|
|
230
|
+
|
|
218
231
|
const typeClass = (entryOrType, maybeEntry) => {
|
|
219
232
|
const entry = maybeEntry || (typeof entryOrType === 'object' ? entryOrType : null);
|
|
220
233
|
const type = entry && entry.type ? entry.type : entryOrType;
|
|
@@ -427,7 +440,7 @@ const DASHBOARD_DOCUMENT = String.raw `<!DOCTYPE html>
|
|
|
427
440
|
|
|
428
441
|
const entrySummaryText = (entry) => {
|
|
429
442
|
const content = entry && entry.content ? entry.content : {};
|
|
430
|
-
if (entry.type === 'request') return [content.responseStatus || '', content.
|
|
443
|
+
if (entry.type === 'request') return [content.responseStatus || '', content.uri || ''].filter(Boolean).join(' ');
|
|
431
444
|
if (entry.type === 'query') return String(content.sql || '').slice(0, 160);
|
|
432
445
|
if (entry.type === 'exception') return [content.class || '', content.message || ''].filter(Boolean).join(': ');
|
|
433
446
|
if (entry.type === 'log') return '[' + String(content.level || 'log') + '] ' + String(content.message || '').slice(0, 160);
|
|
@@ -624,8 +637,8 @@ const DASHBOARD_DOCUMENT = String.raw `<!DOCTYPE html>
|
|
|
624
637
|
const renderTraceItems = (entries, options = {}) => {
|
|
625
638
|
if (entries.length === 0) return '<p class="trace-note">No related entries captured.</p>';
|
|
626
639
|
|
|
627
|
-
const collapsible = options.collapsible
|
|
628
|
-
const isInitiallyOpen = options.collapsed
|
|
640
|
+
const collapsible = options.collapsible !== false;
|
|
641
|
+
const isInitiallyOpen = options.collapsed === false;
|
|
629
642
|
|
|
630
643
|
return '<div class="trace-panel">' + entries.map((entry) => {
|
|
631
644
|
if (collapsible) {
|
|
@@ -633,7 +646,7 @@ const DASHBOARD_DOCUMENT = String.raw `<!DOCTYPE html>
|
|
|
633
646
|
'<details class="trace-item trace-disclosure"' + (isInitiallyOpen ? ' open' : '') + '>',
|
|
634
647
|
'<summary class="trace-item-head trace-summary">',
|
|
635
648
|
'<span class="trace-summary-main">',
|
|
636
|
-
'<span><span class="' + typeClass(entry) + '">' + escapeHtml(entry
|
|
649
|
+
'<span><span class="' + typeClass(entry) + '">' + escapeHtml(entryTypeLabel(entry)) + '</span></span>',
|
|
637
650
|
'<span class="trace-summary-copy">' + entrySummaryInlineHtml(entry) + '</span>',
|
|
638
651
|
'</span>',
|
|
639
652
|
'<span class="activity-head">' + durationHtml(entry) + '<span class="activity-time">' + escapeHtml(timeSince(entry.createdAt)) + '</span></span>',
|
|
@@ -650,7 +663,7 @@ const DASHBOARD_DOCUMENT = String.raw `<!DOCTYPE html>
|
|
|
650
663
|
'<section class="trace-item">',
|
|
651
664
|
'<div class="trace-item-head">',
|
|
652
665
|
'<div>',
|
|
653
|
-
'<span class="' + typeClass(entry) + '">' + escapeHtml(entry
|
|
666
|
+
'<span class="' + typeClass(entry) + '">' + escapeHtml(entryTypeLabel(entry)) + '</span>',
|
|
654
667
|
'</div>',
|
|
655
668
|
'<div class="activity-head">' + durationHtml(entry) + '<span class="activity-time">' + escapeHtml(timeSince(entry.createdAt)) + '</span></div>',
|
|
656
669
|
'</div>',
|
|
@@ -673,14 +686,16 @@ const DASHBOARD_DOCUMENT = String.raw `<!DOCTYPE html>
|
|
|
673
686
|
{ id: 'headers', label: 'Headers' },
|
|
674
687
|
{ id: 'response', label: 'Response' },
|
|
675
688
|
{ id: 'queries', label: 'Queries', count: batchEntriesByType('query').length },
|
|
689
|
+
{ id: 'middleware', label: 'Middleware', count: batchEntriesByType('middleware').length },
|
|
690
|
+
{ id: 'models', label: 'Models', count: batchEntriesByType('model').length },
|
|
676
691
|
{ id: 'logs', label: 'Logs', count: batchEntriesByType('log').length },
|
|
677
692
|
{ id: 'exceptions', label: 'Exceptions', count: batchEntriesByType('exception').length },
|
|
678
693
|
{ id: 'http', label: 'HTTP', count: batchEntriesByType('client_request').length },
|
|
679
694
|
{ id: 'cache', label: 'Cache', count: batchEntriesByType('cache').length },
|
|
680
|
-
{ id: 'other', label: 'Other', count: batchEntries().filter((item) => !['request','query','log','exception','client_request','cache'].includes(item.type)).length }
|
|
695
|
+
{ id: 'other', label: 'Other', count: batchEntries().filter((item) => !['request','query','middleware','model','log','exception','client_request','cache'].includes(item.type)).length }
|
|
681
696
|
];
|
|
682
697
|
const currentTab = traceTabs.some((tab) => tab.id === state.detailTab) ? state.detailTab : 'summary';
|
|
683
|
-
const otherEntries = batchEntries().filter((item) => !['request','query','log','exception','client_request','cache'].includes(item.type));
|
|
698
|
+
const otherEntries = batchEntries().filter((item) => !['request','query','middleware','model','log','exception','client_request','cache'].includes(item.type));
|
|
684
699
|
const panels = {
|
|
685
700
|
summary: [
|
|
686
701
|
'<div class="detail-grid">',
|
|
@@ -699,12 +714,17 @@ const DASHBOARD_DOCUMENT = String.raw `<!DOCTYPE html>
|
|
|
699
714
|
renderMetricBox('Tags', [
|
|
700
715
|
{ label: 'Values', value: tagsHtml(entry.tags) || '<span class="activity-time">-</span>' }
|
|
701
716
|
]),
|
|
717
|
+
renderMetricBox('Route middleware', [
|
|
718
|
+
{ label: 'Attached', value: escapeHtml(Array.isArray(content.middleware) && content.middleware.length > 0 ? content.middleware.join(', ') : 'None') }
|
|
719
|
+
]),
|
|
702
720
|
'</div>'
|
|
703
721
|
].join(''),
|
|
704
722
|
payload: detailJson(content.payload || {}, 'Payload Json'),
|
|
705
723
|
headers: '<div class="detail-stack">' + detailJson(content.headers || {}, 'Request Header Json') + detailJson(content.responseHeaders || {}, 'Response Header Json') + '</div>',
|
|
706
724
|
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>',
|
|
707
|
-
queries: renderTraceItems(batchEntriesByType('query')
|
|
725
|
+
queries: renderTraceItems(batchEntriesByType('query')),
|
|
726
|
+
middleware: renderTraceItems(batchEntriesByType('middleware')),
|
|
727
|
+
models: renderTraceItems(batchEntriesByType('model')),
|
|
708
728
|
logs: renderTraceItems(batchEntriesByType('log')),
|
|
709
729
|
exceptions: renderTraceItems(batchEntriesByType('exception')),
|
|
710
730
|
http: renderTraceItems(batchEntriesByType('client_request')),
|
|
@@ -716,8 +736,8 @@ const DASHBOARD_DOCUMENT = String.raw `<!DOCTYPE html>
|
|
|
716
736
|
'<span class="back-link" data-action="close-detail"><- Back to entries</span>',
|
|
717
737
|
'<section class="panel detail-card">',
|
|
718
738
|
'<div>' + (entry.type === 'request'
|
|
719
|
-
? '<span class="' + typeClass(entry) + '">' + escapeHtml(entry
|
|
720
|
-
: '<span class="' + typeClass(entry) + '">' + escapeHtml(entry
|
|
739
|
+
? '<span class="' + typeClass(entry) + '">' + escapeHtml(entryTypeLabel(entry)) + '</span> <span class="' + typeClass(entry) + '">' + escapeHtml(content.responseStatus || '') + '</span> <span class="mono">' + escapeHtml(content.uri || '') + '</span> ' + tagsHtml(entry.tags)
|
|
740
|
+
: '<span class="' + typeClass(entry) + '">' + escapeHtml(entryTypeLabel(entry)) + '</span> ' + tagsHtml(entry.tags)) + '</div>',
|
|
721
741
|
'<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>',
|
|
722
742
|
'<div class="trace-tabs">',
|
|
723
743
|
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(''),
|
|
@@ -743,10 +763,10 @@ const DASHBOARD_DOCUMENT = String.raw `<!DOCTYPE html>
|
|
|
743
763
|
const recentRows = recent.data || [];
|
|
744
764
|
const recentTable = recentRows.length === 0
|
|
745
765
|
? '<div class="empty">No trace entries recorded.</div>'
|
|
746
|
-
: '<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
|
|
766
|
+
: '<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(entryTypeLabel(entry)) + '</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>';
|
|
747
767
|
const activityList = recentRows.length === 0
|
|
748
768
|
? '<div class="empty">No recent activity.</div>'
|
|
749
|
-
: '<ul class="activity-list">' + recentRows.slice(0, 5).map((entry) => '<li class="activity-item"><div class="activity-head"><span class="' + typeClass(entry) + '">' + escapeHtml(entry
|
|
769
|
+
: '<ul class="activity-list">' + recentRows.slice(0, 5).map((entry) => '<li class="activity-item"><div class="activity-head"><span class="' + typeClass(entry) + '">' + escapeHtml(entryTypeLabel(entry)) + '</span>' + durationHtml(entry) + '<span class="activity-time">' + escapeHtml(timeSince(entry.createdAt)) + '</span></div><div class="activity-summary">' + escapeHtml(entrySummaryText(entry)) + '</div></li>').join('') + '</ul>';
|
|
750
770
|
|
|
751
771
|
main.innerHTML = [
|
|
752
772
|
statsCardsHtml(stats),
|
|
@@ -790,7 +810,7 @@ const DASHBOARD_DOCUMENT = String.raw `<!DOCTYPE html>
|
|
|
790
810
|
const total = Number(response.total || 0);
|
|
791
811
|
const perPage = Number(response.perPage || 50);
|
|
792
812
|
const totalPages = Math.max(1, Math.ceil(total / perPage));
|
|
793
|
-
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
|
|
813
|
+
const rows = data.map((entry) => '<tr class="row-button" data-action="show-detail" data-uuid="' + escapeHtml(entry.uuid) + '"><td><span class="' + typeClass(entry) + '">' + escapeHtml(entryTypeLabel(entry)) + '</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('');
|
|
794
814
|
|
|
795
815
|
main.innerHTML = [
|
|
796
816
|
'<section class="panel">',
|
package/dist/index.d.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
export { TraceConfig } from './config';
|
|
9
9
|
export { TraceStorage } from './storage';
|
|
10
10
|
export type { ITraceStorage } from './storage';
|
|
11
|
+
export { TraceContentBudget } from './storage/TraceContentBudget';
|
|
11
12
|
export { TraceContentRedaction } from './storage/TraceContentRedaction';
|
|
12
13
|
export { TraceContext } from './context';
|
|
13
14
|
export { registerTraceDashboard, registerTraceRoutes } from './dashboard/routes';
|
package/dist/index.js
CHANGED
|
@@ -14,6 +14,7 @@ export { TraceConfig } from './config.js';
|
|
|
14
14
|
// Storage
|
|
15
15
|
// ---------------------------------------------------------------------------
|
|
16
16
|
export { TraceStorage } from './storage/index.js';
|
|
17
|
+
export { TraceContentBudget } from './storage/TraceContentBudget.js';
|
|
17
18
|
export { TraceContentRedaction } from './storage/TraceContentRedaction.js';
|
|
18
19
|
// ---------------------------------------------------------------------------
|
|
19
20
|
// Context
|
package/dist/register.js
CHANGED
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
import { TraceConfig } from './config.js';
|
|
23
23
|
import { TraceContext } from './context.js';
|
|
24
24
|
import { TraceStorage } from './storage/index.js';
|
|
25
|
+
import { TraceContentBudget } from './storage/TraceContentBudget.js';
|
|
25
26
|
import { TraceContentRedaction } from './storage/TraceContentRedaction.js';
|
|
26
27
|
import { TraceEntryFiltering } from './storage/TraceEntryFiltering.js';
|
|
27
28
|
import { TraceWriteDiagnostics } from './storage/TraceWriteDiagnostics.js';
|
|
@@ -256,7 +257,7 @@ if (!traceAlreadyInitialized && Env) {
|
|
|
256
257
|
envKey: 'TRACE_QUERY_CONNECTION',
|
|
257
258
|
});
|
|
258
259
|
await assertTraceStorageReady(core, storageDb, resolvedConnectionName);
|
|
259
|
-
const storage = TraceWriteDiagnostics.wrapStorage(TraceContentRedaction.wrapStorage(TraceEntryFiltering.wrapStorage(TraceStorage.resolveStorage(storageDb), config), config.redaction), {
|
|
260
|
+
const storage = TraceWriteDiagnostics.wrapStorage(TraceContentBudget.wrapStorage(TraceContentRedaction.wrapStorage(TraceEntryFiltering.wrapStorage(TraceStorage.resolveStorage(storageDb), config), config.redaction)), {
|
|
260
261
|
connectionName: resolvedConnectionName,
|
|
261
262
|
logger: core.Logger,
|
|
262
263
|
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TraceStorage — sealed namespace wrapping the D1/SQLite driver.
|
|
3
|
+
* Resolves the correct IDatabase from the app config, then delegates all
|
|
4
|
+
* read/write operations to the trace storage facade.
|
|
5
|
+
*/
|
|
6
|
+
import type { IDatabase } from '@zintrust/core';
|
|
7
|
+
import type { ITraceStorage } from '../types';
|
|
8
|
+
export declare const TraceStorage: Readonly<{
|
|
9
|
+
resolveStorage: (db: IDatabase) => ITraceStorage;
|
|
10
|
+
reset: () => void;
|
|
11
|
+
familyHash: (input: string) => string;
|
|
12
|
+
}>;
|
|
13
|
+
export { type ITraceStorage } from '../types';
|
|
@@ -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,114 @@
|
|
|
1
|
+
const DEFAULT_MAX_ENTRY_BYTES = 64 * 1024;
|
|
2
|
+
const DEFAULT_MAX_STRING_BYTES = 16 * 1024;
|
|
3
|
+
const DEFAULT_MAX_ARRAY_ITEMS = 25;
|
|
4
|
+
const DEFAULT_MAX_OBJECT_ENTRIES = 40;
|
|
5
|
+
const DEFAULT_MAX_DEPTH = 6;
|
|
6
|
+
const DROPPED_FIELD_MESSAGE = '[trace] Value dropped because the field exceeded the trace storage size limit.';
|
|
7
|
+
const COMPACTED_CONTENT_MESSAGE = '[trace] Trace content was compacted because it exceeded the trace storage size limit.';
|
|
8
|
+
const encoder = new TextEncoder();
|
|
9
|
+
const serializedSize = (value) => {
|
|
10
|
+
try {
|
|
11
|
+
return encoder.encode(JSON.stringify(value)).length;
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return Number.MAX_SAFE_INTEGER;
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
const describeValueType = (value) => {
|
|
18
|
+
if (Array.isArray(value))
|
|
19
|
+
return 'array';
|
|
20
|
+
if (value === null)
|
|
21
|
+
return 'null';
|
|
22
|
+
return typeof value;
|
|
23
|
+
};
|
|
24
|
+
const compactValue = (value, depth) => {
|
|
25
|
+
if (depth >= DEFAULT_MAX_DEPTH) {
|
|
26
|
+
return DROPPED_FIELD_MESSAGE;
|
|
27
|
+
}
|
|
28
|
+
if (typeof value === 'string') {
|
|
29
|
+
return serializedSize(value) > DEFAULT_MAX_STRING_BYTES ? DROPPED_FIELD_MESSAGE : value;
|
|
30
|
+
}
|
|
31
|
+
if (Array.isArray(value)) {
|
|
32
|
+
const next = value
|
|
33
|
+
.slice(0, DEFAULT_MAX_ARRAY_ITEMS)
|
|
34
|
+
.map((item) => compactValue(item, depth + 1));
|
|
35
|
+
if (value.length > DEFAULT_MAX_ARRAY_ITEMS) {
|
|
36
|
+
next.push(`[trace] ${String(value.length - DEFAULT_MAX_ARRAY_ITEMS)} additional items were dropped.`);
|
|
37
|
+
}
|
|
38
|
+
return next;
|
|
39
|
+
}
|
|
40
|
+
if (typeof value !== 'object' || value === null) {
|
|
41
|
+
return value;
|
|
42
|
+
}
|
|
43
|
+
const entries = Object.entries(value);
|
|
44
|
+
const compactedEntries = entries
|
|
45
|
+
.slice(0, DEFAULT_MAX_OBJECT_ENTRIES)
|
|
46
|
+
.map(([key, entryValue]) => [key, compactValue(entryValue, depth + 1)]);
|
|
47
|
+
if (entries.length > DEFAULT_MAX_OBJECT_ENTRIES) {
|
|
48
|
+
compactedEntries.push([
|
|
49
|
+
'__traceNotice',
|
|
50
|
+
`[trace] ${String(entries.length - DEFAULT_MAX_OBJECT_ENTRIES)} additional fields were dropped.`,
|
|
51
|
+
]);
|
|
52
|
+
}
|
|
53
|
+
return Object.fromEntries(compactedEntries);
|
|
54
|
+
};
|
|
55
|
+
const compactTopLevelObjectToBudget = (value) => {
|
|
56
|
+
const compacted = {
|
|
57
|
+
...value,
|
|
58
|
+
__traceNotice: COMPACTED_CONTENT_MESSAGE,
|
|
59
|
+
};
|
|
60
|
+
const keysByDescendingSize = Object.keys(compacted)
|
|
61
|
+
.filter((key) => key !== '__traceNotice')
|
|
62
|
+
.sort((left, right) => serializedSize(compacted[right]) - serializedSize(compacted[left]));
|
|
63
|
+
for (const key of keysByDescendingSize) {
|
|
64
|
+
if (serializedSize(compacted) <= DEFAULT_MAX_ENTRY_BYTES)
|
|
65
|
+
break;
|
|
66
|
+
compacted[key] = DROPPED_FIELD_MESSAGE;
|
|
67
|
+
}
|
|
68
|
+
return compacted;
|
|
69
|
+
};
|
|
70
|
+
const fitContentToBudget = (content) => {
|
|
71
|
+
if (serializedSize(content) <= DEFAULT_MAX_ENTRY_BYTES) {
|
|
72
|
+
return content;
|
|
73
|
+
}
|
|
74
|
+
const compacted = compactValue(content, 0);
|
|
75
|
+
if (serializedSize(compacted) <= DEFAULT_MAX_ENTRY_BYTES) {
|
|
76
|
+
return compacted;
|
|
77
|
+
}
|
|
78
|
+
if (typeof compacted === 'object' && compacted !== null && !Array.isArray(compacted)) {
|
|
79
|
+
const topLevelCompacted = compactTopLevelObjectToBudget(compacted);
|
|
80
|
+
if (serializedSize(topLevelCompacted) <= DEFAULT_MAX_ENTRY_BYTES) {
|
|
81
|
+
return topLevelCompacted;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
__traceNotice: COMPACTED_CONTENT_MESSAGE,
|
|
86
|
+
dropped: true,
|
|
87
|
+
valueType: describeValueType(content),
|
|
88
|
+
};
|
|
89
|
+
};
|
|
90
|
+
const fitEntryToBudget = (entry) => ({
|
|
91
|
+
...entry,
|
|
92
|
+
content: fitContentToBudget(entry.content),
|
|
93
|
+
});
|
|
94
|
+
const fitPatchToBudget = (patch) => {
|
|
95
|
+
if (patch.content === undefined)
|
|
96
|
+
return patch;
|
|
97
|
+
return {
|
|
98
|
+
...patch,
|
|
99
|
+
content: fitContentToBudget(patch.content),
|
|
100
|
+
};
|
|
101
|
+
};
|
|
102
|
+
export const TraceContentBudget = Object.freeze({
|
|
103
|
+
wrapStorage(storage) {
|
|
104
|
+
return Object.freeze({
|
|
105
|
+
...storage,
|
|
106
|
+
writeEntry: async (entry) => {
|
|
107
|
+
await storage.writeEntry(fitEntryToBudget(entry));
|
|
108
|
+
},
|
|
109
|
+
updateEntry: async (uuid, patch) => {
|
|
110
|
+
await storage.updateEntry(uuid, fitPatchToBudget(patch));
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
},
|
|
114
|
+
});
|
|
@@ -17,6 +17,12 @@ const normalizeHeaders = (headers) => {
|
|
|
17
17
|
const normalizeHeaderValue = (value) => {
|
|
18
18
|
return Array.isArray(value) ? value.join(', ') : value;
|
|
19
19
|
};
|
|
20
|
+
const resolveRouteMiddleware = (req) => {
|
|
21
|
+
const middleware = req.context?.['traceRouteMiddleware'];
|
|
22
|
+
return Array.isArray(middleware)
|
|
23
|
+
? middleware.filter((value) => typeof value === 'string')
|
|
24
|
+
: [];
|
|
25
|
+
};
|
|
20
26
|
const resolveRequestPayload = (req, config) => {
|
|
21
27
|
const redactFields = [...config.redaction.keys, ...config.redaction.body];
|
|
22
28
|
const requestBody = typeof req.getBody === 'function' ? req.getBody() : req.body;
|
|
@@ -108,7 +114,7 @@ const buildEntry = (req, res, start, config, responseCapture) => {
|
|
|
108
114
|
responseBody: responseCapture.body,
|
|
109
115
|
duration: Date.now() - start,
|
|
110
116
|
memory: TraceContext.getMemory(),
|
|
111
|
-
middleware:
|
|
117
|
+
middleware: resolveRouteMiddleware(req),
|
|
112
118
|
hostname: TraceContext.getHostname(),
|
|
113
119
|
userId: TraceContext.getUserId(),
|
|
114
120
|
};
|
|
@@ -33,7 +33,12 @@ export const MiddlewareWatcher = Object.freeze({
|
|
|
33
33
|
return () => undefined;
|
|
34
34
|
_storage = storage;
|
|
35
35
|
_ignoreRoutes = config.ignoreRoutes;
|
|
36
|
+
globalThis.__zintrust_trace_middleware_emit__ = emit;
|
|
36
37
|
return () => {
|
|
38
|
+
const globalState = globalThis;
|
|
39
|
+
if (globalState.__zintrust_trace_middleware_emit__ === emit) {
|
|
40
|
+
delete globalState.__zintrust_trace_middleware_emit__;
|
|
41
|
+
}
|
|
37
42
|
_storage = null;
|
|
38
43
|
_ignoreRoutes = [];
|
|
39
44
|
};
|
|
@@ -34,7 +34,12 @@ export const ModelWatcher = Object.freeze({
|
|
|
34
34
|
return () => undefined;
|
|
35
35
|
_storage = storage;
|
|
36
36
|
_ignoreRoutes = config.ignoreRoutes;
|
|
37
|
+
globalThis.__zintrust_trace_model_emit__ = emit;
|
|
37
38
|
return () => {
|
|
39
|
+
const globalState = globalThis;
|
|
40
|
+
if (globalState.__zintrust_trace_model_emit__ === emit) {
|
|
41
|
+
delete globalState.__zintrust_trace_model_emit__;
|
|
42
|
+
}
|
|
38
43
|
_storage = null;
|
|
39
44
|
_ignoreRoutes = [];
|
|
40
45
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zintrust/trace",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.95",
|
|
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.4.
|
|
43
|
+
"@zintrust/core": "^0.4.94"
|
|
44
44
|
},
|
|
45
45
|
"publishConfig": {
|
|
46
46
|
"access": "public"
|
package/src/dashboard/ui.ts
CHANGED
|
@@ -221,6 +221,19 @@ const DASHBOARD_DOCUMENT = String.raw`<!DOCTYPE html>
|
|
|
221
221
|
return ' method-other';
|
|
222
222
|
};
|
|
223
223
|
|
|
224
|
+
const normalizeMethodLabel = (value) => {
|
|
225
|
+
const method = String(value || '').trim().toUpperCase();
|
|
226
|
+
if (method === '') return 'Request';
|
|
227
|
+
return method.charAt(0) + method.slice(1).toLowerCase();
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
const entryTypeLabel = (entry) => {
|
|
231
|
+
if (entry && entry.type === 'request') {
|
|
232
|
+
return normalizeMethodLabel(entry.content && entry.content.method);
|
|
233
|
+
}
|
|
234
|
+
return String(entry && entry.type || '');
|
|
235
|
+
};
|
|
236
|
+
|
|
224
237
|
const typeClass = (entryOrType, maybeEntry) => {
|
|
225
238
|
const entry = maybeEntry || (typeof entryOrType === 'object' ? entryOrType : null);
|
|
226
239
|
const type = entry && entry.type ? entry.type : entryOrType;
|
|
@@ -433,7 +446,7 @@ const DASHBOARD_DOCUMENT = String.raw`<!DOCTYPE html>
|
|
|
433
446
|
|
|
434
447
|
const entrySummaryText = (entry) => {
|
|
435
448
|
const content = entry && entry.content ? entry.content : {};
|
|
436
|
-
if (entry.type === 'request') return [content.responseStatus || '', content.
|
|
449
|
+
if (entry.type === 'request') return [content.responseStatus || '', content.uri || ''].filter(Boolean).join(' ');
|
|
437
450
|
if (entry.type === 'query') return String(content.sql || '').slice(0, 160);
|
|
438
451
|
if (entry.type === 'exception') return [content.class || '', content.message || ''].filter(Boolean).join(': ');
|
|
439
452
|
if (entry.type === 'log') return '[' + String(content.level || 'log') + '] ' + String(content.message || '').slice(0, 160);
|
|
@@ -630,8 +643,8 @@ const DASHBOARD_DOCUMENT = String.raw`<!DOCTYPE html>
|
|
|
630
643
|
const renderTraceItems = (entries, options = {}) => {
|
|
631
644
|
if (entries.length === 0) return '<p class="trace-note">No related entries captured.</p>';
|
|
632
645
|
|
|
633
|
-
const collapsible = options.collapsible
|
|
634
|
-
const isInitiallyOpen = options.collapsed
|
|
646
|
+
const collapsible = options.collapsible !== false;
|
|
647
|
+
const isInitiallyOpen = options.collapsed === false;
|
|
635
648
|
|
|
636
649
|
return '<div class="trace-panel">' + entries.map((entry) => {
|
|
637
650
|
if (collapsible) {
|
|
@@ -639,7 +652,7 @@ const DASHBOARD_DOCUMENT = String.raw`<!DOCTYPE html>
|
|
|
639
652
|
'<details class="trace-item trace-disclosure"' + (isInitiallyOpen ? ' open' : '') + '>',
|
|
640
653
|
'<summary class="trace-item-head trace-summary">',
|
|
641
654
|
'<span class="trace-summary-main">',
|
|
642
|
-
'<span><span class="' + typeClass(entry) + '">' + escapeHtml(entry
|
|
655
|
+
'<span><span class="' + typeClass(entry) + '">' + escapeHtml(entryTypeLabel(entry)) + '</span></span>',
|
|
643
656
|
'<span class="trace-summary-copy">' + entrySummaryInlineHtml(entry) + '</span>',
|
|
644
657
|
'</span>',
|
|
645
658
|
'<span class="activity-head">' + durationHtml(entry) + '<span class="activity-time">' + escapeHtml(timeSince(entry.createdAt)) + '</span></span>',
|
|
@@ -656,7 +669,7 @@ const DASHBOARD_DOCUMENT = String.raw`<!DOCTYPE html>
|
|
|
656
669
|
'<section class="trace-item">',
|
|
657
670
|
'<div class="trace-item-head">',
|
|
658
671
|
'<div>',
|
|
659
|
-
'<span class="' + typeClass(entry) + '">' + escapeHtml(entry
|
|
672
|
+
'<span class="' + typeClass(entry) + '">' + escapeHtml(entryTypeLabel(entry)) + '</span>',
|
|
660
673
|
'</div>',
|
|
661
674
|
'<div class="activity-head">' + durationHtml(entry) + '<span class="activity-time">' + escapeHtml(timeSince(entry.createdAt)) + '</span></div>',
|
|
662
675
|
'</div>',
|
|
@@ -679,14 +692,16 @@ const DASHBOARD_DOCUMENT = String.raw`<!DOCTYPE html>
|
|
|
679
692
|
{ id: 'headers', label: 'Headers' },
|
|
680
693
|
{ id: 'response', label: 'Response' },
|
|
681
694
|
{ id: 'queries', label: 'Queries', count: batchEntriesByType('query').length },
|
|
695
|
+
{ id: 'middleware', label: 'Middleware', count: batchEntriesByType('middleware').length },
|
|
696
|
+
{ id: 'models', label: 'Models', count: batchEntriesByType('model').length },
|
|
682
697
|
{ id: 'logs', label: 'Logs', count: batchEntriesByType('log').length },
|
|
683
698
|
{ id: 'exceptions', label: 'Exceptions', count: batchEntriesByType('exception').length },
|
|
684
699
|
{ id: 'http', label: 'HTTP', count: batchEntriesByType('client_request').length },
|
|
685
700
|
{ id: 'cache', label: 'Cache', count: batchEntriesByType('cache').length },
|
|
686
|
-
{ id: 'other', label: 'Other', count: batchEntries().filter((item) => !['request','query','log','exception','client_request','cache'].includes(item.type)).length }
|
|
701
|
+
{ id: 'other', label: 'Other', count: batchEntries().filter((item) => !['request','query','middleware','model','log','exception','client_request','cache'].includes(item.type)).length }
|
|
687
702
|
];
|
|
688
703
|
const currentTab = traceTabs.some((tab) => tab.id === state.detailTab) ? state.detailTab : 'summary';
|
|
689
|
-
const otherEntries = batchEntries().filter((item) => !['request','query','log','exception','client_request','cache'].includes(item.type));
|
|
704
|
+
const otherEntries = batchEntries().filter((item) => !['request','query','middleware','model','log','exception','client_request','cache'].includes(item.type));
|
|
690
705
|
const panels = {
|
|
691
706
|
summary: [
|
|
692
707
|
'<div class="detail-grid">',
|
|
@@ -705,12 +720,17 @@ const DASHBOARD_DOCUMENT = String.raw`<!DOCTYPE html>
|
|
|
705
720
|
renderMetricBox('Tags', [
|
|
706
721
|
{ label: 'Values', value: tagsHtml(entry.tags) || '<span class="activity-time">-</span>' }
|
|
707
722
|
]),
|
|
723
|
+
renderMetricBox('Route middleware', [
|
|
724
|
+
{ label: 'Attached', value: escapeHtml(Array.isArray(content.middleware) && content.middleware.length > 0 ? content.middleware.join(', ') : 'None') }
|
|
725
|
+
]),
|
|
708
726
|
'</div>'
|
|
709
727
|
].join(''),
|
|
710
728
|
payload: detailJson(content.payload || {}, 'Payload Json'),
|
|
711
729
|
headers: '<div class="detail-stack">' + detailJson(content.headers || {}, 'Request Header Json') + detailJson(content.responseHeaders || {}, 'Response Header Json') + '</div>',
|
|
712
730
|
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>',
|
|
713
|
-
queries: renderTraceItems(batchEntriesByType('query')
|
|
731
|
+
queries: renderTraceItems(batchEntriesByType('query')),
|
|
732
|
+
middleware: renderTraceItems(batchEntriesByType('middleware')),
|
|
733
|
+
models: renderTraceItems(batchEntriesByType('model')),
|
|
714
734
|
logs: renderTraceItems(batchEntriesByType('log')),
|
|
715
735
|
exceptions: renderTraceItems(batchEntriesByType('exception')),
|
|
716
736
|
http: renderTraceItems(batchEntriesByType('client_request')),
|
|
@@ -722,8 +742,8 @@ const DASHBOARD_DOCUMENT = String.raw`<!DOCTYPE html>
|
|
|
722
742
|
'<span class="back-link" data-action="close-detail"><- Back to entries</span>',
|
|
723
743
|
'<section class="panel detail-card">',
|
|
724
744
|
'<div>' + (entry.type === 'request'
|
|
725
|
-
? '<span class="' + typeClass(entry) + '">' + escapeHtml(entry
|
|
726
|
-
: '<span class="' + typeClass(entry) + '">' + escapeHtml(entry
|
|
745
|
+
? '<span class="' + typeClass(entry) + '">' + escapeHtml(entryTypeLabel(entry)) + '</span> <span class="' + typeClass(entry) + '">' + escapeHtml(content.responseStatus || '') + '</span> <span class="mono">' + escapeHtml(content.uri || '') + '</span> ' + tagsHtml(entry.tags)
|
|
746
|
+
: '<span class="' + typeClass(entry) + '">' + escapeHtml(entryTypeLabel(entry)) + '</span> ' + tagsHtml(entry.tags)) + '</div>',
|
|
727
747
|
'<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>',
|
|
728
748
|
'<div class="trace-tabs">',
|
|
729
749
|
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(''),
|
|
@@ -749,10 +769,10 @@ const DASHBOARD_DOCUMENT = String.raw`<!DOCTYPE html>
|
|
|
749
769
|
const recentRows = recent.data || [];
|
|
750
770
|
const recentTable = recentRows.length === 0
|
|
751
771
|
? '<div class="empty">No trace entries recorded.</div>'
|
|
752
|
-
: '<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
|
|
772
|
+
: '<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(entryTypeLabel(entry)) + '</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>';
|
|
753
773
|
const activityList = recentRows.length === 0
|
|
754
774
|
? '<div class="empty">No recent activity.</div>'
|
|
755
|
-
: '<ul class="activity-list">' + recentRows.slice(0, 5).map((entry) => '<li class="activity-item"><div class="activity-head"><span class="' + typeClass(entry) + '">' + escapeHtml(entry
|
|
775
|
+
: '<ul class="activity-list">' + recentRows.slice(0, 5).map((entry) => '<li class="activity-item"><div class="activity-head"><span class="' + typeClass(entry) + '">' + escapeHtml(entryTypeLabel(entry)) + '</span>' + durationHtml(entry) + '<span class="activity-time">' + escapeHtml(timeSince(entry.createdAt)) + '</span></div><div class="activity-summary">' + escapeHtml(entrySummaryText(entry)) + '</div></li>').join('') + '</ul>';
|
|
756
776
|
|
|
757
777
|
main.innerHTML = [
|
|
758
778
|
statsCardsHtml(stats),
|
|
@@ -796,7 +816,7 @@ const DASHBOARD_DOCUMENT = String.raw`<!DOCTYPE html>
|
|
|
796
816
|
const total = Number(response.total || 0);
|
|
797
817
|
const perPage = Number(response.perPage || 50);
|
|
798
818
|
const totalPages = Math.max(1, Math.ceil(total / perPage));
|
|
799
|
-
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
|
|
819
|
+
const rows = data.map((entry) => '<tr class="row-button" data-action="show-detail" data-uuid="' + escapeHtml(entry.uuid) + '"><td><span class="' + typeClass(entry) + '">' + escapeHtml(entryTypeLabel(entry)) + '</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('');
|
|
800
820
|
|
|
801
821
|
main.innerHTML = [
|
|
802
822
|
'<section class="panel">',
|
package/src/index.ts
CHANGED
|
@@ -18,6 +18,7 @@ export { TraceConfig } from './config';
|
|
|
18
18
|
// ---------------------------------------------------------------------------
|
|
19
19
|
export { TraceStorage } from './storage';
|
|
20
20
|
export type { ITraceStorage } from './storage';
|
|
21
|
+
export { TraceContentBudget } from './storage/TraceContentBudget';
|
|
21
22
|
export { TraceContentRedaction } from './storage/TraceContentRedaction';
|
|
22
23
|
|
|
23
24
|
// ---------------------------------------------------------------------------
|
package/src/register.ts
CHANGED
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
import { TraceConfig } from './config';
|
|
23
23
|
import { TraceContext } from './context';
|
|
24
24
|
import { TraceStorage } from './storage';
|
|
25
|
+
import { TraceContentBudget } from './storage/TraceContentBudget';
|
|
25
26
|
import { TraceContentRedaction } from './storage/TraceContentRedaction';
|
|
26
27
|
import { TraceEntryFiltering } from './storage/TraceEntryFiltering';
|
|
27
28
|
import { TraceWriteDiagnostics } from './storage/TraceWriteDiagnostics';
|
|
@@ -379,9 +380,11 @@ if (!traceAlreadyInitialized && Env) {
|
|
|
379
380
|
await assertTraceStorageReady(core, storageDb, resolvedConnectionName);
|
|
380
381
|
|
|
381
382
|
const storage = TraceWriteDiagnostics.wrapStorage(
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
383
|
+
TraceContentBudget.wrapStorage(
|
|
384
|
+
TraceContentRedaction.wrapStorage(
|
|
385
|
+
TraceEntryFiltering.wrapStorage(TraceStorage.resolveStorage(storageDb), config),
|
|
386
|
+
config.redaction
|
|
387
|
+
)
|
|
385
388
|
),
|
|
386
389
|
{
|
|
387
390
|
connectionName: resolvedConnectionName,
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import type { ITraceEntry, ITraceStorage } from '../types';
|
|
2
|
+
|
|
3
|
+
const DEFAULT_MAX_ENTRY_BYTES = 64 * 1024;
|
|
4
|
+
const DEFAULT_MAX_STRING_BYTES = 16 * 1024;
|
|
5
|
+
const DEFAULT_MAX_ARRAY_ITEMS = 25;
|
|
6
|
+
const DEFAULT_MAX_OBJECT_ENTRIES = 40;
|
|
7
|
+
const DEFAULT_MAX_DEPTH = 6;
|
|
8
|
+
|
|
9
|
+
const DROPPED_FIELD_MESSAGE =
|
|
10
|
+
'[trace] Value dropped because the field exceeded the trace storage size limit.';
|
|
11
|
+
const COMPACTED_CONTENT_MESSAGE =
|
|
12
|
+
'[trace] Trace content was compacted because it exceeded the trace storage size limit.';
|
|
13
|
+
|
|
14
|
+
const encoder = new TextEncoder();
|
|
15
|
+
|
|
16
|
+
const serializedSize = (value: unknown): number => {
|
|
17
|
+
try {
|
|
18
|
+
return encoder.encode(JSON.stringify(value)).length;
|
|
19
|
+
} catch {
|
|
20
|
+
return Number.MAX_SAFE_INTEGER;
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const describeValueType = (value: unknown): string => {
|
|
25
|
+
if (Array.isArray(value)) return 'array';
|
|
26
|
+
if (value === null) return 'null';
|
|
27
|
+
return typeof value;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const compactValue = (value: unknown, depth: number): unknown => {
|
|
31
|
+
if (depth >= DEFAULT_MAX_DEPTH) {
|
|
32
|
+
return DROPPED_FIELD_MESSAGE;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (typeof value === 'string') {
|
|
36
|
+
return serializedSize(value) > DEFAULT_MAX_STRING_BYTES ? DROPPED_FIELD_MESSAGE : value;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (Array.isArray(value)) {
|
|
40
|
+
const next = value
|
|
41
|
+
.slice(0, DEFAULT_MAX_ARRAY_ITEMS)
|
|
42
|
+
.map((item) => compactValue(item, depth + 1));
|
|
43
|
+
|
|
44
|
+
if (value.length > DEFAULT_MAX_ARRAY_ITEMS) {
|
|
45
|
+
next.push(
|
|
46
|
+
`[trace] ${String(value.length - DEFAULT_MAX_ARRAY_ITEMS)} additional items were dropped.`
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return next;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (typeof value !== 'object' || value === null) {
|
|
54
|
+
return value;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const entries = Object.entries(value);
|
|
58
|
+
const compactedEntries = entries
|
|
59
|
+
.slice(0, DEFAULT_MAX_OBJECT_ENTRIES)
|
|
60
|
+
.map(([key, entryValue]) => [key, compactValue(entryValue, depth + 1)]);
|
|
61
|
+
|
|
62
|
+
if (entries.length > DEFAULT_MAX_OBJECT_ENTRIES) {
|
|
63
|
+
compactedEntries.push([
|
|
64
|
+
'__traceNotice',
|
|
65
|
+
`[trace] ${String(entries.length - DEFAULT_MAX_OBJECT_ENTRIES)} additional fields were dropped.`,
|
|
66
|
+
]);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return Object.fromEntries(compactedEntries);
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const compactTopLevelObjectToBudget = (value: Record<string, unknown>): Record<string, unknown> => {
|
|
73
|
+
const compacted: Record<string, unknown> = {
|
|
74
|
+
...value,
|
|
75
|
+
__traceNotice: COMPACTED_CONTENT_MESSAGE,
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const keysByDescendingSize = Object.keys(compacted)
|
|
79
|
+
.filter((key) => key !== '__traceNotice')
|
|
80
|
+
.sort((left, right) => serializedSize(compacted[right]) - serializedSize(compacted[left]));
|
|
81
|
+
|
|
82
|
+
for (const key of keysByDescendingSize) {
|
|
83
|
+
if (serializedSize(compacted) <= DEFAULT_MAX_ENTRY_BYTES) break;
|
|
84
|
+
compacted[key] = DROPPED_FIELD_MESSAGE;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return compacted;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const fitContentToBudget = (content: unknown): unknown => {
|
|
91
|
+
if (serializedSize(content) <= DEFAULT_MAX_ENTRY_BYTES) {
|
|
92
|
+
return content;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const compacted = compactValue(content, 0);
|
|
96
|
+
if (serializedSize(compacted) <= DEFAULT_MAX_ENTRY_BYTES) {
|
|
97
|
+
return compacted;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (typeof compacted === 'object' && compacted !== null && !Array.isArray(compacted)) {
|
|
101
|
+
const topLevelCompacted = compactTopLevelObjectToBudget(compacted as Record<string, unknown>);
|
|
102
|
+
if (serializedSize(topLevelCompacted) <= DEFAULT_MAX_ENTRY_BYTES) {
|
|
103
|
+
return topLevelCompacted;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
__traceNotice: COMPACTED_CONTENT_MESSAGE,
|
|
109
|
+
dropped: true,
|
|
110
|
+
valueType: describeValueType(content),
|
|
111
|
+
};
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const fitEntryToBudget = (entry: ITraceEntry): ITraceEntry => ({
|
|
115
|
+
...entry,
|
|
116
|
+
content: fitContentToBudget(entry.content),
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const fitPatchToBudget = (
|
|
120
|
+
patch: Partial<Pick<ITraceEntry, 'content' | 'isLatest'>>
|
|
121
|
+
): Partial<Pick<ITraceEntry, 'content' | 'isLatest'>> => {
|
|
122
|
+
if (patch.content === undefined) return patch;
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
...patch,
|
|
126
|
+
content: fitContentToBudget(patch.content),
|
|
127
|
+
};
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
export const TraceContentBudget = Object.freeze({
|
|
131
|
+
wrapStorage(storage: ITraceStorage): ITraceStorage {
|
|
132
|
+
return Object.freeze({
|
|
133
|
+
...storage,
|
|
134
|
+
writeEntry: async (entry: ITraceEntry): Promise<void> => {
|
|
135
|
+
await storage.writeEntry(fitEntryToBudget(entry));
|
|
136
|
+
},
|
|
137
|
+
updateEntry: async (
|
|
138
|
+
uuid: string,
|
|
139
|
+
patch: Partial<Pick<ITraceEntry, 'content' | 'isLatest'>>
|
|
140
|
+
): Promise<void> => {
|
|
141
|
+
await storage.updateEntry(uuid, fitPatchToBudget(patch));
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
},
|
|
145
|
+
});
|
|
@@ -26,6 +26,13 @@ const normalizeHeaderValue = (value: string | string[]): string => {
|
|
|
26
26
|
return Array.isArray(value) ? value.join(', ') : value;
|
|
27
27
|
};
|
|
28
28
|
|
|
29
|
+
const resolveRouteMiddleware = (req: IRequest): string[] => {
|
|
30
|
+
const middleware = req.context?.['traceRouteMiddleware'];
|
|
31
|
+
return Array.isArray(middleware)
|
|
32
|
+
? middleware.filter((value): value is string => typeof value === 'string')
|
|
33
|
+
: [];
|
|
34
|
+
};
|
|
35
|
+
|
|
29
36
|
const resolveRequestPayload = (req: IRequest, config: ITraceConfig): unknown => {
|
|
30
37
|
const redactFields = [...config.redaction.keys, ...config.redaction.body];
|
|
31
38
|
const requestBody = typeof req.getBody === 'function' ? req.getBody() : req.body;
|
|
@@ -150,7 +157,7 @@ const buildEntry = (
|
|
|
150
157
|
responseBody: responseCapture.body,
|
|
151
158
|
duration: Date.now() - start,
|
|
152
159
|
memory: TraceContext.getMemory(),
|
|
153
|
-
middleware:
|
|
160
|
+
middleware: resolveRouteMiddleware(req),
|
|
154
161
|
hostname: TraceContext.getHostname(),
|
|
155
162
|
userId: TraceContext.getUserId(),
|
|
156
163
|
};
|
|
@@ -6,6 +6,10 @@ import { RequestFilter } from '../utils/requestFilter';
|
|
|
6
6
|
let _storage: ITraceWatcherConfig['storage'] | null = null;
|
|
7
7
|
let _ignoreRoutes: string[] = [];
|
|
8
8
|
|
|
9
|
+
type GlobalMiddlewareTraceState = {
|
|
10
|
+
__zintrust_trace_middleware_emit__?: typeof emit;
|
|
11
|
+
};
|
|
12
|
+
|
|
9
13
|
const emit = (name: string, event: MiddlewareContent['event'], duration?: number): void => {
|
|
10
14
|
if (!_storage) return;
|
|
11
15
|
if (RequestFilter.shouldIgnoreCurrentRequest(_ignoreRoutes)) return;
|
|
@@ -34,7 +38,12 @@ export const MiddlewareWatcher: ITraceWatcher & { emit: typeof emit } = Object.f
|
|
|
34
38
|
if (config.watchers.middleware === false) return () => undefined;
|
|
35
39
|
_storage = storage;
|
|
36
40
|
_ignoreRoutes = config.ignoreRoutes;
|
|
41
|
+
(globalThis as unknown as GlobalMiddlewareTraceState).__zintrust_trace_middleware_emit__ = emit;
|
|
37
42
|
return () => {
|
|
43
|
+
const globalState = globalThis as unknown as GlobalMiddlewareTraceState;
|
|
44
|
+
if (globalState.__zintrust_trace_middleware_emit__ === emit) {
|
|
45
|
+
delete globalState.__zintrust_trace_middleware_emit__;
|
|
46
|
+
}
|
|
38
47
|
_storage = null;
|
|
39
48
|
_ignoreRoutes = [];
|
|
40
49
|
};
|
|
@@ -6,6 +6,10 @@ import { RequestFilter } from '../utils/requestFilter';
|
|
|
6
6
|
let _storage: ITraceWatcherConfig['storage'] | null = null;
|
|
7
7
|
let _ignoreRoutes: string[] = [];
|
|
8
8
|
|
|
9
|
+
type GlobalModelTraceState = {
|
|
10
|
+
__zintrust_trace_model_emit__?: typeof emit;
|
|
11
|
+
};
|
|
12
|
+
|
|
9
13
|
const emit = (
|
|
10
14
|
action: ModelContent['action'],
|
|
11
15
|
model: string,
|
|
@@ -40,7 +44,12 @@ export const ModelWatcher: ITraceWatcher & { emit: typeof emit } = Object.freeze
|
|
|
40
44
|
if (config.watchers.model === false) return () => undefined;
|
|
41
45
|
_storage = storage;
|
|
42
46
|
_ignoreRoutes = config.ignoreRoutes;
|
|
47
|
+
(globalThis as unknown as GlobalModelTraceState).__zintrust_trace_model_emit__ = emit;
|
|
43
48
|
return () => {
|
|
49
|
+
const globalState = globalThis as unknown as GlobalModelTraceState;
|
|
50
|
+
if (globalState.__zintrust_trace_model_emit__ === emit) {
|
|
51
|
+
delete globalState.__zintrust_trace_model_emit__;
|
|
52
|
+
}
|
|
44
53
|
_storage = null;
|
|
45
54
|
_ignoreRoutes = [];
|
|
46
55
|
};
|