Compare commits

...

2 Commits

Author SHA1 Message Date
d90343b379 TEMP 2024-06-05 22:59:20 +03:00
2048f9c57d TEMP 2024-06-02 19:38:39 +03:00
11 changed files with 902 additions and 439 deletions

1
.nvmrc Normal file
View File

@ -0,0 +1 @@
v22

View File

@ -2,19 +2,39 @@
## Development ## Development
### Backend
Pre-requisites: `pipenv`, or whichever venv manager you prefer
```sh ```sh
# Install all dependencies. `pipenv` was used previously: # Install all dependencies. `pipenv` was used previously:
pipenv --python 3.11 pipenv --python 3.11
pipenv install pipenv install
pipenv shell
# Start dev server # Start dev server
pipenv shell
fastapi dev app/main.py fastapi dev app/main.py
# Start posting data to the tracing endpoint: # Start posting data to the tracing endpoint:
traceroute git.rauhala.info -q1 | http POST localhost:8000/trace/MYHOSTNAME traceroute git.rauhala.info -q1 | http POST localhost:8000/trace/MYHOSTNAME
``` ```
### Frontend
Pre-requisites: `nvm`, `tsc` / `typescript`
```sh
# Install latest npm version with node version manager:
nvm install 22
# Install project packages:
npm install
# Regular usage, activate project node version & start TS compiler in watch mode:
nvm use
tsc -w
```
## URLs of interest ## URLs of interest
- http://localhost:8000/ - http://localhost:8000/

View File

@ -1,95 +0,0 @@
import json
import uuid
import hashlib
from datetime import datetime
from .db import Database
def parse_traceroute_output(data: str, origin: str):
lines = data.strip().split("\n")
target = lines[0].split()[2]
created = datetime.now().isoformat()
trace = {"target": target, "created": created, "origin": origin, "hops": []}
prev_latency = 0
for line in lines[1:]:
hop_info = line.split()
print("LINE:", hop_info)
try:
number, name, ip, latency, _ = hop_info
hop = {
"created": created,
"number": number,
"name": name,
"ip": ip.strip("()"),
"latency": float(latency),
}
except ValueError:
number, name = hop_info
hop = {
"created": created,
"number": number,
"name": name,
"ip": None,
"latency": None,
}
trace["hops"].append(hop)
return trace
def store_traceroute(trace):
db = Database()
# hops_json = json.dumps(trace['hops'])
path_ids = {}
previous_hop_ip = None
previous_hop_latency = None
for hop in trace["hops"]:
hop_number = hop["number"]
hop_name = hop.get("name")
hop_ip = hop.get("ip")
hop_latency = hop.get("latency")
link_id = None
# insert links and get their id's
if previous_hop_ip:
link_id = db.create_link(previous_hop_ip, hop_ip)
path_ids[hop_number] = link_id
previous_hop_ip = hop_ip
# Save hop details
db.create_hop(hop_name, hop_ip, hop_latency)
# calculate link latency if possible and store it
if link_id and previous_hop_latency:
link_latency = hop_latency - previous_hop_latency
db.create_latency(link_id, trace["created"], link_latency)
# make entry to "Paths" table
if path_ids:
json_path_ids = json.dumps(path_ids)
db.create_path(node, trace["target"], json_path_ids)
db.end()
def generate_node_id():
mac = uuid.getnode()
mac_str = ":".join(
["{:02x}".format((mac >> ele) & 0xFF) for ele in range(0, 8 * 6, 8)][::-1]
)
# Hash the MAC address using SHA-256 to generate a unique ID
unique_id = hashlib.sha256(mac_str.encode()).hexdigest()
return unique_id

119
app/db.py
View File

