summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorCosta Tsaousis <costa@netdata.cloud>2024-01-30 19:14:20 +0200
committerGitHub <noreply@github.com>2024-01-30 19:14:20 +0200
commit9cbf6b7ae9377db33c84930508a99362950d6898 (patch)
tree383f0eadc3099b91ae31aa1c81bc03e6ff031988
parent5d960b419ee4fde0aba5c1d4e456d96b25f4e021 (diff)
Network viewer fixes (#16877)
* minor fixes * fix hostname
-rw-r--r--collectors/network-viewer.plugin/viewer.html371
-rw-r--r--collectors/plugins.d/local-sockets.h4
-rw-r--r--health/schema.d/health:alert:prototype.json12
3 files changed, 379 insertions, 8 deletions
diff --git a/collectors/network-viewer.plugin/viewer.html b/collectors/network-viewer.plugin/viewer.html
new file mode 100644
index 0000000000..673dbbc3eb
--- /dev/null
+++ b/collectors/network-viewer.plugin/viewer.html
@@ -0,0 +1,371 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>Network Viewer</title>
+ <style>
+ /* Styles to make the canvas full width and height */
+ body, html {
+ margin: 0;
+ padding: 0;
+ height: 100%;
+ width: 100%;
+ }
+ #d3-canvas {
+ height: 100%;
+ width: 100%;
+ }
+ </style>
+ <link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400&display=swap" rel="stylesheet">
+ <!-- Include D3.js -->
+ <script src="https://d3js.org/d3.v7.min.js"></script>
+ <script>
+
+ // The transformData function
+ function transformData(dataPayload, desiredColumnNames, columns) {
+ console.log(dataPayload);
+
+ const transformedData = [];
+
+ // Create a map to store data per application
+ const appMap = new Map();
+
+ dataPayload.data.forEach(row => {
+ const rowData = {};
+ desiredColumnNames.forEach(columnName => {
+ const columnIndex = columns[columnName].index;
+ rowData[columnName] = row[columnIndex];
+ });
+
+ const appName = rowData['Process'];
+ if (!appMap.has(appName)) {
+ appMap.set(appName, {
+ listenCount: 0,
+ inboundCount: 0,
+ outboundCount: 0,
+ localCount: 0,
+ privateCount: 0,
+ publicCount: 0,
+ totalCount: 0
+ });
+ }
+
+ const appData = appMap.get(appName);
+ appData.totalCount++;
+
+ if (rowData['Direction'] === 'listen') {
+ appData.listenCount++;
+ }
+ else if (rowData['Direction'] === 'local') {
+ appData.localCount++;
+ }
+ else if (rowData['Direction'] === 'inbound') {
+ appData.inboundCount++;
+ }
+ else if (rowData['Direction'] === 'outbound') {
+ appData.outboundCount++;
+ }
+
+ if (rowData['RemoteAddressSpace'] === 'public') {
+ appData.publicCount++;
+ }
+ else if (rowData['RemoteAddressSpace'] === 'private') {
+ appData.privateCount++;
+ }
+ });
+
+ // Convert the map to an array format
+ for (let [appName, appData] of appMap) {
+ transformedData.push({
+ name: appName,
+ ...appData
+ });
+ }
+
+ console.log(transformedData);
+
+ return transformedData;
+ }
+ </script>
+</head>
+<body>
+<div id="d3-canvas"></div> <!-- Div for D3 rendering -->
+
+<script>
+ // Function to draw the circles and labels for each application with border forces
+ function drawApplications(data) {
+ console.log(data);
+ console.log(data.length)
+ const maxTotalCount = d3.max(data, d => d.totalCount);
+ const maxXCount = d3.max(data, d => Math.abs(Math.max(d.publicCount, d.privateCount)));
+ const maxStrength = 0.005;
+ const borderPadding = 40;
+ console.log("maxTotalCount", maxTotalCount);
+ console.log("maxXCount", maxXCount);
+
+ const max = {
+ totalCount: d3.max(data, d => d.totalCount),
+ localCount: d3.max(data, d => d.localCount),
+ listenCount: d3.max(data, d => d.listenCount),
+ privateCount: d3.max(data, d => d.privateCount),
+ publicCount: d3.max(data, d => d.publicCount),
+ inboundCount: d3.max(data, d => d.inboundCount),
+ outboundCount: d3.max(data, d => d.outboundCount),
+ }
+
+ const w = window.innerWidth;
+ const h = window.innerHeight;
+ const cw = w / 2;
+ const ch = h / 2;
+ console.log(w, h, cw, ch);
+ console.log(w, h, cw, ch);
+
+ const minSize = 13;
+ const maxSize = Math.max(5, Math.min(w, h) / data.length) + minSize; // Avoid division by zero or too large sizes
+
+ const publicColor = "#bbaa00";
+ const privateColor = "#5555ff";
+ const serverColor = "#009900";
+ const clientColor = "#990000";
+
+ function hexToHalfOpacityRGBA(hex) {
+ // Ensure the hex color is valid
+ if (hex.length !== 7 || hex[0] !== '#') {
+ throw new Error('Invalid hex color format');
+ }
+
+ // Extract the red, green, and blue components
+ const r = parseInt(hex.slice(1, 3), 16);
+ const g = parseInt(hex.slice(3, 5), 16);
+ const b = parseInt(hex.slice(5, 7), 16);
+
+ // Return the color in RGBA format with half opacity
+ return `rgba(${r}, ${g}, ${b}, 0.5)`;
+ }
+
+ const pieColors = d3.scaleOrdinal()
+ .domain(["publicCount", "privateCount", "listenInboundCount", "outboundCount", "others"])
+ .range([publicColor, privateColor, serverColor, clientColor, "#666666"]); // Example colors
+
+ const pie = d3.pie().value(d => d.value);
+ const arc = d3.arc();
+
+ function getPieData(d) {
+ const others = d.totalCount - (d.publicCount + d.privateCount + d.listenCount + d.inboundCount + d.outboundCount);
+ return [
+ {value: d.publicCount},
+ {value: d.privateCount},
+ {value: d.listenCount + d.inboundCount},
+ {value: d.outboundCount},
+ {value: others > 0 ? others : 0}
+ ];
+ }
+
+ const forceStrengthScale = d3.scaleLinear()
+ .domain([0, maxTotalCount])
+ .range([0, maxStrength]);
+
+ const circleSize = d3.scaleLog()
+ .domain([1, maxTotalCount]) // Assuming maxTotalCount is the maximum value in your data
+ .range([minSize, maxSize])
+ .clamp(true); // Clamps the output so that it stays within the range
+
+ const logScaleRight = d3.scaleLog().domain([1, max.publicCount + 1]).range([0, cw - borderPadding]);
+ const logScaleLeft = d3.scaleLog().domain([1, max.privateCount + 1]).range([0, cw - borderPadding]);
+ const logScaleTop = d3.scaleLog().domain([1, max.outboundCount + 1]).range([0, ch - borderPadding]);
+ const logScaleBottom = d3.scaleLog().domain([1, (max.listenCount + max.inboundCount) / 2 + 1]).range([0, ch - borderPadding]);
+
+ data.forEach((d, i) => {
+ const forces = {
+ total: d.totalCount / max.totalCount,
+ local: d.localCount / max.localCount,
+ listen: d.listenCount / max.listenCount,
+ private: d.privateCount / max.privateCount,
+ public: d.publicCount / max.publicCount,
+ inbound: d.inboundCount / max.inboundCount,
+ outbound: d.outboundCount / max.outboundCount,
+ }
+
+ const pos = {
+ right: logScaleRight(d.publicCount + 1),
+ left: logScaleLeft(d.privateCount + 1),
+ top: logScaleTop(d.outboundCount + 1),
+ bottom: logScaleBottom((d.listenCount + d.inboundCount) / 2 + 1),
+ };
+
+ d.targetX = cw + pos.right - pos.left;
+ d.targetY = ch + pos.bottom - pos.top;
+
+ if(d.name === 'deluged')
+ console.log("object", d, "forces", forces, "pos", pos);
+ });
+
+
+ console.log(data);
+
+ const svg = d3.select('#d3-canvas').append('svg')
+ .attr('width', '100%')
+ .attr('height', '100%');
+
+ // Top area - Clients
+ svg.append('rect')
+ .attr('x', 0)
+ .attr('y', 0)
+ .attr('width', '100%')
+ .attr('height', borderPadding / 2) // Adjust height as needed
+ .style('fill', hexToHalfOpacityRGBA(clientColor));
+
+ svg.append('text')
+ .text('Clients')
+ .attr('x', '50%')
+ .attr('y', borderPadding / 2 - 4) // Adjust y position based on the rectangle's height
+ .attr('text-anchor', 'middle')
+ .style('font-family', 'IBM Plex Sans')
+ .style('font-size', '14px')
+ .style('font-weight', 'bold'); // Make the font bold
+
+ // Bottom area - Servers
+ svg.append('rect')
+ .attr('x', 0)
+ .attr('y', h - borderPadding / 2)
+ .attr('width', '100%')
+ .attr('height', borderPadding / 2) // Adjust height as needed
+ .style('fill', hexToHalfOpacityRGBA(serverColor));
+
+ svg.append('text')
+ .text('Servers')
+ .attr('x', '50%')
+ .attr('y', h - borderPadding / 2 + 16) // Adjust y position based on the rectangle's height
+ .attr('text-anchor', 'middle')
+ .style('font-family', 'IBM Plex Sans')
+ .style('font-size', '14px')
+ .style('font-weight', 'bold'); // Make the font bold
+
+ svg.append('rect')
+ .attr('x', w - borderPadding / 2) // Position it close to the right edge
+ .attr('y', 0)
+ .attr('width', borderPadding / 2) // Width of the border area
+ .attr('height', '100%')
+ .style('fill', hexToHalfOpacityRGBA(publicColor));
+
+ svg.append('text')
+ .text('Public')
+ .attr('x', w - (borderPadding / 2)) // Position close to the right edge
+ .attr('y', ch - 10) // Vertically centered
+ .attr('text-anchor', 'middle')
+ .attr('dominant-baseline', 'middle') // Center alignment of the text
+ .attr('transform', `rotate(90, ${w - (borderPadding / 2)}, ${ch})`) // Rotate around the text's center
+ .style('font-family', 'IBM Plex Sans')
+ .style('font-size', '14px')
+ .style('font-weight', 'bold'); // Make the font bold
+
+ svg.append('rect')
+ .attr('x', 0) // Positioned at the left edge
+ .attr('y', 0)
+ .attr('width', borderPadding / 2) // Width of the border area
+ .attr('height', '100%')
+ .style('fill', hexToHalfOpacityRGBA(privateColor));
+
+ svg.append('text')
+ .text('Private')
+ .attr('x', borderPadding / 2) // Position close to the left edge
+ .attr('y', ch) // Vertically centered
+ .attr('text-anchor', 'middle')
+ .attr('dominant-baseline', 'middle') // Center alignment of the text
+ .attr('transform', `rotate(-90, ${borderPadding / 2 - 10}, ${ch})`) // Rotate around the text's center
+ .style('font-family', 'IBM Plex Sans')
+ .style('font-size', '14px')
+ .style('font-weight', 'bold'); // Make the font bold
+
+ function boundaryForce(alpha) {
+ return function(d) {
+ const nodeRadius = circleSize(d.totalCount) / 2;
+ d.x = Math.max(borderPadding + nodeRadius, Math.min(w - borderPadding - nodeRadius, d.x));
+ d.y = Math.max(borderPadding + nodeRadius, Math.min(h - borderPadding - nodeRadius, d.y));
+ };
+ }
+
+ const simulation = d3.forceSimulation(data)
+ .force('center', d3.forceCenter(window.innerWidth / 2, window.innerHeight / 2).strength(1)) // Scale center force strength
+ .force("x", d3.forceX(d => d.targetX).strength(0.3))
+ .force("y", d3.forceY(d => d.targetY).strength(0.3))
+ .force("charge", d3.forceManyBody().strength(-0.5))
+ .force("collide", d3.forceCollide(d => circleSize(d.totalCount) * 1.1 + 15).strength(1))
+ .force("boundary", boundaryForce(0.5))
+ .on('tick', ticked);
+
+ const app = svg.selectAll('.app')
+ .data(data)
+ .enter().append('g')
+ .attr('class', 'app')
+ .call(d3.drag()
+ .on('start', dragstarted)
+ .on('drag', dragged)
+ .on('end', dragended));
+
+ app.each(function(d) {
+ const group = d3.select(this);
+ const pieData = pie(getPieData(d));
+ const radius = circleSize(d.totalCount);
+
+ group.selectAll('path')
+ .data(pieData)
+ .enter().append('path')
+ .attr('d', arc.innerRadius(0).outerRadius(radius))
+ .attr('fill', (d, i) => pieColors(i));
+ });
+
+ app.append('text')
+ .text(d => d.name)
+ .attr('text-anchor', 'middle')
+ .attr('y', d => circleSize(d.totalCount) + 10)
+ .style('font-family', 'IBM Plex Sans') // Set the font family
+ .style('font-size', '12px') // Set the font size
+ .style('font-weight', 'bold'); // Make the font bold
+
+ // Initialize app positions at the center
+ app.attr('transform', `translate(${window.innerWidth / 2}, ${window.innerHeight / 2})`);
+
+ function ticked() {
+ app.attr('transform', d => `translate(${d.x}, ${d.y})`);
+ }
+
+ function dragstarted(event, d) {
+ if (!event.active) simulation.alphaTarget(1).restart();
+ d.fx = d.x;
+ d.fy = d.y;
+ }
+
+ function dragged(event, d) {
+ d.fx = event.x;
+ d.fy = event.y;
+ }
+
+ function dragended(event, d) {
+ if (!event.active) simulation.alphaTarget(0);
+ d.fx = null;
+ d.fy = null;
+ }
+ }
+
+ // Modify your fetchData function to call drawApplications after data transformation
+ function fetchData() {
+ fetch('http://localhost:19999/api/v1/function?function=network-viewer')
+ .then(response => response.json())
+ .then(data => {
+ // Your existing code
+ const desiredColumns = ["Direction", "Protocol", "Namespace", "Process", "CommandLine", "LocalIP", "LocalPort", "RemoteIP", "RemotePort", "LocalAddressSpace", "RemoteAddressSpace"];
+ const transformed = transformData(data, desiredColumns, data.columns);
+
+ // Now draw the applications with border forces
+ drawApplications(transformed);
+ })
+ .catch(error => console.error('Error fetching data:', error));
+ }
+
+ // Load data on start
+ window.onload = fetchData;
+</script>
+</body>
+</html>
diff --git a/collectors/plugins.d/local-sockets.h b/collectors/plugins.d/local-sockets.h
index b51c2ca854..51d48bd1a0 100644
--- a/collectors/plugins.d/local-sockets.h
+++ b/collectors/plugins.d/local-sockets.h
@@ -170,7 +170,7 @@ typedef struct local_socket {
static inline void local_sockets_log(LS_STATE *ls, const char *format, ...) __attribute__ ((format(__printf__, 2, 3)));
static inline void local_sockets_log(LS_STATE *ls, const char *format, ...) {
if(++ls->stats.errors_encountered == ls->config.max_errors) {
- nd_log(NDLS_COLLECTORS, NDLP_ERR, "LOCAL-LISTENERS: max number of logs reached. Not logging anymore");
+ nd_log(NDLS_COLLECTORS, NDLP_ERR, "LOCAL-SOCKETS: max number of logs reached. Not logging anymore");
return;
}
@@ -183,7 +183,7 @@ static inline void local_sockets_log(LS_STATE *ls, const char *format, ...) {
vsnprintf(buf, sizeof(buf), format, args);
va_end(args);
- nd_log(NDLS_COLLECTORS, NDLP_ERR, "LOCAL-LISTENERS: %s", buf);
+ nd_log(NDLS_COLLECTORS, NDLP_ERR, "LOCAL-SOCKETS: %s", buf);
}
// --------------------------------------------------------------------------------------------------------------------
diff --git a/health/schema.d/health:alert:prototype.json b/health/schema.d/health:alert:prototype.json
index 6340f8f4f5..833af5275b 100644
--- a/health/schema.d/health:alert:prototype.json
+++ b/health/schema.d/health:alert:prototype.json
@@ -452,9 +452,9 @@
"ui:classNames": "dyncfg-grid-col-span-5-2"
},
"value": {
- "ui:classNames": "dyncfg-grid-col-span-1-6",
+ "ui:classNames": "dyncfg-grid dyncfg-grid-col-6 dyncfg-grid-col-span-1-6",
"database_lookup": {
- "ui:classNames": "dyncfg-grid-col-span-1-6",
+ "ui:classNames": "dyncfg-grid dyncfg-grid-col-6 dyncfg-grid-col-span-1-6",
"after": {
"ui:classNames": "dyncfg-grid-col-span-1-1"
},
@@ -479,7 +479,7 @@
}
},
"conditions": {
- "ui:classNames": "dyncfg-grid-col-span-1-6",
+ "ui:classNames": "dyncfg-grid dyncfg-grid-col-6 dyncfg-grid-col-span-1-6",
"warning_condition": {
"ui:classNames": "dyncfg-grid-col-span-1-2"
},
@@ -494,7 +494,7 @@
}
},
"action": {
- "ui:classNames": "dyncfg-grid-col-span-1-6",
+ "ui:classNames": "dyncfg-grid dyncfg-grid-col-6 dyncfg-grid-col-span-1-6",
"execute": {
"ui:classNames": "dyncfg-grid-col-span-1-3"
},
@@ -507,7 +507,7 @@
"delay": {
"ui:Collapsible": true,
"ui:InitiallyExpanded": false,
- "ui:classNames": "dyncfg-grid-col-span-1-6",
+ "ui:classNames": "dyncfg-grid dyncfg-grid-col-6 dyncfg-grid-col-span-1-6",
"up": {
"ui:classNames": "dyncfg-grid-col-span-1-2"
},
@@ -524,7 +524,7 @@
"repeat": {
"ui:Collapsible": true,
"ui:InitiallyExpanded": false,
- "ui:classNames": "dyncfg-grid-col-span-1-6",
+ "ui:classNames": "dyncfg-grid dyncfg-grid-col-6 dyncfg-grid-col-span-1-6",
"enabled": {
"ui:classNames": "dyncfg-grid-col-span-1-2"
},