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"
               },