@ -25,19 +25,24 @@ class Database:
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
created TEXT NOT NULL, created TEXT NOT NULL,
origin TEXT NOT NULL, origin TEXT NOT NULL,
target TEXT NOT NULL target TEXT NOT NULL,
unparsed TEXT NOT NULL
); );
CREATE TABLE IF NOT EXISTS Hops ( CREATE TABLE IF NOT EXISTS Nodes (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
trace_id INTEGER,
created TEXT NOT NULL, created TEXT NOT NULL,
number INTEGER NOT NULL,
name TEXT, name TEXT,
ip TEXT, ip TEXT,
latency TEXT, latency_ms REAL NOT NULL,
);
FOREIGN KEY(trace_id) REFERENCES Traces(id) CREATE TABLE IF NOT EXISTS Links (
id INTEGER PRIMARY KEY AUTOINCREMENT,
created TEXT NOT NULL,
source TEXT NOT NULL,
target TEXT NOT NULL,
latency_ms REAL NOT NULL,
); );
""" """
) )
@ -48,6 +53,7 @@ class Database:
self.conn.commit() self.conn.commit()
self.conn.close() self.conn.close()
# TODO
def list_traces(self): def list_traces(self):
# TODO: time filter # TODO: time filter
result = [] result = []
@ -59,7 +65,12 @@ class Database:
trace = dict(t) trace = dict(t)
self.cursor.execute( self.cursor.execute(
"SELECT number, name, ip, latency FROM Hops WHERE trace_id = ? ORDER BY number ASC", """
SELECT number, name, ip, latency, link_latency
FROM Hops
WHERE trace_id = ?
ORDER BY number ASC
""",
(trace["id"],), (trace["id"],),
) )
hops = self.cursor.fetchall() hops = self.cursor.fetchall()
@ -69,6 +80,7 @@ class Database:
return result return result
# TODO
def create_trace(self, trace): def create_trace(self, trace):
self.cursor.execute( self.cursor.execute(
"INSERT OR IGNORE INTO Traces (created, origin, target) VALUES (?, ?, ?)", "INSERT OR IGNORE INTO Traces (created, origin, target) VALUES (?, ?, ?)",
@ -78,14 +90,15 @@ class Database:
for hop in trace["hops"]: for hop in trace["hops"]:
self.cursor.execute( self.cursor.execute(
"INSERT OR IGNORE INTO Hops (trace_id, created, number, name, ip, latency) VALUES (?, ?, ?, ?, ?, ?)", "INSERT OR IGNORE INTO Hops (trace_id, created, number, name, ip, latency, link_latency) VALUES (?, ?, ?, ?, ?, ?, ?)",
( (
trace_id, trace_id,
hop["created"], hop["created"], # TODO: trace.created
hop["number"], hop["number"],
hop["name"], hop["name"],
hop["ip"], hop["ip"],
hop["latency"], hop["latency"],
hop["link_latency"],
), ),
) )
@ -97,96 +110,8 @@ class Database:
(name, ip, latency), (name, ip, latency),
) )
def create_latency(self, link_id, timestamp, link_latency):
self.cursor.execute(
"INSERT INTO Latency (link_id, timestamp, latency_ms) VALUES (?, ?, ?)",
(link_id, timestamp, link_latency),
)
def create_path(self, node, target, json):
self.cursor.execute(
"INSERT OR IGNORE INTO Paths (node, target, hops_json) VALUES (?, ?, ?)",
(node, target, json),
)
def ensure_table_setup(): def ensure_table_setup():
db = Database() db = Database()
db.create_tables() db.create_tables()
db.end() db.end()
####################################################################
####################################################################
####################################################################
####################################################################
def with_connection(func):
@wraps(func)
def wrapped(*args, **kwargs):
conn = sqlite3.connect(DB_FILE)
cursor = conn.cursor()
result = func(cursor, *args, **kwargs)
conn.commit()
conn.close()
return result
return wrapped
@with_connection
def init_db(cursor: Cursor):
cursor.executescript(
"""
CREATE TABLE IF NOT EXISTS Links (
id INTEGER PRIMARY KEY,
source_ip TEXT NOT NULL,
destination_ip TEXT NOT NULL,
UNIQUE(source_ip, destination_ip)
);
CREATE TABLE IF NOT EXISTS Paths (
id INTEGER PRIMARY KEY,
node TEXT NOT NULL,
target TEXT NOT NULL,
hops_json TEXT NOT NULL,
UNIQUE(node, target, hops_json)
);
CREATE TABLE IF NOT EXISTS Latency (
id INTEGER PRIMARY KEY,
link_id INTEGER NOT NULL,
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
latency_ms REAL NOT NULL,
FOREIGN KEY (link_id) REFERENCES Links(id)
);
CREATE TABLE IF NOT EXISTS HopDetails (
id INTEGER PRIMARY KEY,
hop_name TEXT,
hop_ip TEXT,
hop_latency TEXT
);
"""
)
@with_connection
def insert_hop(cursor: Cursor, previous_hop_ip: str, hop_ip: str):
"""Insert a new hop and return related Link id"""
cursor.execute(
"INSERT OR IGNORE INTO Links (source_ip, destination_ip) VALUES (?, ?)",
(previous_hop_ip, hop_ip),
)
cursor.execute(
"SELECT id FROM Links WHERE source_ip = ? AND destination_ip = ?",
(previous_hop_ip, hop_ip),
)
return cursor.fetchone()

View File

@ -1,7 +1,7 @@
from fastapi import Request, FastAPI from fastapi import Request, FastAPI
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
from .collector import parse_traceroute_output, store_traceroute from .parser import parse_traceroute_output
from .db import Database, ensure_table_setup from .db import Database, ensure_table_setup
from pprint import pprint as print from pprint import pprint as print

119
app/parser.py Executable file
View File

