From 9cbf6b7ae9377db33c84930508a99362950d6898 Mon Sep 17 00:00:00 2001 From: Costa Tsaousis <costa@netdata.cloud> Date: Tue, 30 Jan 2024 19:14:20 +0200 Subject: [PATCH] Network viewer fixes (#16877) * minor fixes * fix hostname --- collectors/network-viewer.plugin/viewer.html | 371 +++++++++++++++++++ collectors/plugins.d/local-sockets.h | 4 +- health/schema.d/health:alert:prototype.json | 12 +- 3 files changed, 379 insertions(+), 8 deletions(-) create mode 100644 collectors/network-viewer.plugin/viewer.html 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" },