The ASN explorer now displays bytes, protocol, RTT and client links.

This commit is contained in:
Herbert Wolverson 2024-07-27 09:16:29 -05:00
parent b4d1d5deff
commit 80995b7f95
2 changed files with 125 additions and 64 deletions

View File

@ -10,12 +10,14 @@ let asnList = [];
let asnData = []; let asnData = [];
let graphMinTime = Number.MAX_SAFE_INTEGER; let graphMinTime = Number.MAX_SAFE_INTEGER;
let graphMaxTime = Number.MIN_SAFE_INTEGER; let graphMaxTime = Number.MIN_SAFE_INTEGER;
let throughputDownMax = 0;
let throughputUpMax = 0;
const itemsPerPage = 20; const itemsPerPage = 20;
let page = 0; let page = 0;
function unixTimeToDate(unixTime) {
return new Date(unixTime * 1000).toLocaleString();
}
function asnDropdown() { function asnDropdown() {
$.get(LIST_URL, (data) => { $.get(LIST_URL, (data) => {
asnList = data; asnList = data;
@ -45,7 +47,7 @@ function asnDropdown() {
// Add items // Add items
data.forEach((row) => { data.forEach((row) => {
let li = document.createElement("li"); let li = document.createElement("li");
li.innerHTML = row.name + " (" + row.count + ")"; li.innerHTML = "#" + row.asn + " " + row.name + " (" + row.count + ")";
li.classList.add("dropdown-item"); li.classList.add("dropdown-item");
li.onclick = () => { li.onclick = () => {
selectAsn(row.asn); selectAsn(row.asn);
@ -92,14 +94,14 @@ function renderAsn(asn, data) {
return a.start - b.start; return a.start - b.start;
}); });
// Build the flows display let div = document.createElement("div");
let flowsDiv = document.createElement("div"); div.classList.add("row");
let minTime = Number.MAX_SAFE_INTEGER; let minTime = Number.MAX_SAFE_INTEGER;
let maxTime = Number.MIN_SAFE_INTEGER; let maxTime = Number.MIN_SAFE_INTEGER;
for (let i= page * itemsPerPage; i<(page+1) * itemsPerPage; i++) {
if (i >= data.length) break;
let row = data[i];
// Calculate time overall
data.forEach((row) => {
// Update min/max time // Update min/max time
if (row.start < minTime) { if (row.start < minTime) {
minTime = row.start; minTime = row.start;
@ -107,63 +109,87 @@ function renderAsn(asn, data) {
if (row.end > maxTime) { if (row.end > maxTime) {
maxTime = row.end; maxTime = row.end;
} }
});
let div = document.createElement("div");
div.classList.add("row");
// Build the heading
let headingCol = document.createElement("div");
headingCol.classList.add("col-1");
let ht = "<p class='text-secondary small'>" + scaleNumber(row.total_bytes.down, 0) + " / " + scaleNumber(row.total_bytes.up);
if (row.rtt[0] !== undefined) {
ht += "<br /> RTT: " + scaleNanos(row.rtt[0].nanoseconds, 0);
} else {
ht += "<br /> RTT: -";
}
if (row.rtt[1] !== undefined) {
ht += " / " + scaleNanos(row.rtt[1].nanoseconds, 0);
}
ht += "</p>";
headingCol.innerHTML = ht;
//div.appendChild(headingCol);
// Build a canvas div, we'll decorate this later
let canvasCol = document.createElement("div");
canvasCol.classList.add("col-12");
let canvas = document.createElement("canvas");
canvas.id = "flowCanvas" + i;
canvas.style.width = "100%";
canvas.style.height = "30px";
canvasCol.appendChild(canvas);
div.appendChild(canvasCol);
flowsDiv.appendChild(div);
}
// Store the global time range // Store the global time range
graphMinTime = minTime; graphMinTime = minTime;
graphMaxTime = maxTime; graphMaxTime = maxTime;
// Calculate the max down and up for every item // Header row (explain the columns)
let maxDown = 0; let headerDiv = document.createElement("div");
let maxUp = 0; headerDiv.classList.add("row");
data.forEach((row) => { let headerBytes = document.createElement("div");
row.throughput.forEach((value) => { headerBytes.classList.add("col-1", "text-secondary");
if (value.down > maxDown) { headerBytes.innerText = "Bytes";
maxDown = value.down; headerDiv.appendChild(headerBytes);
let headerRtt = document.createElement("div");
headerRtt.classList.add("col-1", "text-secondary");
headerRtt.innerText = "RTT";
headerDiv.appendChild(headerRtt);
let headerClient = document.createElement("div");
headerClient.classList.add("col-1", "text-secondary");
headerClient.innerText = "Client";
headerDiv.appendChild(headerClient);
let headerProtocol = document.createElement("div");
headerProtocol.classList.add("col-1", "text-secondary");
headerProtocol.innerText = "Protocol";
headerDiv.appendChild(headerProtocol);
let headerTime1 = document.createElement("div");
headerTime1.classList.add("col-4", "text-secondary");
headerTime1.innerText = unixTimeToDate(minTime);
headerDiv.appendChild(headerTime1);
let headerTime2 = document.createElement("div");
headerTime2.classList.add("col-4", "text-secondary", "text-end");
console.log(maxTime);
headerTime2.innerText = unixTimeToDate(maxTime);
headerDiv.appendChild(headerTime2);
let flowsDiv = document.createElement("div");
for (let i= page * itemsPerPage; i<(page+1) * itemsPerPage; i++) {
if (i >= data.length) break;
let row = data[i];
// Build the headings
let totalCol = document.createElement("div");
totalCol.classList.add("col-1", "text-secondary", "small");
totalCol.innerText = scaleNumber(row.total_bytes.down, 0) + " / " + scaleNumber(row.total_bytes.up);
div.appendChild(totalCol);
let rttCol = document.createElement("div");
rttCol.classList.add("col-1", "text-secondary", "small");
let rttDown = row.rtt[0] !== undefined ? scaleNanos(row.rtt[0].nanoseconds, 0) : "-";
let rttUp = row.rtt[1] !== undefined ? scaleNanos(row.rtt[1].nanoseconds, 0) : "-";
rttCol.innerText = rttDown + " / " + rttUp;
div.appendChild(rttCol);
let clientCol = document.createElement("div");
clientCol.classList.add("col-1", "text-secondary", "small");
if (row.circuit_id !== "") {
let clientLink = document.createElement("a");
clientLink.href = "/circuit/" + encodeURI(row.circuit_id);
clientLink.innerText = row.circuit_name;
clientCol.appendChild(clientLink);
} else {
clientCol.innerText = row.circuit_name;
} }
if (value.up > maxUp) { div.appendChild(clientCol);
maxUp = value.up;
} let protocolCol = document.createElement("div");
}); protocolCol.classList.add("col-1", "text-secondary", "small");
}); protocolCol.innerText = row.protocol;
if (maxDown > throughputDownMax) { div.appendChild(protocolCol);
throughputDownMax = maxDown;
} // Build a canvas div, we'll decorate this later
if (maxUp > throughputUpMax) { let canvasCol = document.createElement("div");
throughputUpMax = maxUp; canvasCol.classList.add("col-8");
let canvas = document.createElement("canvas");
canvas.id = "flowCanvas" + i;
canvas.style.width = "100%";
canvas.style.height = "20px";
canvasCol.appendChild(canvas);
div.appendChild(canvasCol);
flowsDiv.appendChild(div);
} }
// Apply the data to the page // Apply the data to the page
@ -182,17 +208,25 @@ function renderAsn(asn, data) {
let prevButton = document.createElement("button"); let prevButton = document.createElement("button");
nextButton.classList.add("btn", "btn-secondary", "btn-sm", "me-2"); nextButton.classList.add("btn", "btn-secondary", "btn-sm", "me-2");
prevButton.innerHTML = "<i class='fa fa-arrow-left'></i> Previous"; prevButton.innerHTML = "<i class='fa fa-arrow-left'></i> Prev";
prevButton.onclick = () => { prevButton.onclick = () => {
page--; page--;
if (page < 0) page = 0; if (page < 0) page = 0;
renderAsn(asn, data); renderAsn(asn, data);
} }
let paginator = document.createElement("span");
paginator.classList.add("text-secondary", "small", "ms-2", "me-2");
paginator.innerText = "Page " + (page + 1) + " of " + Math.ceil(data.length / itemsPerPage);
paginator.id = "paginator";
let controlDiv = document.createElement("div"); let controlDiv = document.createElement("div");
controlDiv.classList.add("mb-2"); controlDiv.classList.add("mb-2");
controlDiv.appendChild(prevButton); controlDiv.appendChild(prevButton);
controlDiv.appendChild(paginator);
controlDiv.appendChild(nextButton); controlDiv.appendChild(nextButton);
target.appendChild(controlDiv); target.appendChild(controlDiv);
target.appendChild(headerDiv);
target.appendChild(flowsDiv); target.appendChild(flowsDiv);
@ -257,6 +291,18 @@ function drawTimeline() {
ctx.lineTo(timeToX(row.end, width), height / 2); ctx.lineTo(timeToX(row.end, width), height / 2);
ctx.stroke(); ctx.stroke();
// Calculate maxThroughputUp and maxThroughputDown for this row
let maxThroughputDown = 0;
let maxThroughputUp = 0;
row.throughput.forEach((value) => {
if (value.down > maxThroughputDown) {
maxThroughputDown = value.down;
}
if (value.up > maxThroughputUp) {
maxThroughputUp = value.up;
}
});
// Draw a throughput down line. Y from y/2 to height, scaled to maxThroughputDown // Draw a throughput down line. Y from y/2 to height, scaled to maxThroughputDown
ctx.strokeStyle = lineColor; ctx.strokeStyle = lineColor;
ctx.beginPath(); ctx.beginPath();
@ -267,9 +313,9 @@ function drawTimeline() {
let sampleWidth = (endX - startX) / numberOfSamples; let sampleWidth = (endX - startX) / numberOfSamples;
let x = timeToX(row.start, width); let x = timeToX(row.start, width);
ctx.moveTo(x, height/2); ctx.moveTo(x, height/2);
let trimmedHeight = height - 10; let trimmedHeight = height - 4;
row.throughput.forEach((value, index) => { row.throughput.forEach((value, index) => {
let downPercent = value.down / throughputDownMax; let downPercent = value.down / maxThroughputDown;
let y = (height/2) - (downPercent * (trimmedHeight / 2)); let y = (height/2) - (downPercent * (trimmedHeight / 2));
ctx.lineTo(x, y); ctx.lineTo(x, y);
@ -280,7 +326,7 @@ function drawTimeline() {
x = timeToX(row.start, width); x = timeToX(row.start, width);
ctx.moveTo(x, height/2); ctx.moveTo(x, height/2);
row.throughput.forEach((value, index) => { row.throughput.forEach((value, index) => {
let upPercent = value.up / throughputUpMax; let upPercent = value.up / maxThroughputUp;
let y = (height/2) + (upPercent * (trimmedHeight / 2)); let y = (height/2) + (upPercent * (trimmedHeight / 2));
ctx.lineTo(x, y); ctx.lineTo(x, y);

View File

@ -4,6 +4,7 @@ use axum::Json;
use serde::Serialize; use serde::Serialize;
use lqos_utils::units::DownUpOrder; use lqos_utils::units::DownUpOrder;
use lqos_utils::unix_time::{time_since_boot, unix_now}; use lqos_utils::unix_time::{time_since_boot, unix_now};
use crate::shaped_devices_tracker::SHAPED_DEVICES;
use crate::throughput_tracker::flow_data::{AsnListEntry, RECENT_FLOWS, RttData}; use crate::throughput_tracker::flow_data::{AsnListEntry, RECENT_FLOWS, RttData};
pub async fn asn_list() -> Json<Vec<AsnListEntry>> { pub async fn asn_list() -> Json<Vec<AsnListEntry>> {
@ -21,6 +22,9 @@ pub struct FlowTimeline {
retransmit_times_down: Vec<u64>, retransmit_times_down: Vec<u64>,
retransmit_times_up: Vec<u64>, retransmit_times_up: Vec<u64>,
total_bytes: DownUpOrder<u64>, total_bytes: DownUpOrder<u64>,
protocol: String,
circuit_id: String,
circuit_name: String,
} }
pub async fn flow_timeline(Path(asn_id): Path<u32>) -> Json<Vec<FlowTimeline>> { pub async fn flow_timeline(Path(asn_id): Path<u32>) -> Json<Vec<FlowTimeline>> {
@ -38,6 +42,14 @@ pub async fn flow_timeline(Path(asn_id): Path<u32>) -> Json<Vec<FlowTimeline>> {
}) })
.map(|flow| { .map(|flow| {
let (circuit_id, mut circuit_name) = {
let sd = SHAPED_DEVICES.read().unwrap();
sd.get_circuit_id_and_name_from_ip(&flow.0.local_ip).unwrap_or((String::new(), String::new()))
};
if circuit_name.is_empty() {
circuit_name = flow.0.local_ip.as_ip().to_string();
}
FlowTimeline { FlowTimeline {
start: boot_time + Duration::from_nanos(flow.1.start_time).as_secs(), start: boot_time + Duration::from_nanos(flow.1.start_time).as_secs(),
end: boot_time + Duration::from_nanos(flow.1.last_seen).as_secs(), end: boot_time + Duration::from_nanos(flow.1.last_seen).as_secs(),
@ -54,6 +66,9 @@ pub async fn flow_timeline(Path(asn_id): Path<u32>) -> Json<Vec<FlowTimeline>> {
.map(|t| boot_time + Duration::from_nanos(*t).as_secs()) .map(|t| boot_time + Duration::from_nanos(*t).as_secs())
.collect(), .collect(),
total_bytes: flow.1.bytes_sent.clone(), total_bytes: flow.1.bytes_sent.clone(),
protocol: flow.2.protocol_analysis.to_string(),
circuit_id,
circuit_name,
} }
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();