@ -0,0 +1,119 @@
import json
import uuid
import hashlib
from datetime import datetime
from .db import Database
def parse_node(hop_info: list[str], target, origin):
try:
# Regular lines.
number, name, ip, latency, _ = hop_info
latency = float(latency)
ip = (ip.strip("()"),)
return {
"render_id": ip,
"hop_number": number,
"name": name,
"ip": ip,
"latency_ms": latency,
}
except ValueError:
# Asterisks, no data found for hop.
number, name = hop_info
return {
"render_id": f"{origin}-{target}-{number}",
"hop_number": number,
"name": name,
"ip": None,
"latency_ms": None,
}
def parse_link(node, prev_node):
latency = (
node["latency"] - prev_node["latency"]
if prev_node is not None
else node["latency"]
)
return {
"source": prev_node.get("render_id"),
"target": node["render_id"],
"latency_ms": latency,
}
def parse_traceroute_output(data: str, origin: str):
# TODO: data validation
lines = data.strip().split("\n")
target = lines[0].split()[2]
created = datetime.now().isoformat()
trace = {"target": target, "created": created, "origin": origin, "hops": []}
prev_node = None
for line in lines[1:]:
hop_info = line.split()
print("LINE:", hop_info)
node = parse_node(hop_info, target, origin)
link = parse_link(node, prev_node)
trace["nodes"].append(node)
trace["links"].append(link)
prev_node = node
return trace
# def store_traceroute(trace):
# # hops_json = json.dumps(trace['hops'])
#
# path_ids = {}
#
# previous_hop_ip = None
# previous_hop_latency = None
# for hop in trace["hops"]:
# hop_number = hop["number"]
# hop_name = hop.get("name")
# hop_ip = hop.get("ip")
# hop_latency = hop.get("latency")
# link_id = None
#
# # insert links and get their id's
# if previous_hop_ip:
# link_id = db.create_link(previous_hop_ip, hop_ip)
# path_ids[hop_number] = link_id
#
# previous_hop_ip = hop_ip
#
# # Save hop details
# db.create_hop(hop_name, hop_ip, hop_latency)
#
# # calculate link latency if possible and store it
# if link_id and previous_hop_latency:
# link_latency = hop_latency - previous_hop_latency
# db.create_latency(link_id, trace["created"], link_latency)
#
# # make entry to "Paths" table
# if path_ids:
# json_path_ids = json.dumps(path_ids)
# db.create_path(node, trace["target"], json_path_ids)
# def generate_node_id():
# mac = uuid.getnode()
# mac_str = ":".join(
# ["{:02x}".format((mac >> ele) & 0xFF) for ele in range(0, 8 * 6, 8)][::-1]
# )
#
# # Hash the MAC address using SHA-256 to generate a unique ID
# unique_id = hashlib.sha256(mac_str.encode()).hexdigest()
#
# return unique_id

184
app/static/example.js Normal file
View File

