nestjs-profiler 1.0.22 → 1.0.24
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 +112 -47
- package/collectors/http-collector.d.ts +12 -0
- package/collectors/http-collector.js +132 -0
- package/collectors/http-collector.js.map +1 -0
- package/collectors/log-collector.d.ts +3 -1
- package/collectors/log-collector.js +33 -12
- package/collectors/log-collector.js.map +1 -1
- package/common/profiler-options.interface.d.ts +1 -0
- package/common/profiler.model.d.ts +14 -0
- package/controllers/profiler.controller.d.ts +4 -0
- package/controllers/profiler.controller.js +86 -0
- package/controllers/profiler.controller.js.map +1 -1
- package/package.json +1 -1
- package/profiler.module.js +2 -0
- package/profiler.module.js.map +1 -1
- package/services/profiler.service.d.ts +43 -1
- package/services/profiler.service.js +120 -0
- package/services/profiler.service.js.map +1 -1
- package/services/template-builder.service.d.ts +5 -0
- package/services/template-builder.service.js +272 -1
- package/services/template-builder.service.js.map +1 -1
- package/services/view.service.js +20 -12
- package/services/view.service.js.map +1 -1
- package/views/dashboard.html +24 -0
- package/views/detail.html +2 -0
- package/views/layout.html +45 -0
- package/views/live-logs.html +324 -0
- package/views/summary.html +205 -0
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
<!-- Live Logs Terminal -->
|
|
2
|
+
<div class="flex flex-col h-full" style="height: calc(100vh - 120px);">
|
|
3
|
+
|
|
4
|
+
<!-- Toolbar -->
|
|
5
|
+
<div class="flex flex-wrap items-center gap-3 mb-3">
|
|
6
|
+
<!-- Status indicator -->
|
|
7
|
+
<div class="flex items-center gap-2">
|
|
8
|
+
<span id="statusDot" class="w-2.5 h-2.5 rounded-full bg-yellow-400 animate-pulse"></span>
|
|
9
|
+
<span id="statusText" class="text-sm font-medium text-gray-600">Connecting…</span>
|
|
10
|
+
</div>
|
|
11
|
+
|
|
12
|
+
<div class="h-4 border-l border-gray-200"></div>
|
|
13
|
+
|
|
14
|
+
<!-- Level filter -->
|
|
15
|
+
<div class="flex items-center gap-1">
|
|
16
|
+
<span class="text-xs text-gray-500 mr-1">Level:</span>
|
|
17
|
+
<button onclick="toggleLevel('all')" id="btn-all"
|
|
18
|
+
class="level-btn px-2.5 py-1 text-xs font-medium rounded-full bg-indigo-600 text-white">All</button>
|
|
19
|
+
<button onclick="toggleLevel('log')" id="btn-log"
|
|
20
|
+
class="level-btn px-2.5 py-1 text-xs font-medium rounded-full text-gray-600 bg-gray-100 hover:bg-gray-200">Log</button>
|
|
21
|
+
<button onclick="toggleLevel('warn')" id="btn-warn"
|
|
22
|
+
class="level-btn px-2.5 py-1 text-xs font-medium rounded-full text-yellow-700 bg-yellow-50 hover:bg-yellow-100">Warn</button>
|
|
23
|
+
<button onclick="toggleLevel('error')" id="btn-error"
|
|
24
|
+
class="level-btn px-2.5 py-1 text-xs font-medium rounded-full text-red-700 bg-red-50 hover:bg-red-100">Error</button>
|
|
25
|
+
<button onclick="toggleLevel('debug')" id="btn-debug"
|
|
26
|
+
class="level-btn px-2.5 py-1 text-xs font-medium rounded-full text-gray-500 bg-gray-50 hover:bg-gray-100">Debug</button>
|
|
27
|
+
</div>
|
|
28
|
+
|
|
29
|
+
<div class="h-4 border-l border-gray-200"></div>
|
|
30
|
+
|
|
31
|
+
<!-- Search -->
|
|
32
|
+
<div class="relative flex-1 max-w-xs">
|
|
33
|
+
<svg class="absolute left-2.5 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
34
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
|
|
35
|
+
</svg>
|
|
36
|
+
<input id="searchInput" type="text" placeholder="Filter logs..."
|
|
37
|
+
class="w-full pl-8 pr-3 py-1.5 text-xs border border-gray-200 rounded-lg focus:outline-none focus:ring-1 focus:ring-indigo-400"
|
|
38
|
+
oninput="applyFilter()">
|
|
39
|
+
</div>
|
|
40
|
+
|
|
41
|
+
<div class="ml-auto flex items-center gap-2">
|
|
42
|
+
<!-- High-volume warning badge (hidden by default) -->
|
|
43
|
+
<span id="floodBadge" class="hidden items-center gap-1 px-2 py-0.5 text-xs font-semibold rounded-full bg-orange-100 text-orange-700">
|
|
44
|
+
⚡ High volume — sampling
|
|
45
|
+
</span>
|
|
46
|
+
<!-- Log counter -->
|
|
47
|
+
<span class="text-xs text-gray-400"><span id="visibleCount">0</span> / <span id="totalCount">0</span> lines</span>
|
|
48
|
+
|
|
49
|
+
<!-- Pause / Resume -->
|
|
50
|
+
<button id="pauseBtn" onclick="togglePause()"
|
|
51
|
+
class="flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium rounded-lg border border-gray-200 bg-white hover:bg-gray-50 text-gray-600">
|
|
52
|
+
<svg id="pauseIcon" class="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
53
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 9v6m4-6v6"/>
|
|
54
|
+
</svg>
|
|
55
|
+
<span id="pauseLabel">Pause</span>
|
|
56
|
+
</button>
|
|
57
|
+
|
|
58
|
+
<!-- Auto-scroll toggle -->
|
|
59
|
+
<button id="scrollBtn" onclick="toggleScroll()"
|
|
60
|
+
class="flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium rounded-lg border border-indigo-200 bg-indigo-50 text-indigo-700">
|
|
61
|
+
<svg class="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
62
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
|
|
63
|
+
</svg>
|
|
64
|
+
Auto-scroll
|
|
65
|
+
</button>
|
|
66
|
+
|
|
67
|
+
<!-- Clear -->
|
|
68
|
+
<button onclick="clearLogs()"
|
|
69
|
+
class="flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium rounded-lg border border-gray-200 bg-white hover:bg-red-50 hover:border-red-200 hover:text-red-600 text-gray-600">
|
|
70
|
+
<svg class="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
71
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/>
|
|
72
|
+
</svg>
|
|
73
|
+
Clear
|
|
74
|
+
</button>
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
|
|
78
|
+
<!-- Terminal window -->
|
|
79
|
+
<div class="flex-1 bg-gray-950 rounded-xl border border-gray-800 overflow-hidden flex flex-col" style="min-height: 0;">
|
|
80
|
+
<!-- Terminal titlebar -->
|
|
81
|
+
<div class="flex items-center gap-1.5 px-4 py-2.5 bg-gray-900 border-b border-gray-800 flex-shrink-0">
|
|
82
|
+
<span class="w-3 h-3 rounded-full bg-red-500"></span>
|
|
83
|
+
<span class="w-3 h-3 rounded-full bg-yellow-500"></span>
|
|
84
|
+
<span class="w-3 h-3 rounded-full bg-green-500"></span>
|
|
85
|
+
<span class="ml-3 text-xs text-gray-500 font-mono">NestJS Application — live output</span>
|
|
86
|
+
</div>
|
|
87
|
+
|
|
88
|
+
<!-- Log output -->
|
|
89
|
+
<div id="logOutput" class="flex-1 overflow-y-auto p-4 font-mono text-xs leading-5 space-y-0.5"
|
|
90
|
+
style="min-height: 0; scroll-behavior: smooth;">
|
|
91
|
+
<div class="text-gray-600 italic">Waiting for logs...</div>
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
</div>
|
|
95
|
+
|
|
96
|
+
<script>
|
|
97
|
+
// ── State ─────────────────────────────────────────────────────────────────
|
|
98
|
+
let paused = false;
|
|
99
|
+
let autoScroll = true;
|
|
100
|
+
let activeLevel = 'all';
|
|
101
|
+
let activeSearch = '';
|
|
102
|
+
|
|
103
|
+
// Line accounting — maintained manually so we never querySelectorAll on every append
|
|
104
|
+
const MAX_LINES = 500;
|
|
105
|
+
let domLineCount = 0; // lines currently in DOM
|
|
106
|
+
let visibleCount = 0; // lines currently visible (pass filter)
|
|
107
|
+
let totalReceived = 0; // all-time lines received this session
|
|
108
|
+
|
|
109
|
+
// Flood-rate tracking (lines per second)
|
|
110
|
+
let rateCount = 0;
|
|
111
|
+
let rateTimer = setInterval(() => {
|
|
112
|
+
const badge = document.getElementById('floodBadge');
|
|
113
|
+
if (badge) {
|
|
114
|
+
if (rateCount > 50) { badge.classList.remove('hidden'); badge.classList.add('flex'); }
|
|
115
|
+
else { badge.classList.add('hidden'); badge.classList.remove('flex'); }
|
|
116
|
+
}
|
|
117
|
+
rateCount = 0;
|
|
118
|
+
}, 1000);
|
|
119
|
+
|
|
120
|
+
// Pending queue for batched DOM writes (filled by SSE, drained by rAF)
|
|
121
|
+
let pendingEntries = [];
|
|
122
|
+
let rafScheduled = false;
|
|
123
|
+
|
|
124
|
+
// ── Colour map ────────────────────────────────────────────────────────────
|
|
125
|
+
const COLORS = {
|
|
126
|
+
error: { text: 'text-red-400', badge: 'text-red-500 bg-red-950' },
|
|
127
|
+
warn: { text: 'text-yellow-300', badge: 'text-yellow-400 bg-yellow-950' },
|
|
128
|
+
log: { text: 'text-green-300', badge: 'text-green-500 bg-green-950' },
|
|
129
|
+
verbose: { text: 'text-cyan-300', badge: 'text-cyan-500 bg-cyan-950' },
|
|
130
|
+
debug: { text: 'text-gray-400', badge: 'text-gray-500 bg-gray-900' },
|
|
131
|
+
system: { text: 'text-indigo-300', badge: 'text-indigo-400 bg-indigo-950' },
|
|
132
|
+
};
|
|
133
|
+
function getColors(level) { return COLORS[level] || COLORS['log']; }
|
|
134
|
+
|
|
135
|
+
// ── Helpers ───────────────────────────────────────────────────────────────
|
|
136
|
+
function formatTime(ts) {
|
|
137
|
+
const d = new Date(ts);
|
|
138
|
+
return d.toTimeString().slice(0, 8) + '.' + String(d.getMilliseconds()).padStart(3, '0');
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function escHtml(s) {
|
|
142
|
+
return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function buildElement(entry) {
|
|
146
|
+
const c = getColors(entry.level);
|
|
147
|
+
const time = formatTime(entry.timestamp);
|
|
148
|
+
const ctx = entry.context ? `<span class="text-indigo-400 mr-2">[${escHtml(entry.context)}]</span>` : '';
|
|
149
|
+
const msg = escHtml(entry.message || '');
|
|
150
|
+
const lvl = escHtml(entry.level);
|
|
151
|
+
|
|
152
|
+
const el = document.createElement('div');
|
|
153
|
+
el.className = 'log-line flex items-start gap-2 hover:bg-gray-900 rounded px-1 py-0.5';
|
|
154
|
+
el.dataset.level = entry.level;
|
|
155
|
+
el.dataset.msg = (entry.message || '').toLowerCase();
|
|
156
|
+
el.innerHTML = `
|
|
157
|
+
<span class="flex-shrink-0 text-gray-600 select-none">${time}</span>
|
|
158
|
+
<span class="flex-shrink-0 w-14 text-center text-xs px-1 rounded font-semibold uppercase ${c.badge}">${lvl}</span>
|
|
159
|
+
<span class="${c.text} break-all">${ctx}${msg}</span>`;
|
|
160
|
+
return el;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function passesFilter(entry) {
|
|
164
|
+
const levelOk = activeLevel === 'all' || entry.level === activeLevel;
|
|
165
|
+
const searchOk = !activeSearch || (entry.message || '').toLowerCase().includes(activeSearch);
|
|
166
|
+
return levelOk && searchOk;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// ── Batch DOM flush (called by requestAnimationFrame) ─────────────────────
|
|
170
|
+
function flushPending() {
|
|
171
|
+
rafScheduled = false;
|
|
172
|
+
if (pendingEntries.length === 0) return;
|
|
173
|
+
|
|
174
|
+
const output = document.getElementById('logOutput');
|
|
175
|
+
if (!output) return;
|
|
176
|
+
|
|
177
|
+
// Remove placeholder on first real log
|
|
178
|
+
const ph = output.querySelector('.italic');
|
|
179
|
+
if (ph) ph.remove();
|
|
180
|
+
|
|
181
|
+
const entries = pendingEntries.splice(0); // drain the queue
|
|
182
|
+
const frag = document.createDocumentFragment();
|
|
183
|
+
|
|
184
|
+
entries.forEach(entry => {
|
|
185
|
+
const el = buildElement(entry);
|
|
186
|
+
const visible = passesFilter(entry);
|
|
187
|
+
if (!visible) el.style.display = 'none';
|
|
188
|
+
else visibleCount++;
|
|
189
|
+
frag.appendChild(el);
|
|
190
|
+
domLineCount++;
|
|
191
|
+
totalReceived++;
|
|
192
|
+
rateCount++;
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
output.appendChild(frag);
|
|
196
|
+
|
|
197
|
+
// Trim oldest lines when over the cap — batch-remove all excess at once
|
|
198
|
+
const excess = domLineCount - MAX_LINES;
|
|
199
|
+
if (excess > 0) {
|
|
200
|
+
// Collect first `excess` .log-line children
|
|
201
|
+
let removed = 0, child = output.firstElementChild;
|
|
202
|
+
while (child && removed < excess) {
|
|
203
|
+
const next = child.nextElementSibling;
|
|
204
|
+
if (child.classList.contains('log-line')) {
|
|
205
|
+
if (child.style.display !== 'none') visibleCount = Math.max(0, visibleCount - 1);
|
|
206
|
+
output.removeChild(child);
|
|
207
|
+
domLineCount--;
|
|
208
|
+
removed++;
|
|
209
|
+
}
|
|
210
|
+
child = next;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Update counters once per frame, not once per line
|
|
215
|
+
document.getElementById('totalCount').textContent = totalReceived;
|
|
216
|
+
document.getElementById('visibleCount').textContent = visibleCount;
|
|
217
|
+
|
|
218
|
+
if (autoScroll) output.scrollTop = output.scrollHeight;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function scheduleFlush() {
|
|
222
|
+
if (!rafScheduled) { rafScheduled = true; requestAnimationFrame(flushPending); }
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// ── Public UI actions ─────────────────────────────────────────────────────
|
|
226
|
+
function applyFilter() {
|
|
227
|
+
activeSearch = document.getElementById('searchInput').value.toLowerCase();
|
|
228
|
+
const output = document.getElementById('logOutput');
|
|
229
|
+
if (!output) return;
|
|
230
|
+
visibleCount = 0;
|
|
231
|
+
output.querySelectorAll('.log-line').forEach(line => {
|
|
232
|
+
const ok = (activeLevel === 'all' || line.dataset.level === activeLevel)
|
|
233
|
+
&& (!activeSearch || line.dataset.msg.includes(activeSearch));
|
|
234
|
+
line.style.display = ok ? '' : 'none';
|
|
235
|
+
if (ok) visibleCount++;
|
|
236
|
+
});
|
|
237
|
+
document.getElementById('visibleCount').textContent = visibleCount;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function toggleLevel(level) {
|
|
241
|
+
activeLevel = level;
|
|
242
|
+
document.querySelectorAll('.level-btn').forEach(btn => {
|
|
243
|
+
btn.classList.toggle('ring-2', btn.id === `btn-${level}`);
|
|
244
|
+
});
|
|
245
|
+
applyFilter();
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function togglePause() {
|
|
249
|
+
paused = !paused;
|
|
250
|
+
const icon = document.getElementById('pauseIcon');
|
|
251
|
+
const label = document.getElementById('pauseLabel');
|
|
252
|
+
const dot = document.getElementById('statusDot');
|
|
253
|
+
const statusText = document.getElementById('statusText');
|
|
254
|
+
if (paused) {
|
|
255
|
+
icon.innerHTML = '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z"/>';
|
|
256
|
+
label.textContent = 'Resume';
|
|
257
|
+
dot.className = 'w-2.5 h-2.5 rounded-full bg-yellow-400';
|
|
258
|
+
statusText.textContent = 'Paused';
|
|
259
|
+
} else {
|
|
260
|
+
icon.innerHTML = '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 9v6m4-6v6"/>';
|
|
261
|
+
label.textContent = 'Pause';
|
|
262
|
+
dot.className = 'w-2.5 h-2.5 rounded-full bg-green-400 animate-pulse';
|
|
263
|
+
statusText.textContent = 'Connected';
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function toggleScroll() {
|
|
268
|
+
autoScroll = !autoScroll;
|
|
269
|
+
const btn = document.getElementById('scrollBtn');
|
|
270
|
+
if (autoScroll) {
|
|
271
|
+
btn.className = 'flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium rounded-lg border border-indigo-200 bg-indigo-50 text-indigo-700';
|
|
272
|
+
const output = document.getElementById('logOutput');
|
|
273
|
+
if (output) output.scrollTop = output.scrollHeight;
|
|
274
|
+
} else {
|
|
275
|
+
btn.className = 'flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium rounded-lg border border-gray-200 bg-white text-gray-500';
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function clearLogs() {
|
|
280
|
+
domLineCount = 0;
|
|
281
|
+
visibleCount = 0;
|
|
282
|
+
totalReceived = 0;
|
|
283
|
+
document.getElementById('totalCount').textContent = '0';
|
|
284
|
+
document.getElementById('visibleCount').textContent = '0';
|
|
285
|
+
const output = document.getElementById('logOutput');
|
|
286
|
+
if (output) output.innerHTML = '<div class="text-gray-600 italic">Cleared. Waiting for new logs...</div>';
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// ── SSE connection ────────────────────────────────────────────────────────
|
|
290
|
+
function connect() {
|
|
291
|
+
if (window.__profilerLogES) { window.__profilerLogES.close(); window.__profilerLogES = null; }
|
|
292
|
+
|
|
293
|
+
const dot = document.getElementById('statusDot');
|
|
294
|
+
const statusText = document.getElementById('statusText');
|
|
295
|
+
const es = new EventSource('/__profiler/logs/stream');
|
|
296
|
+
window.__profilerLogES = es;
|
|
297
|
+
|
|
298
|
+
es.onopen = () => {
|
|
299
|
+
if (dot) dot.className = 'w-2.5 h-2.5 rounded-full bg-green-400 animate-pulse';
|
|
300
|
+
if (statusText) statusText.textContent = 'Connected';
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
es.onmessage = (e) => {
|
|
304
|
+
if (paused) return;
|
|
305
|
+
try {
|
|
306
|
+
const data = JSON.parse(e.data);
|
|
307
|
+
// Server sends either a single entry or an array (batched)
|
|
308
|
+
const entries = Array.isArray(data) ? data : [data];
|
|
309
|
+
entries.forEach(entry => pendingEntries.push(entry));
|
|
310
|
+
scheduleFlush();
|
|
311
|
+
} catch (_) {}
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
es.onerror = () => {
|
|
315
|
+
if (dot) dot.className = 'w-2.5 h-2.5 rounded-full bg-red-400';
|
|
316
|
+
if (statusText) statusText.textContent = 'Reconnecting…';
|
|
317
|
+
es.close();
|
|
318
|
+
window.__profilerLogES = null;
|
|
319
|
+
setTimeout(connect, 3000);
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
connect();
|
|
324
|
+
</script>
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
<!-- Summary Dashboard -->
|
|
2
|
+
<div class="space-y-6">
|
|
3
|
+
|
|
4
|
+
<!-- Page Header -->
|
|
5
|
+
<div class="flex items-center justify-between">
|
|
6
|
+
<div>
|
|
7
|
+
<h1 class="text-2xl font-bold text-gray-900">Summary</h1>
|
|
8
|
+
<p class="mt-1 text-sm text-gray-500">Aggregated stats across all captured requests</p>
|
|
9
|
+
</div>
|
|
10
|
+
<span class="text-xs text-gray-400 bg-white border border-gray-200 px-3 py-1.5 rounded-lg">{{ totalRequests }} requests sampled</span>
|
|
11
|
+
</div>
|
|
12
|
+
|
|
13
|
+
<!-- KPI Cards Row 1 -->
|
|
14
|
+
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
|
|
15
|
+
<!-- Total Requests -->
|
|
16
|
+
<div class="bg-white rounded-xl border border-gray-200 shadow-sm p-5">
|
|
17
|
+
<div class="flex items-center justify-between mb-3">
|
|
18
|
+
<p class="text-xs font-semibold text-gray-500 uppercase tracking-wide">Total Requests</p>
|
|
19
|
+
<span class="p-1.5 bg-indigo-50 rounded-lg">
|
|
20
|
+
<svg class="w-4 h-4 text-indigo-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
21
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9"/>
|
|
22
|
+
</svg>
|
|
23
|
+
</span>
|
|
24
|
+
</div>
|
|
25
|
+
<p class="text-3xl font-bold text-gray-900">{{ totalRequests }}</p>
|
|
26
|
+
</div>
|
|
27
|
+
|
|
28
|
+
<!-- Avg Duration -->
|
|
29
|
+
<div class="bg-white rounded-xl border border-gray-200 shadow-sm p-5">
|
|
30
|
+
<div class="flex items-center justify-between mb-3">
|
|
31
|
+
<p class="text-xs font-semibold text-gray-500 uppercase tracking-wide">Avg Duration</p>
|
|
32
|
+
<span class="p-1.5 bg-blue-50 rounded-lg">
|
|
33
|
+
<svg class="w-4 h-4 text-blue-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
34
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
|
35
|
+
</svg>
|
|
36
|
+
</span>
|
|
37
|
+
</div>
|
|
38
|
+
<p class="text-3xl font-bold text-gray-900">{{ avgDuration }}<span class="text-base font-medium text-gray-400 ml-1">ms</span></p>
|
|
39
|
+
<p class="text-xs text-gray-400 mt-1">p95: {{ p95Duration }}ms</p>
|
|
40
|
+
</div>
|
|
41
|
+
|
|
42
|
+
<!-- Error Rate -->
|
|
43
|
+
<div class="bg-white rounded-xl border border-gray-200 shadow-sm p-5">
|
|
44
|
+
<div class="flex items-center justify-between mb-3">
|
|
45
|
+
<p class="text-xs font-semibold text-gray-500 uppercase tracking-wide">Error Rate</p>
|
|
46
|
+
<span class="{{{ errorRateBadgeClass }}} p-1.5 rounded-lg">
|
|
47
|
+
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
48
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/>
|
|
49
|
+
</svg>
|
|
50
|
+
</span>
|
|
51
|
+
</div>
|
|
52
|
+
<p class="text-3xl font-bold {{{ errorRateTextClass }}}">{{ errorRate }}<span class="text-base font-medium text-gray-400 ml-0.5">%</span></p>
|
|
53
|
+
<p class="text-xs text-gray-400 mt-1">{{ errorCount }} errors</p>
|
|
54
|
+
</div>
|
|
55
|
+
|
|
56
|
+
<!-- Total Queries -->
|
|
57
|
+
<div class="bg-white rounded-xl border border-gray-200 shadow-sm p-5">
|
|
58
|
+
<div class="flex items-center justify-between mb-3">
|
|
59
|
+
<p class="text-xs font-semibold text-gray-500 uppercase tracking-wide">DB Queries</p>
|
|
60
|
+
<span class="p-1.5 bg-purple-50 rounded-lg">
|
|
61
|
+
<svg class="w-4 h-4 text-purple-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
62
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4"/>
|
|
63
|
+
</svg>
|
|
64
|
+
</span>
|
|
65
|
+
</div>
|
|
66
|
+
<p class="text-3xl font-bold text-gray-900">{{ totalQueries }}</p>
|
|
67
|
+
<p class="text-xs text-gray-400 mt-1">{{ avgQueriesPerRequest }} avg/request</p>
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
|
|
71
|
+
<!-- KPI Cards Row 2 -->
|
|
72
|
+
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
|
|
73
|
+
<!-- Slow Queries -->
|
|
74
|
+
<div class="bg-white rounded-xl border border-gray-200 shadow-sm p-5">
|
|
75
|
+
<div class="flex items-center justify-between mb-3">
|
|
76
|
+
<p class="text-xs font-semibold text-gray-500 uppercase tracking-wide">Slow Queries</p>
|
|
77
|
+
<span class="p-1.5 bg-yellow-50 rounded-lg">
|
|
78
|
+
<svg class="w-4 h-4 text-yellow-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
79
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/>
|
|
80
|
+
</svg>
|
|
81
|
+
</span>
|
|
82
|
+
</div>
|
|
83
|
+
<p class="text-3xl font-bold {{{ slowQueriesTextClass }}}">{{ slowQueries }}</p>
|
|
84
|
+
<p class="text-xs text-gray-400 mt-1">>100ms threshold</p>
|
|
85
|
+
</div>
|
|
86
|
+
|
|
87
|
+
<!-- N+1 Detected -->
|
|
88
|
+
<div class="bg-white rounded-xl border border-gray-200 shadow-sm p-5">
|
|
89
|
+
<div class="flex items-center justify-between mb-3">
|
|
90
|
+
<p class="text-xs font-semibold text-gray-500 uppercase tracking-wide">N+1 Detected</p>
|
|
91
|
+
<span class="p-1.5 {{{ nPlusOneBadgeClass }}} rounded-lg">
|
|
92
|
+
<svg class="w-4 h-4 {{{ nPlusOneIconClass }}}" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
93
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>
|
|
94
|
+
</svg>
|
|
95
|
+
</span>
|
|
96
|
+
</div>
|
|
97
|
+
<p class="text-3xl font-bold {{{ nPlusOneTextClass }}}">{{ nPlusOneCount }}</p>
|
|
98
|
+
<p class="text-xs text-gray-400 mt-1">duplicate query patterns</p>
|
|
99
|
+
</div>
|
|
100
|
+
|
|
101
|
+
<!-- Cache Hit Rate -->
|
|
102
|
+
<div class="bg-white rounded-xl border border-gray-200 shadow-sm p-5">
|
|
103
|
+
<div class="flex items-center justify-between mb-3">
|
|
104
|
+
<p class="text-xs font-semibold text-gray-500 uppercase tracking-wide">Cache Hit Rate</p>
|
|
105
|
+
<span class="p-1.5 bg-green-50 rounded-lg">
|
|
106
|
+
<svg class="w-4 h-4 text-green-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
107
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
|
108
|
+
</svg>
|
|
109
|
+
</span>
|
|
110
|
+
</div>
|
|
111
|
+
<p class="text-3xl font-bold text-gray-900">{{ cacheHitRate }}<span class="text-base font-medium text-gray-400 ml-0.5">%</span></p>
|
|
112
|
+
<p class="text-xs text-gray-400 mt-1">{{ totalCacheOps }} total operations</p>
|
|
113
|
+
</div>
|
|
114
|
+
|
|
115
|
+
<!-- Memory Usage -->
|
|
116
|
+
<div class="bg-white rounded-xl border border-gray-200 shadow-sm p-5">
|
|
117
|
+
<div class="flex items-center justify-between mb-3">
|
|
118
|
+
<p class="text-xs font-semibold text-gray-500 uppercase tracking-wide">Heap Used</p>
|
|
119
|
+
<span class="p-1.5 bg-slate-50 rounded-lg">
|
|
120
|
+
<svg class="w-4 h-4 text-slate-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
121
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 3H5a2 2 0 00-2 2v4m6-6h10a2 2 0 012 2v4M9 3v18m0 0h10a2 2 0 002-2V9M9 21H5a2 2 0 01-2-2V9m0 0h18"/>
|
|
122
|
+
</svg>
|
|
123
|
+
</span>
|
|
124
|
+
</div>
|
|
125
|
+
<p class="text-3xl font-bold text-gray-900">{{ heapUsed }}<span class="text-base font-medium text-gray-400 ml-1">MB</span></p>
|
|
126
|
+
<p class="text-xs text-gray-400 mt-1">{{ heapTotal }} MB total / {{ rss }} MB RSS</p>
|
|
127
|
+
</div>
|
|
128
|
+
</div>
|
|
129
|
+
|
|
130
|
+
<!-- Mid Row: Distributions + Seq Scans -->
|
|
131
|
+
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
132
|
+
|
|
133
|
+
<!-- HTTP Method Distribution -->
|
|
134
|
+
<div class="bg-white rounded-xl border border-gray-200 shadow-sm p-5">
|
|
135
|
+
<h3 class="text-sm font-semibold text-gray-700 mb-4">Requests by Method</h3>
|
|
136
|
+
{{{ methodBars }}}
|
|
137
|
+
</div>
|
|
138
|
+
|
|
139
|
+
<!-- Status Code Distribution -->
|
|
140
|
+
<div class="bg-white rounded-xl border border-gray-200 shadow-sm p-5">
|
|
141
|
+
<h3 class="text-sm font-semibold text-gray-700 mb-4">Requests by Status</h3>
|
|
142
|
+
{{{ statusBars }}}
|
|
143
|
+
</div>
|
|
144
|
+
|
|
145
|
+
<!-- Query Issue Breakdown -->
|
|
146
|
+
<div class="bg-white rounded-xl border border-gray-200 shadow-sm p-5">
|
|
147
|
+
<h3 class="text-sm font-semibold text-gray-700 mb-4">Query Issues</h3>
|
|
148
|
+
<div class="space-y-3">
|
|
149
|
+
<div class="flex items-center justify-between">
|
|
150
|
+
<div class="flex items-center gap-2">
|
|
151
|
+
<span class="w-2.5 h-2.5 rounded-full bg-yellow-400"></span>
|
|
152
|
+
<span class="text-sm text-gray-600">Slow (>100ms)</span>
|
|
153
|
+
</div>
|
|
154
|
+
<span class="text-sm font-semibold text-gray-800">{{ slowQueries }}</span>
|
|
155
|
+
</div>
|
|
156
|
+
<div class="flex items-center justify-between">
|
|
157
|
+
<div class="flex items-center gap-2">
|
|
158
|
+
<span class="w-2.5 h-2.5 rounded-full bg-orange-400"></span>
|
|
159
|
+
<span class="text-sm text-gray-600">N+1 Patterns</span>
|
|
160
|
+
</div>
|
|
161
|
+
<span class="text-sm font-semibold text-gray-800">{{ nPlusOneCount }}</span>
|
|
162
|
+
</div>
|
|
163
|
+
<div class="flex items-center justify-between">
|
|
164
|
+
<div class="flex items-center gap-2">
|
|
165
|
+
<span class="w-2.5 h-2.5 rounded-full bg-red-400"></span>
|
|
166
|
+
<span class="text-sm text-gray-600">Seq Scans</span>
|
|
167
|
+
</div>
|
|
168
|
+
<span class="text-sm font-semibold text-gray-800">{{ seqScanCount }}</span>
|
|
169
|
+
</div>
|
|
170
|
+
<div class="pt-3 border-t border-gray-100">
|
|
171
|
+
<div class="flex items-center justify-between">
|
|
172
|
+
<span class="text-xs text-gray-400">Total queries</span>
|
|
173
|
+
<span class="text-xs font-semibold text-gray-600">{{ totalQueries }}</span>
|
|
174
|
+
</div>
|
|
175
|
+
</div>
|
|
176
|
+
</div>
|
|
177
|
+
</div>
|
|
178
|
+
</div>
|
|
179
|
+
|
|
180
|
+
<!-- Bottom Row: Top Slow Endpoints + Top Slow Queries -->
|
|
181
|
+
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
182
|
+
|
|
183
|
+
<!-- Top Slow Endpoints -->
|
|
184
|
+
<div class="bg-white rounded-xl border border-gray-200 shadow-sm overflow-hidden">
|
|
185
|
+
<div class="px-5 py-4 border-b border-gray-100">
|
|
186
|
+
<h3 class="text-sm font-semibold text-gray-700">Top Slowest Endpoints</h3>
|
|
187
|
+
<p class="text-xs text-gray-400 mt-0.5">Ranked by average response time</p>
|
|
188
|
+
</div>
|
|
189
|
+
{{{ topEndpointsTable }}}
|
|
190
|
+
</div>
|
|
191
|
+
|
|
192
|
+
<!-- Top Slow Queries -->
|
|
193
|
+
<div class="bg-white rounded-xl border border-gray-200 shadow-sm overflow-hidden">
|
|
194
|
+
<div class="px-5 py-4 border-b border-gray-100">
|
|
195
|
+
<h3 class="text-sm font-semibold text-gray-700">Top Slowest Queries</h3>
|
|
196
|
+
<p class="text-xs text-gray-400 mt-0.5">Ranked by execution time</p>
|
|
197
|
+
</div>
|
|
198
|
+
{{{ topQueriesTable }}}
|
|
199
|
+
</div>
|
|
200
|
+
</div>
|
|
201
|
+
|
|
202
|
+
<!-- Recent Errors -->
|
|
203
|
+
{{{ recentErrorsSection }}}
|
|
204
|
+
|
|
205
|
+
</div>
|