@cryptotaxi247 / netdata-1 / commits / 9cbf6b7ae

Network viewer fixes (#16877)

* minor fixes * fix hostname

Costa Tsaousis committed Jan 30, 2024 at 19:14 UTC 9cbf6b7ae9377db33c84930508a99362950d6898
3 files changed +379 -8
collectors/network-viewer.plugin/viewer.html new
+371
@@ -0,0 +1,371 @@
1 +<!DOCTYPE html>
2 +<html lang="en">
3 +<head>
4 + <meta charset="UTF-8">
5 + <meta name="viewport" content="width=device-width, initial-scale=1.0">
6 + <title>Network Viewer</title>
7 + <style>
8 + /* Styles to make the canvas full width and height */
9 + body, html {
10 + margin: 0;
11 + padding: 0;
12 + height: 100%;
13 + width: 100%;
14 + }
15 + #d3-canvas {
16 + height: 100%;
17 + width: 100%;
18 + }
19 + </style>
20 + <link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400&display=swap" rel="stylesheet">
21 + <!-- Include D3.js -->
22 + <script src="https://d3js.org/d3.v7.min.js"></script>
23 + <script>
24 +
25 + // The transformData function
26 + function transformData(dataPayload, desiredColumnNames, columns) {
27 + console.log(dataPayload);
28 +
29 + const transformedData = [];
30 +
31 + // Create a map to store data per application
32 + const appMap = new Map();
33 +
34 + dataPayload.data.forEach(row => {
35 + const rowData = {};
36 + desiredColumnNames.forEach(columnName => {
37 + const columnIndex = columns[columnName].index;
38 + rowData[columnName] = row[columnIndex];
39 + });
40 +
41 + const appName = rowData['Process'];
42 + if (!appMap.has(appName)) {
43 + appMap.set(appName, {
44 + listenCount: 0,
45 + inboundCount: 0,
46 + outboundCount: 0,
47 + localCount: 0,
48 + privateCount: 0,
49 + publicCount: 0,
50 + totalCount: 0
51 + });
52 + }
53 +
54 + const appData = appMap.get(appName);
55 + appData.totalCount++;
56 +
57 + if (rowData['Direction'] === 'listen') {
58 + appData.listenCount++;
59 + }
60 + else if (rowData['Direction'] === 'local') {
61 + appData.localCount++;
62 + }
63 + else if (rowData['Direction'] === 'inbound') {
64 + appData.inboundCount++;
65 + }
66 + else if (rowData['Direction'] === 'outbound') {
67 + appData.outboundCount++;
68 + }
69 +
70 + if (rowData['RemoteAddressSpace'] === 'public') {
71 + appData.publicCount++;
72 + }
73 + else if (rowData['RemoteAddressSpace'] === 'private') {
74 + appData.privateCount++;
75 + }
76 + });
77 +
78 + // Convert the map to an array format
79 + for (let [appName, appData] of appMap) {
80 + transformedData.push({
81 + name: appName,
82 + ...appData
83 + });
84 + }
85 +
86 + console.log(transformedData);
87 +
88 + return transformedData;
89 + }
90 + </script>
91 +</head>
92 +<body>
93 +<div id="d3-canvas"></div> <!-- Div for D3 rendering -->
94 +
95 +<script>
96 + // Function to draw the circles and labels for each application with border forces
97 + function drawApplications(data) {
98 + console.log(data);
99 + console.log(data.length)
100 + const maxTotalCount = d3.max(data, d => d.totalCount);
101 + const maxXCount = d3.max(data, d => Math.abs(Math.max(d.publicCount, d.privateCount)));
102 + const maxStrength = 0.005;
103 + const borderPadding = 40;
104 + console.log("maxTotalCount", maxTotalCount);
105 + console.log("maxXCount", maxXCount);
106 +
107 + const max = {
108 + totalCount: d3.max(data, d => d.totalCount),
109 + localCount: d3.max(data, d => d.localCount),
110 + listenCount: d3.max(data, d => d.listenCount),
111 + privateCount: d3.max(data, d => d.privateCount),
112 + publicCount: d3.max(data, d => d.publicCount),
113 + inboundCount: d3.max(data, d => d.inboundCount),
114 + outboundCount: d3.max(data, d => d.outboundCount),
115 + }
116 +
117 + const w = window.innerWidth;
118 + const h = window.innerHeight;
119 + const cw = w / 2;
120 + const ch = h / 2;
121 + console.log(w, h, cw, ch);
122 + console.log(w, h, cw, ch);
123 +
124 + const minSize = 13;
125 + const maxSize = Math.max(5, Math.min(w, h) / data.length) + minSize; // Avoid division by zero or too large sizes
126 +
127 + const publicColor = "#bbaa00";
128 + const privateColor = "#5555ff";
129 + const serverColor = "#009900";
130 + const clientColor = "#990000";
131 +
132 + function hexToHalfOpacityRGBA(hex) {
133 + // Ensure the hex color is valid
134 + if (hex.length !== 7 || hex[0] !== '#') {
135 + throw new Error('Invalid hex color format');
136 + }
137 +
138 + // Extract the red, green, and blue components
139 + const r = parseInt(hex.slice(1, 3), 16);
140 + const g = parseInt(hex.slice(3, 5), 16);
141 + const b = parseInt(hex.slice(5, 7), 16);
142 +
143 + // Return the color in RGBA format with half opacity
144 + return `rgba(${r}, ${g}, ${b}, 0.5)`;
145 + }
146 +
147 + const pieColors = d3.scaleOrdinal()
148 + .domain(["publicCount", "privateCount", "listenInboundCount", "outboundCount", "others"])
149 + .range([publicColor, privateColor, serverColor, clientColor, "#666666"]); // Example colors
150 +
151 + const pie = d3.pie().value(d => d.value);
152 + const arc = d3.arc();
153 +
154 + function getPieData(d) {
155 + const others = d.totalCount - (d.publicCount + d.privateCount + d.listenCount + d.inboundCount + d.outboundCount);
156 + return [
157 + {value: d.publicCount},
158 + {value: d.privateCount},
159 + {value: d.listenCount + d.inboundCount},
160 + {value: d.outboundCount},
161 + {value: others > 0 ? others : 0}
162 + ];
163 + }
164 +
165 + const forceStrengthScale = d3.scaleLinear()
166 + .domain([0, maxTotalCount])
167 + .range([0, maxStrength]);
168 +
169 + const circleSize = d3.scaleLog()
170 + .domain([1, maxTotalCount]) // Assuming maxTotalCount is the maximum value in your data
171 + .range([minSize, maxSize])
172 + .clamp(true); // Clamps the output so that it stays within the range
173 +
174 + const logScaleRight = d3.scaleLog().domain([1, max.publicCount + 1]).range([0, cw - borderPadding]);
175 + const logScaleLeft = d3.scaleLog().domain([1, max.privateCount + 1]).range([0, cw - borderPadding]);
176 + const logScaleTop = d3.scaleLog().domain([1, max.outboundCount + 1]).range([0, ch - borderPadding]);
177 + const logScaleBottom = d3.scaleLog().domain([1, (max.listenCount + max.inboundCount) / 2 + 1]).range([0, ch - borderPadding]);
178 +
179 + data.forEach((d, i) => {
180 + const forces = {
181 + total: d.totalCount / max.totalCount,
182 + local: d.localCount / max.localCount,
183 + listen: d.listenCount / max.listenCount,
184 + private: d.privateCount / max.privateCount,
185 + public: d.publicCount / max.publicCount,
186 + inbound: d.inboundCount / max.inboundCount,
187 + outbound: d.outboundCount / max.outboundCount,
188 + }
189 +
190 + const pos = {
191 + right: logScaleRight(d.publicCount + 1),
192 + left: logScaleLeft(d.privateCount + 1),
193 + top: logScaleTop(d.outboundCount + 1),
194 + bottom: logScaleBottom((d.listenCount + d.inboundCount) / 2 + 1),
195 + };
196 +
197 + d.targetX = cw + pos.right - pos.left;
198 + d.targetY = ch + pos.bottom - pos.top;
199 +
200 + if(d.name === 'deluged')
201 + console.log("object", d, "forces", forces, "pos", pos);
202 + });
203 +
204 +
205 + console.log(data);
206 +
207 + const svg = d3.select('#d3-canvas').append('svg')
208 + .attr('width', '100%')
209 + .attr('height', '100%');
210 +
211 + // Top area - Clients
212 + svg.append('rect')
213 + .attr('x', 0)
214 + .attr('y', 0)
215 + .attr('width', '100%')
216 + .attr('height', borderPadding / 2) // Adjust height as needed
217 + .style('fill', hexToHalfOpacityRGBA(clientColor));
218 +
219 + svg.append('text')
220 + .text('Clients')
221 + .attr('x', '50%')
222 + .attr('y', borderPadding / 2 - 4) // Adjust y position based on the rectangle's height
223 + .attr('text-anchor', 'middle')
224 + .style('font-family', 'IBM Plex Sans')
225 + .style('font-size', '14px')
226 + .style('font-weight', 'bold'); // Make the font bold
227 +
228 + // Bottom area - Servers
229 + svg.append('rect')
230 + .attr('x', 0)
231 + .attr('y', h - borderPadding / 2)
232 + .attr('width', '100%')
233 + .attr('height', borderPadding / 2) // Adjust height as needed
234 + .style('fill', hexToHalfOpacityRGBA(serverColor));
235 +
236 + svg.append('text')
237 + .text('Servers')
238 + .attr('x', '50%')
239 + .attr('y', h - borderPadding / 2 + 16) // Adjust y position based on the rectangle's height
240 + .attr('text-anchor', 'middle')
241 + .style('font-family', 'IBM Plex Sans')
242 + .style('font-size', '14px')
243 + .style('font-weight', 'bold'); // Make the font bold
244 +
245 + svg.append('rect')
246 + .attr('x', w - borderPadding / 2) // Position it close to the right edge
247 + .attr('y', 0)
248 + .attr('width', borderPadding / 2) // Width of the border area
249 + .attr('height', '100%')
250 + .style('fill', hexToHalfOpacityRGBA(publicColor));
251 +
252 + svg.append('text')
253 + .text('Public')
254 + .attr('x', w - (borderPadding / 2)) // Position close to the right edge
255 + .attr('y', ch - 10) // Vertically centered
256 + .attr('text-anchor', 'middle')
257 + .attr('dominant-baseline', 'middle') // Center alignment of the text
258 + .attr('transform', `rotate(90, ${w - (borderPadding / 2)}, ${ch})`) // Rotate around the text's center
259 + .style('font-family', 'IBM Plex Sans')
260 + .style('font-size', '14px')
261 + .style('font-weight', 'bold'); // Make the font bold
262 +
263 + svg.append('rect')
264 + .attr('x', 0) // Positioned at the left edge
265 + .attr('y', 0)
266 + .attr('width', borderPadding / 2) // Width of the border area
267 + .attr('height', '100%')
268 + .style('fill', hexToHalfOpacityRGBA(privateColor));
269 +
270 + svg.append('text')
271 + .text('Private')
272 + .attr('x', borderPadding / 2) // Position close to the left edge
273 + .attr('y', ch) // Vertically centered
274 + .attr('text-anchor', 'middle')
275 + .attr('dominant-baseline', 'middle') // Center alignment of the text
276 + .attr('transform', `rotate(-90, ${borderPadding / 2 - 10}, ${ch})`) // Rotate around the text's center
277 + .style('font-family', 'IBM Plex Sans')
278 + .style('font-size', '14px')
279 + .style('font-weight', 'bold'); // Make the font bold
280 +
281 + function boundaryForce(alpha) {
282 + return function(d) {
283 + const nodeRadius = circleSize(d.totalCount) / 2;
284 + d.x = Math.max(borderPadding + nodeRadius, Math.min(w - borderPadding - nodeRadius, d.x));
285 + d.y = Math.max(borderPadding + nodeRadius, Math.min(h - borderPadding - nodeRadius, d.y));
286 + };
287 + }
288 +
289 + const simulation = d3.forceSimulation(data)
290 + .force('center', d3.forceCenter(window.innerWidth / 2, window.innerHeight / 2).strength(1)) // Scale center force strength
291 + .force("x", d3.forceX(d => d.targetX).strength(0.3))
292 + .force("y", d3.forceY(d => d.targetY).strength(0.3))
293 + .force("charge", d3.forceManyBody().strength(-0.5))
294 + .force("collide", d3.forceCollide(d => circleSize(d.totalCount) * 1.1 + 15).strength(1))
295 + .force("boundary", boundaryForce(0.5))
296 + .on('tick', ticked);
297 +
298 + const app = svg.selectAll('.app')
299 + .data(data)
300 + .enter().append('g')
301 + .attr('class', 'app')
302 + .call(d3.drag()
303 + .on('start', dragstarted)
304 + .on('drag', dragged)
305 + .on('end', dragended));
306 +
307 + app.each(function(d) {
308 + const group = d3.select(this);
309 + const pieData = pie(getPieData(d));
310 + const radius = circleSize(d.totalCount);
311 +
312 + group.selectAll('path')
313 + .data(pieData)
314 + .enter().append('path')
315 + .attr('d', arc.innerRadius(0).outerRadius(radius))
316 + .attr('fill', (d, i) => pieColors(i));
317 + });
318 +
319 + app.append('text')
320 + .text(d => d.name)
321 + .attr('text-anchor', 'middle')
322 + .attr('y', d => circleSize(d.totalCount) + 10)
323 + .style('font-family', 'IBM Plex Sans') // Set the font family
324 + .style('font-size', '12px') // Set the font size
325 + .style('font-weight', 'bold'); // Make the font bold
326 +
327 + // Initialize app positions at the center
328 + app.attr('transform', `translate(${window.innerWidth / 2}, ${window.innerHeight / 2})`);
329 +
330 + function ticked() {
331 + app.attr('transform', d => `translate(${d.x}, ${d.y})`);
332 + }
333 +
334 + function dragstarted(event, d) {
335 + if (!event.active) simulation.alphaTarget(1).restart();
336 + d.fx = d.x;
337 + d.fy = d.y;
338 + }
339 +
340 + function dragged(event, d) {
341 + d.fx = event.x;
342 + d.fy = event.y;
343 + }
344 +
345 + function dragended(event, d) {
346 + if (!event.active) simulation.alphaTarget(0);
347 + d.fx = null;
348 + d.fy = null;
349 + }
350 + }
351 +
352 + // Modify your fetchData function to call drawApplications after data transformation
353 + function fetchData() {
354 + fetch('http://localhost:19999/api/v1/function?function=network-viewer')
355 + .then(response => response.json())
356 + .then(data => {
357 + // Your existing code
358 + const desiredColumns = ["Direction", "Protocol", "Namespace", "Process", "CommandLine", "LocalIP", "LocalPort", "RemoteIP", "RemotePort", "LocalAddressSpace", "RemoteAddressSpace"];
359 + const transformed = transformData(data, desiredColumns, data.columns);
360 +
361 + // Now draw the applications with border forces
362 + drawApplications(transformed);
363 + })
364 + .catch(error => console.error('Error fetching data:', error));
365 + }
366 +
367 + // Load data on start
368 + window.onload = fetchData;
369 +</script>
370 +</body>
371 +</html>
collectors/plugins.d/local-sockets.h
+2 -2
@@ -170,7 +170,7 @@ typedef struct local_socket {
170 static inline void local_sockets_log(LS_STATE *ls, const char *format, ...) __attribute__ ((format(__printf__, 2, 3)));
171 static inline void local_sockets_log(LS_STATE *ls, const char *format, ...) {
172 if(++ls->stats.errors_encountered == ls->config.max_errors) {
173 - nd_log(NDLS_COLLECTORS, NDLP_ERR, "LOCAL-LISTENERS: max number of logs reached. Not logging anymore");
173 + nd_log(NDLS_COLLECTORS, NDLP_ERR, "LOCAL-SOCKETS: max number of logs reached. Not logging anymore");
174 return;
175 }
176
@@ -183,7 +183,7 @@ static inline void local_sockets_log(LS_STATE *ls, const char *format, ...) {
183 vsnprintf(buf, sizeof(buf), format, args);
184 va_end(args);
185
186 - nd_log(NDLS_COLLECTORS, NDLP_ERR, "LOCAL-LISTENERS: %s", buf);
186 + nd_log(NDLS_COLLECTORS, NDLP_ERR, "LOCAL-SOCKETS: %s", buf);
187 }
188
189 // --------------------------------------------------------------------------------------------------------------------
health/schema.d/health:alert:prototype.json
+6 -6
@@ -452,9 +452,9 @@
452 "ui:classNames": "dyncfg-grid-col-span-5-2"
453 },
454 "value": {
455 - "ui:classNames": "dyncfg-grid-col-span-1-6",
455 + "ui:classNames": "dyncfg-grid dyncfg-grid-col-6 dyncfg-grid-col-span-1-6",
456 "database_lookup": {
457 - "ui:classNames": "dyncfg-grid-col-span-1-6",
457 + "ui:classNames": "dyncfg-grid dyncfg-grid-col-6 dyncfg-grid-col-span-1-6",
458 "after": {
459 "ui:classNames": "dyncfg-grid-col-span-1-1"
460 },
@@ -479,7 +479,7 @@
479 }
480 },
481 "conditions": {
482 - "ui:classNames": "dyncfg-grid-col-span-1-6",
482 + "ui:classNames": "dyncfg-grid dyncfg-grid-col-6 dyncfg-grid-col-span-1-6",
483 "warning_condition": {
484 "ui:classNames": "dyncfg-grid-col-span-1-2"
485 },
@@ -494,7 +494,7 @@
494 }
495 },
496 "action": {
497 - "ui:classNames": "dyncfg-grid-col-span-1-6",
497 + "ui:classNames": "dyncfg-grid dyncfg-grid-col-6 dyncfg-grid-col-span-1-6",
498 "execute": {
499 "ui:classNames": "dyncfg-grid-col-span-1-3"
500 },
@@ -507,7 +507,7 @@
507 "delay": {
508 "ui:Collapsible": true,
509 "ui:InitiallyExpanded": false,
510 - "ui:classNames": "dyncfg-grid-col-span-1-6",
510 + "ui:classNames": "dyncfg-grid dyncfg-grid-col-6 dyncfg-grid-col-span-1-6",
511 "up": {
512 "ui:classNames": "dyncfg-grid-col-span-1-2"
513 },
@@ -524,7 +524,7 @@
524 "repeat": {
525 "ui:Collapsible": true,
526 "ui:InitiallyExpanded": false,
527 - "ui:classNames": "dyncfg-grid-col-span-1-6",
527 + "ui:classNames": "dyncfg-grid dyncfg-grid-col-6 dyncfg-grid-col-span-1-6",
528 "enabled": {
529 "ui:classNames": "dyncfg-grid-col-span-1-2"
530 },