@ -0,0 +1,184 @@
/**
* This example showcases sigma's reducers, which aim to facilitate dynamically
* changing the appearance of nodes and edges, without actually changing the
* main graphology data.
*/
import Graph from "graphology";
import Sigma from "sigma";
import { Coordinates, EdgeDisplayData, NodeDisplayData } from "sigma/types";
import { onStoryDown } from "../utils";
import data from "./data.json";
export default () => {
// Retrieve some useful DOM elements:
const container = document.getElementById("sigma-container") as HTMLElement;
const searchInput = document.getElementById("search-input") as HTMLInputElement;
const searchSuggestions = document.getElementById("suggestions") as HTMLDataListElement;
// Instantiate sigma:
const graph = new Graph();
graph.import(data);
const renderer = new Sigma(graph, container);
// Type and declare internal state:
interface State {
hoveredNode?: string;
searchQuery: string;
// State derived from query:
selectedNode?: string;
suggestions?: Set<string>;
// State derived from hovered node:
hoveredNeighbors?: Set<string>;
}
const state: State = { searchQuery: "" };
// Feed the datalist autocomplete values:
searchSuggestions.innerHTML = graph
.nodes()
.map((node) => `<option value="${graph.getNodeAttribute(node, "label")}"></option>`)
.join("\n");
// Actions:
function setSearchQuery(query: string) {
state.searchQuery = query;
if (searchInput.value !== query) searchInput.value = query;
if (query) {
const lcQuery = query.toLowerCase();
const suggestions = graph
.nodes()
.map((n) => ({ id: n, label: graph.getNodeAttribute(n, "label") as string }))
.filter(({ label }) => label.toLowerCase().includes(lcQuery));
// If we have a single perfect match, them we remove the suggestions, and
// we consider the user has selected a node through the datalist
// autocomplete:
if (suggestions.length === 1 && suggestions[0].label === query) {
state.selectedNode = suggestions[0].id;
state.suggestions = undefined;
// Move the camera to center it on the selected node:
const nodePosition = renderer.getNodeDisplayData(state.selectedNode) as Coordinates;
renderer.getCamera().animate(nodePosition, {
duration: 500,
});
}
// Else, we display the suggestions list:
else {
state.selectedNode = undefined;
state.suggestions = new Set(suggestions.map(({ id }) => id));
}
}
// If the query is empty, then we reset the selectedNode / suggestions state:
else {
state.selectedNode = undefined;
state.suggestions = undefined;
}
// Refresh rendering
// You can directly call `renderer.refresh()`, but if you need performances
// you can provide some options to the refresh method.
// In this case, we don't touch the graph data so we can skip its reindexation
renderer.refresh({
skipIndexation: true,
});
}
function setHoveredNode(node?: string) {
if (node) {
state.hoveredNode = node;
state.hoveredNeighbors = new Set(graph.neighbors(node));
}
// Compute the partial that we need to re-render to optimize the refresh
const nodes = graph.filterNodes((n) => n !== state.hoveredNode && !state.hoveredNeighbors?.has(n));
const nodesIndex = new Set(nodes);
const edges = graph.filterEdges((e) => graph.extremities(e).some((n) => nodesIndex.has(n)));
if (!node) {
state.hoveredNode = undefined;
state.hoveredNeighbors = undefined;
}
// Refresh rendering
renderer.refresh({
partialGraph: {
nodes,
edges,
},
// We don't touch the graph data so we can skip its reindexation
skipIndexation: true,
});
}
// Bind search input interactions:
searchInput.addEventListener("input", () => {
setSearchQuery(searchInput.value || "");
});
searchInput.addEventListener("blur", () => {
setSearchQuery("");
});
// Bind graph interactions:
renderer.on("enterNode", ({ node }) => {
setHoveredNode(node);
});
renderer.on("leaveNode", () => {
setHoveredNode(undefined);
});
// Render nodes accordingly to the internal state:
// 1. If a node is selected, it is highlighted
// 2. If there is query, all non-matching nodes are greyed
// 3. If there is a hovered node, all non-neighbor nodes are greyed
renderer.setSetting("nodeReducer", (node, data) => {
const res: Partial<NodeDisplayData> = { ...data };
if (state.hoveredNeighbors && !state.hoveredNeighbors.has(node) && state.hoveredNode !== node) {
res.label = "";
res.color = "#f6f6f6";
}
if (state.selectedNode === node) {
res.highlighted = true;
} else if (state.suggestions) {
if (state.suggestions.has(node)) {
res.forceLabel = true;
} else {
res.label = "";
res.color = "#f6f6f6";
}
}
return res;
});
// Render edges accordingly to the internal state:
// 1. If a node is hovered, the edge is hidden if it is not connected to the
// node
// 2. If there is a query, the edge is only visible if it connects two
// suggestions
renderer.setSetting("edgeReducer", (edge, data) => {
const res: Partial<EdgeDisplayData> = { ...data };
if (state.hoveredNode && !graph.hasExtremity(edge, state.hoveredNode)) {
res.hidden = true;
}
if (
state.suggestions &&
(!state.suggestions.has(graph.source(edge)) || !state.suggestions.has(graph.target(edge)))
) {
res.hidden = true;
}
return res;
});
onStoryDown(() => {
renderer.kill();
});
};

View File

@ -4,9 +4,6 @@
<title>Kalzu</title> <title>Kalzu</title>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<script src="https://cdnjs.cloudflare.com/ajax/libs/sigma.js/2.4.0/sigma.min.js" integrity="sha512-iiPEYww3QXZU5C795JnnINBRNgHqDnRHs9mA7aJoqx4pNE4u3CknCDGmePHFoHtKR/6C9aIcRFa+HJ6obtlteQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/graphology/0.25.4/graphology.umd.min.js" integrity="sha512-tjMBhL9fLMcqoccPOwpRiIQIOAyUh18lWUlUvE10zvG1UNMfxUC4qSERmUq+VF30iavIyqs/q6fSP2o475FAUw==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="/static/d3.v7.min.js"></script>
<style> <style>
#container { #container {
height: 95vh; height: 95vh;

View File

