Top 10 Requests
Completed requests in the selected window, ranked by their larger inbound or outbound size. Select a row for details.
- | Rank | Direction | Host | Client address | Size |
+ | Rank | Direction | Host | Client address | Size | Completed |
No completed requests in this window.
@@ -73,9 +75,17 @@ let selected = new Set(), latest = null, source;
const $ = id => document.getElementById(id);
function bytesPerSecond(value) { const bits = value * 8; if (bits >= 1e9) return (bits/1e9).toFixed(1)+' Gbit/s'; if (bits >= 1e6) return (bits/1e6).toFixed(1)+' Mbit/s'; if (bits >= 1e3) return (bits/1e3).toFixed(1)+' Kbit/s'; return Math.round(bits)+' bit/s'; }
function duration(v) { return v == null ? '—' : v.toFixed(3)+' s'; }
+function formatTimestamp(timestamp) {
+ return new Date(timestamp*1000).toLocaleString(undefined, {year:'numeric', month:'short', day:'numeric', hour:'numeric', minute:'2-digit', second:'2-digit', timeZoneName:'short'});
+}
+function timeAgo(timestamp, now = Date.now()) {
+ const seconds = Math.max(0, Math.floor((now - timestamp*1000)/1000));
+ const [value, unit] = seconds < 60 ? [seconds, 'second'] : seconds < 3600 ? [Math.floor(seconds/60), 'minute'] : seconds < 86400 ? [Math.floor(seconds/3600), 'hour'] : [Math.floor(seconds/86400), 'day'];
+ return `${value} ${unit}${value === 1 ? '' : 's'} ago`;
+}
function applyTheme(value) { const actual = value === 'system' ? (matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light') : value; document.documentElement.dataset.theme = actual; document.documentElement.style.colorScheme = actual; if (typeof trafficChart !== 'undefined') { trafficChart.options.scales.x.grid.color = getComputedStyle(document.documentElement).getPropertyValue('--chart-grid'); hostChart.options.scales.x.grid.color = trafficChart.options.scales.x.grid.color; trafficChart.update(); hostChart.update(); } }
-function metric(label, value) { return `
`; }
-function renderMetrics(s) { const status = s.status.reduce((a,b)=>a+b,0); const errors = status ? ((s.status[2]+s.status[3])/status*100).toFixed(1)+'%' : '0.0%'; $('metrics').innerHTML = metric('Inbound', bytesPerSecond(s.incoming))+metric('Outbound', bytesPerSecond(s.outgoing))+metric('Requests', s.requests.toFixed(1)+'/sec')+metric('Transferred', `
${formatBytes(s.total_incoming+s.total_outgoing)}`)+metric('Avg latency', duration(s.latency))+metric('p95 latency', duration(s.p95))+metric('Upstream avg', duration(s.upstream))+metric('Errors (4xx/5xx)', errors)+metric('Active hosts', s.active_hosts)+metric('Busiest host', escapeHtml(s.busiest_host || '—')); }
+function metric(label, value, wide = false) { return `
`; }
+function renderMetrics(s) { const status = s.status.reduce((a,b)=>a+b,0); const errors = status ? ((s.status[2]+s.status[3])/status*100).toFixed(1)+'%' : '0.0%'; $('metrics').innerHTML = metric('Inbound', bytesPerSecond(s.incoming))+metric('Outbound', bytesPerSecond(s.outgoing))+metric('Requests', s.requests.toFixed(1)+'/sec')+metric('Transferred', `
${formatBytes(s.total_incoming+s.total_outgoing)}`)+metric('Avg latency', duration(s.latency))+metric('p95 latency', duration(s.p95))+metric('Upstream avg', duration(s.upstream))+metric('Errors (4xx/5xx)', errors)+metric('Active hosts', s.active_hosts)+metric('Busiest host', escapeHtml(s.busiest_host || '—'), true); }
function formatBytes(n) { const units=['B','KB','MB','GB','TB']; let i=0; while(n>=1000 && i
bytesPerSecond(v)}}}, plugins:{legend:{display:true}}}}); }
const trafficChart = makeChart('traffic-chart',[{label:'Inbound',data:[],borderColor:'#3273dc',backgroundColor:'#3273dc',tension:.25,pointRadius:0},{label:'Outbound',data:[],borderColor:'#23d160',backgroundColor:'#23d160',tension:.25,pointRadius:0}]);
@@ -87,7 +97,7 @@ function escapeHtml(s) { return s.replace(/[&<>'"]/g,c=>({'&':'&','<':'<'
let openedRequest = null, openerId = null, modalGeneration = 0;
function requestFields(record) {
return [
- ['Request ID', String(record.id)], ['Completed at', new Date(record.timestamp*1000).toISOString()],
+ ['Request ID', String(record.id)], ['Completed at', formatTimestamp(record.timestamp)],
['Host', record.host], ['Client address', record.client_address || 'Unavailable'],
['Method', record.method], ['Path', record.path || 'Unavailable'], ['Status', String(record.status)],
['Direction', record.direction === 'inbound' ? 'Inbound' : 'Outbound'],
@@ -131,6 +141,12 @@ function renderRequests(data) {
}
row.lastElementChild.title = `${record.size} bytes`;
row.lastElementChild.className = 'request-size';
+ const completed = document.createElement('td');
+ completed.dataset.timestamp = String(record.timestamp);
+ completed.textContent = timeAgo(record.timestamp);
+ completed.title = formatTimestamp(record.timestamp);
+ completed.style.whiteSpace = 'nowrap';
+ row.append(completed);
row.onclick = () => openRequest(record);
row.onkeydown = event => { if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); openRequest(record); } };
return row;
@@ -141,6 +157,12 @@ function renderRequests(data) {
(rows.find(row => row.dataset.requestId === focusedId) || $('top-requests-title')).focus();
}
}
+setInterval(() => {
+ const now = Date.now();
+ document.querySelectorAll('#request-rows [data-timestamp]').forEach(cell => {
+ cell.textContent = timeAgo(Number(cell.dataset.timestamp), now);
+ });
+}, 1000);
$('request-close').onclick = () => $('request-dialog').close();
$('request-dialog').addEventListener('click', event => {
const bounds = $('request-dialog').getBoundingClientRect();