@ -1,36 +1,43 @@
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
const linkArc = (d) => { const linkArc = (d) => {
const r = Math.hypot(d.target.x - d.source.x, d.target.y - d.source.y) * 3; const r = Math.hypot(d.target.x - d.source.x, d.target.y - d.source.y) * 3;
return ` return `
M${d.source.x},${d.source.y} M${d.source.x},${d.source.y}
A${r},${r} 0 0,1 ${d.target.x},${d.target.y} A${r},${r} 0 0,1 ${d.target.x},${d.target.y}
`; `;
}; };
const drag = (simulation) => { const drag = (simulation) => {
function dragstarted(event, d) { function dragstarted(event, d) {
if (!event.active) simulation.alphaTarget(0.3).restart(); if (!event.active)
d.fx = d.x; simulation.alphaTarget(0.3).restart();
d.fy = d.y; d.fx = d.x;
} d.fy = d.y;
}
function dragged(event, d) { function dragged(event, d) {
d.fx = event.x; d.fx = event.x;
d.fy = event.y; d.fy = event.y;
} }
function dragended(event, d) {
function dragended(event, d) { if (!event.active)
if (!event.active) simulation.alphaTarget(0); simulation.alphaTarget(0);
d.fx = null; d.fx = null;
d.fy = null; d.fy = null;
} }
return d3
return d3 .drag()
.drag() .on("start", dragstarted)
.on("start", dragstarted) .on("drag", dragged)
.on("drag", dragged) .on("end", dragended);
.on("end", dragended);
}; };
// const drawChart = (data) => { // const drawChart = (data) => {
// // Specify the dimensions of the chart. // // Specify the dimensions of the chart.
// const width = 1600; // const width = 1600;
@ -129,229 +136,284 @@ const drag = (simulation) => {
// //
// return svg.node(); // return svg.node();
// }; // };
// const drawChart2 = (data) => {
const drawChart2 = (data) => { // // Data parsing
// Data parsing // const nodes = Array.from(
const nodes = Array.from( // new Set(data.flatMap((l) => [l.source, l.target])),
new Set(data.flatMap((l) => [l.source, l.target])), // (id) => ({ id }),
(id) => ({ id }), // );
); // const links = data.map((d) => Object.create(d));
const links = data.map((d) => Object.create(d)); //
// // Styles
// Styles // const width = window.visualViewport.width;
const width = window.visualViewport.width; // const height = window.visualViewport.height - 10;
const height = window.visualViewport.height - 10; //
// const colors = Array.from(new Set(data.map((d) => d.traceId)));
const colors = Array.from(new Set(data.map((d) => d.traceId))); // const color = d3.scaleOrdinal(colors, d3.schemeCategory10);
const color = d3.scaleOrdinal(colors, d3.schemeCategory10); //
// const simulation = d3
const simulation = d3 // .forceSimulation(nodes)
.forceSimulation(nodes) // .force(
.force( // "link",
"link", // d3.forceLink(links).id((d) => d.id),
d3.forceLink(links).id((d) => d.id), // )
) // .force("charge", d3.forceManyBody().strength(-400))
.force("charge", d3.forceManyBody().strength(-400)) // .force("x", d3.forceX())
.force("x", d3.forceX()) // .force("y", d3.forceY());
.force("y", d3.forceY()); //
// // Canvas settings
// Canvas settings // const svg = d3
const svg = d3 // .create("svg")
.create("svg") // .attr("width", width)
.attr("width", width) // .attr("height", height)
.attr("height", height) // .attr("viewBox", [-width / 2, -height / 2, width, height])
.attr("viewBox", [-width / 2, -height / 2, width, height]) // .attr("style", "max-width: 100%; height: auto; font: 14px monospace;");
.attr("style", "max-width: 100%; height: auto; font: 14px monospace;"); //
// // Pre-type arrowheads, as they don't inherit styles.
// Pre-type arrowheads, as they don't inherit styles. // svg
svg // .append("defs")
.append("defs") // .selectAll("marker")
.selectAll("marker") // .data(colors)
.data(colors) // .join("marker")
.join("marker") // .attr("id", (d) => `arrow-${d}`)
.attr("id", (d) => `arrow-${d}`) // .attr("viewBox", "0 -5 10 10")
.attr("viewBox", "0 -5 10 10") // .attr("refX", 15)
.attr("refX", 15) // .attr("refY", -0.5)
.attr("refY", -0.5) // .attr("markerWidth", 5)
.attr("markerWidth", 5) // .attr("markerHeight", 5)
.attr("markerHeight", 5) // .attr("orient", "auto")
.attr("orient", "auto") // .append("path")
.append("path") // .attr("fill", color)
.attr("fill", color) // .attr("d", "M0,-5L10,0L0,5");
.attr("d", "M0,-5L10,0L0,5"); //
// const link = svg
const link = svg // .append("g")
.append("g") // .attr("fill", "none")
.attr("fill", "none") // .attr("stroke-width", 1.5)
.attr("stroke-width", 1.5) // .selectAll("path")
.selectAll("path") // .data(links)
.data(links) // .join("path")
.join("path") // .attr("stroke", (d) => color(d.traceId))
.attr("stroke", (d) => color(d.traceId)) // .attr(
.attr( // "marker-end",
"marker-end", // (d) => `url(${new URL(`#arrow-${d.traceId}`, location)})`,
(d) => `url(${new URL(`#arrow-${d.traceId}`, location)})`, // );
); //
// const node = svg
const node = svg // .append("g")
.append("g") // .attr("fill", "currentColor")
.attr("fill", "currentColor") // .attr("stroke-linecap", "round")
.attr("stroke-linecap", "round") // .attr("stroke-linejoin", "round")
.attr("stroke-linejoin", "round") // .selectAll("g")
.selectAll("g") // .data(nodes)
.data(nodes) // .join("g")
.join("g") // .call(drag(simulation));
.call(drag(simulation)); //
// // Node "icon"
// Node "icon" // node
node // .append("circle")
.append("circle") // .attr("stroke", "white")
.attr("stroke", "white") // .attr("stroke-width", 1.5)
.attr("stroke-width", 1.5) // .attr("r", 5);
.attr("r", 5); //
// // Node text
// Node text // node
node // .append("text")
.append("text") // .attr("x", 8)
.attr("x", 8) // .attr("y", 4)
.attr("y", 4) // .text((d) => (d.id.endsWith("*") ? "*" : d.id))
.text((d) => (d.id.endsWith("*") ? "*" : d.id)) // .clone(true)
.clone(true) // .lower()
.lower() // .attr("fill", "black")
.attr("fill", "black") // .attr("stroke", "white")
.attr("stroke", "white") // .attr("stroke-width", 2);
.attr("stroke-width", 2); //
// simulation.on("tick", () => {
simulation.on("tick", () => { // link.attr("d", linkArc);
link.attr("d", linkArc); // // link
// link // // .attr("x1", (d) => d.source.x)
// .attr("x1", (d) => d.source.x) // // .attr("y1", (d) => d.source.y)
// .attr("y1", (d) => d.source.y) // // .attr("x2", (d) => d.target.x)
// .attr("x2", (d) => d.target.x) // // .attr("y2", (d) => d.target.y);
// .attr("y2", (d) => d.target.y); // node.attr("transform", (d) => `translate(${d.x},${d.y})`);
node.attr("transform", (d) => `translate(${d.x},${d.y})`); // });
}); //
// // invalidation.then(() => simulation.stop());
// invalidation.then(() => simulation.stop()); //
// return Object.assign(svg.node(), {
return Object.assign(svg.node(), { scales: { color } }); // scales: { color },
}; // });
// };
const state = {};
const drawSigma = (data) => { const drawSigma = (data) => {
// Create a graphology graph // Create a graphology graph
const graph = new graphology.MultiDirectedGraph(); const graph = new graphology.MultiDirectedGraph();
const setHoveredNode = (node) => {
data.nodes.forEach((n, i) => { if (node) {
console.log("Node:", n); const props = graph.getNodeAttributes(node);
try { state.hoveredNode = node;
graph.addNode(n.id, n); state.hoveredTrace = props.traceId;
} catch (e) { // Compute the partial that we need to re-render to optimize the refresh
console.log("Node add:", e); const nodes = graph.filterNodes((n) => {
} const np = graph.getNodeAttributes(n);
}); return np.traceId === state.hoveredTrace;
});
data.links.forEach((l, i) => { const nodesIndex = new Set(nodes);
graph.addEdge(l.source, l.target, l); const edges = graph.filterEdges((e) => graph.extremities(e).some((n) => nodesIndex.has(n)));
}); // Refresh rendering
renderer.refresh({
// Instantiate sigma.js and render the graph partialGraph: {
const renderer = new Sigma(graph, document.getElementById("container"), { nodes,
labelThreshold: -10000, edges,
}); },
// We don't touch the graph data so we can skip its reindexation
skipIndexation: true,
});
}
else {
state.hoveredNode = undefined;
state.hoveredTrace = undefined;
// Refresh rendering
renderer.refresh({
// We don't touch the graph data so we can skip its reindexation
skipIndexation: true,
});
}
};
data.nodes.forEach((n) => {
try {
graph.addNode(n.id, n);
}
catch (e) {
// Duplicate node found, which is correct in our data scenario.
}
});
data.links.forEach((l) => {
graph.addEdge(l.source, l.target, l);
});
// Instantiate sigma.js and render the graph
const renderer = new Sigma(graph, document.getElementById("container"), {
// labelThreshold: -10000,
renderEdgeLabels: true,
});
// Bind graph interactions:
renderer.on("enterNode", ({ node }) => {
console.log("enter:", node);
setHoveredNode(node);
});
renderer.on("leaveNode", () => {
setHoveredNode(undefined);
});
// Render nodes accordingly to the internal state:
// 1. If a node is selected, it is highlighted
// 2. If there is query, all non-matching nodes are greyed
// 3. If there is a hovered node, all non-neighbor nodes are greyed
renderer.setSetting("nodeReducer", (node, data) => {
const res = Object.assign({}, data);
const props = graph.getNodeAttributes(node);
if (state.hoveredTrace && props.traceId !== state.hoveredTrace) {
res.label = "";
res.color = "#f6f6f6";
}
return res;
});
// Render edges accordingly to the internal state:
// 1. If a node is hovered, the edge is hidden if it is not connected to the
// node
// 2. If there is a query, the edge is only visible if it connects two
// suggestions
renderer.setSetting("edgeReducer", (edge, data) => {
const res = Object.assign({}, data);
const props = graph.getEdgeAttributes(edge);
if (state.hoveredTrace && props.traceId !== state.hoveredTrace) {
res.hidden = true;
}
return res;
});
}; };
const getNodeID = (hop, prevId, trace) => { const getNodeID = (hop, prevId, trace) => {
if (prevId === null) { if (prevId === null) {
return trace.origin; return trace.origin;
} }
if (hop.name === "*") { if (hop.name === "*") {
return `${trace.id}-${hop.number}-*`; return `${trace.target}-${hop.number}-*`;
} //return `${trace.id}-${hop.number}-*`;
}
return hop.ip; return hop.ip;
}; };
const parseNodesAndLinks = (traces) => { const parseNodesAndLinks = (traces) => {
const nodes = []; const nodes = [];
const links = []; const links = [];
const colors = Array.from(new Set(traces.map((t) => t.id)));
const colors = Array.from(new Set(traces.map((t) => t.id))); const color = d3.scaleOrdinal(colors, d3.schemeCategory10);
const color = d3.scaleOrdinal(colors, d3.schemeCategory10); traces.forEach((trace) => {
let prevId = null;
traces.forEach((trace) => { let latestNumber = null;
let prevId = null; trace.hops.forEach((hop) => {
let latestNumber = null; const id = getNodeID(hop, prevId, trace);
// New node
trace.hops.forEach((hop) => { nodes.push({
const id = getNodeID(hop, prevId, trace); id: id,
label: id.endsWith("*") ? "*" : id,
// New node x: trace.id,
nodes.push({ y: hop.number / 2,
id: id, traceId: trace.id,
label: id.endsWith("*") ? "*" : id, size: 9,
x: trace.id, labelSize: 30,
y: hop.number / 2, color: color(trace.id),
size: 9, });
labelSize: 30, if (prevId) {
color: color(trace.id), // New link
}); links.push({
label: hop.link_latency,
if (prevId) { source: prevId,
// New link target: id,
links.push({ traceId: trace.id,
label: "asd", origin: trace.origin,
source: prevId, size: 3,
target: id, color: color(trace.id),
traceId: trace.id, });
origin: trace.origin, }
size: 1, prevId = id;
color: color(trace.id), latestNumber = hop.number;
});
/**
* Last "destination" node, just for candy
*/
nodes.push({
id: trace.id,
label: trace.target,
traceId: trace.id,
x: trace.id,
y: (latestNumber + 1) / 2,
size: 8,
color: "black",
});
links.push({
label: "-",
size: 8,
traceId: trace.id,
source: prevId,
target: trace.id,
}); });
}
prevId = id;
latestNumber = hop.number;
}); });
// { id: ip, group: origin, radius: 2 }
/** // { source: prev.ip, target: ip, value: latency }
* Last "destination" node, just for candy return {
*/ nodes,
nodes.push({ links,
id: trace.id, };
label: trace.target,
x: trace.id,
y: (latestNumber + 1) / 2,
size: 8,
color: "black",
});
links.push({
source: prevId,
target: trace.id,
});
});
// { id: ip, group: origin, radius: 2 }
// { source: prev.ip, target: ip, value: latency }
return {
nodes,
links,
};
}; };
function main() {
async function main() { return __awaiter(this, void 0, void 0, function* () {
const response = await fetch("/trace/"); const response = yield fetch("/trace/");
const traces = await response.json(); const traces = yield response.json();
console.log("Traces:", traces);
console.log("Traces:", traces); const data = parseNodesAndLinks(traces);
const data = parseNodesAndLinks(traces); console.log("Data:", data);
// const chart = drawChart2(data.links);
console.log("Data:", data); const chart = drawSigma(data);
// const chart = drawChart2(data.links); // container.append(chart);
});
const chart = drawSigma(data);
// container.append(chart);
} }
main(); main();

240
app/static/index.ts Normal file
View File

@ -0,0 +1,240 @@
import { MultiDirectedGraph } from "graphology";
import Sigma from "sigma";
import { Coordinates, EdgeDisplayData, NodeDisplayData } from "sigma/types";
// Nice soft greens
const colors = ["#E8F9C2", "#C0DF81", "#96C832", "#59761E", "#263409"];
interface Node {
id: string;
label: string;
x: number;
y: number;
size: number;
color: string;
}
interface Link {
label: string;
size: number;
traceId: string;
source: string;
target: string;
}
interface Data {
nodes: Node[];
links: Link[];
}
interface State {
hoveredTrace: string | null;
hoveredNode: string | null;
}
const state: State = {
hoveredTrace: null,
hoveredNode: null,
};
const drawSigma = (data: Data) => {
// Create a graphology graph
const graph = new MultiDirectedGraph();
// Add nodes
data.nodes.forEach((n) => {
try {
graph.addNode(n.id, n);
} catch (e) {
// Duplicate node found, which is correct in our data scenario.
}
});
// Add links
data.links.forEach((l) => {
graph.addEdge(l.source, l.target, l);
});
const container = document.getElementById("container") as HTMLElement;
// Instantiate sigma.js and render the graph
const renderer = new Sigma(graph, container, {
// labelThreshold: -10000,
renderEdgeLabels: true,
});
// Bind graph interactions:
renderer.on("enterNode", ({ node }) => {
console.log("enter:", node);
if (!node) {
return;
}
const props = graph.getNodeAttributes(node);
state.hoveredNode = node;
state.hoveredTrace = props.traceId;
// Compute the partial that we need to re-render to optimize the refresh
const nodes = graph.filterNodes((n) => {
const np = graph.getNodeAttributes(n);
return np.traceId === state.hoveredTrace;
});
const nodesIndex = new Set(nodes);
const edges = graph.filterEdges((e) =>
graph.extremities(e).some((n) => nodesIndex.has(n)),
);
// Refresh rendering
renderer.refresh({
partialGraph: {
nodes,
edges,
},
// We don't touch the graph data so we can skip its reindexation
skipIndexation: true,
});
});
renderer.on("leaveNode", () => {
state.hoveredNode = null;
state.hoveredTrace = null;
// Refresh rendering
renderer.refresh({
// We don't touch the graph data so we can skip its reindexation
skipIndexation: true,
});
});
// Render nodes accordingly to the internal state:
// 1. If a node is selected, it is highlighted
// 2. If there is query, all non-matching nodes are greyed
// 3. If there is a hovered node, all non-neighbor nodes are greyed
renderer.setSetting("nodeReducer", (node, data) => {
const res = { ...data };
const props = graph.getNodeAttributes(node);
if (state.hoveredTrace && props.traceId !== state.hoveredTrace) {
res.label = "";
res.color = "#f6f6f6";
}
return res;
});
// Render edges accordingly to the internal state:
// 1. If a node is hovered, the edge is hidden if it is not connected to the
// node
// 2. If there is a query, the edge is only visible if it connects two
// suggestions
renderer.setSetting("edgeReducer", (edge, data) => {
const res = { ...data };
const props = graph.getEdgeAttributes(edge);
if (state.hoveredTrace && props.traceId !== state.hoveredTrace) {
res.hidden = true;
}
return res;
});
};
// const getNodeID = (hop: number, prevId: string, trace) => {
// if (prevId === null) {
// return trace.origin;
// }
// if (hop.name === "*") {
// return `${trace.target}-${hop.number}-*`;
// //return `${trace.id}-${hop.number}-*`;
// }
//
// return hop.ip;
// };
// const parseNodesAndLinks = (traces) => {
// const nodes = [];
// const links = [];
//
// const colors = Array.from(new Set(traces.map((t) => t.id)));
// // TODO: Replace this with something
// const color = d3.scaleOrdinal(colors, d3.schemeCategory10);
//
// traces.forEach((trace) => {
// let prevId = null;
// let latestNumber = null;
//
// trace.hops.forEach((hop) => {
// const id = getNodeID(hop, prevId, trace);
//
// // New node
// nodes.push({
// id: id,
// label: id.endsWith("*") ? "*" : id,
// x: trace.id,
// y: hop.number / 2,
// traceId: trace.id,
// size: 9,
// labelSize: 30,
// color: color(trace.id),
// });
//
// if (prevId) {
// // New link
// links.push({
// label: hop.link_latency,
// source: prevId,
// target: id,
// traceId: trace.id,
// origin: trace.origin,
// size: 3,
// color: color(trace.id),
// });
// }
//
// prevId = id;
// latestNumber = hop.number;
// });
//
// /**
// * Last "destination" node, just for candy
// */
// nodes.push({
// id: trace.id,
// label: trace.target,
// traceId: trace.id,
// x: trace.id,
// y: (latestNumber + 1) / 2,
// size: 8,
// color: "black",
// });
//
// links.push({
// label: "-",
// size: 8,
// traceId: trace.id,
// source: prevId,
// target: trace.id,
// });
// });
//
// // { id: ip, group: origin, radius: 2 }
// // { source: prev.ip, target: ip, value: latency }
// return {
// nodes,
// links,
// };
// };
async function main() {
const response = await fetch("/trace/");
const data = await response.json();
console.log("Traces:", data);
console.log("Data:", data);
drawSigma(data);
}
main();

10
tsconfig.json Normal file
View File

@ -0,0 +1,10 @@
{
"compilerOptions": {
"target": "es2016",
"module": "commonjs",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true
}
}