summaryrefslogtreecommitdiffstats
path: root/src/collectors/network-viewer.plugin
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-05-05 12:08:03 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-05-05 12:08:18 +0000
commit5da14042f70711ea5cf66e034699730335462f66 (patch)
tree0f6354ccac934ed87a2d555f45be4c831cf92f4a /src/collectors/network-viewer.plugin
parentReleasing debian version 1.44.3-2. (diff)
downloadnetdata-5da14042f70711ea5cf66e034699730335462f66.tar.xz
netdata-5da14042f70711ea5cf66e034699730335462f66.zip
Merging upstream version 1.45.3+dfsg.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'src/collectors/network-viewer.plugin')
-rw-r--r--src/collectors/network-viewer.plugin/network-connections-chart.html706
-rw-r--r--src/collectors/network-viewer.plugin/network-viewer.c803
2 files changed, 1509 insertions, 0 deletions
diff --git a/src/collectors/network-viewer.plugin/network-connections-chart.html b/src/collectors/network-viewer.plugin/network-connections-chart.html
new file mode 100644
index 000000000..4471b2add
--- /dev/null
+++ b/src/collectors/network-viewer.plugin/network-connections-chart.html
@@ -0,0 +1,706 @@
+<!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%;
+ overflow: hidden;
+ }
+
+ #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>
+</head>
+<body>
+<div id="d3-canvas"></div> <!-- Div for D3 rendering -->
+
+<script>
+
+ let config = {
+ listen: true,
+ }
+
+ function transformData(dataPayload) {
+ // console.log("dataPayload", dataPayload);
+ const desiredColumns = ["Direction", "Protocol", "Namespace", "Process", "LocalIP", "LocalPort", "RemoteIP", "RemotePort", "LocalAddressSpace", "RemoteAddressSpace"];
+
+ const transformedData = [];
+ const appMap = new Map();
+
+ console.log(dataPayload);
+ dataPayload.data.forEach(row => {
+ const rowData = {};
+ desiredColumns.forEach(columnName => {
+ const columnIndex = dataPayload.columns[columnName].index;
+ rowData[columnName] = row[columnIndex];
+ });
+
+ const appName = rowData['Process'];
+ if (!appMap.has(appName)) {
+ appMap.set(appName, {
+ counts: {
+ listen: 0,
+ inbound: 0,
+ outbound: 0,
+ local: 0,
+ private: 0,
+ public: 0,
+ total: 0
+ }
+ });
+ }
+
+ const appData = appMap.get(appName);
+
+ if(config.listen || rowData['Direction'] !== 'listen')
+ appData.counts.total++;
+
+ if (rowData['Direction'] === 'listen')
+ appData.counts.listen++;
+ else if (rowData['Direction'] === 'local')
+ appData.counts.local++;
+ else if (rowData['Direction'] === 'inbound')
+ appData.counts.inbound++;
+ else if (rowData['Direction'] === 'outbound')
+ appData.counts.outbound++;
+
+ if (rowData['RemoteAddressSpace'] === 'public')
+ appData.counts.public++;
+ else if (rowData['RemoteAddressSpace'] === 'private')
+ appData.counts.private++;
+ });
+
+ // Convert the map to an array format
+ for (let [appName, appData] of appMap) {
+ transformedData.push({
+ name: appName,
+ ...appData
+ });
+ }
+
+ if(!config.listen)
+ return transformedData.filter(function(d) {
+ return d.counts.total > 0;
+ });
+ else
+ return transformedData;
+ }
+
+ function normalizeData(data, w, h, borderPadding) {
+ const cw = w / 2 - borderPadding;
+ const ch = h / 2 - borderPadding;
+
+ const minSize = 13;
+ const maxSize = Math.min(
+ (cw * 2) / 3 - borderPadding,
+ (ch * 2) / 3 - borderPadding,
+ Math.max(5, Math.min(w, h) / data.length) + minSize
+ )
+
+ const max = {
+ total: d3.max(data, d => d.counts.total),
+ local: d3.max(data, d => d.counts.local),
+ listen: d3.max(data, d => d.counts.listen),
+ private: d3.max(data, d => d.counts.private),
+ public: d3.max(data, d => d.counts.public),
+ inbound: d3.max(data, d => d.counts.inbound),
+ outbound: d3.max(data, d => d.counts.outbound),
+ }
+
+ const circleSize = d3.scaleLog()
+ .domain([1, max.total])
+ .range([minSize, maxSize])
+ .clamp(true); // Clamps the output so that it stays within the range
+
+ let listenerDelta = 50;
+ let listenersAdded = 0;
+ let listenersAddedRight = 0;
+ let listenersAddedLeft = 0;
+ let listenersH = h - borderPadding;
+
+ data.forEach((d, i) => {
+ const logScaleRight = d3.scaleLog().domain([1, d.counts.public + 1]).range([0, cw]);
+ const logScaleLeft = d3.scaleLog().domain([1, d.counts.private + 1]).range([0, cw]);
+ const logScaleTop = d3.scaleLog().domain([1, d.counts.outbound + 1]).range([0, ch]);
+ const logScaleBottom = d3.scaleLog().domain([1, (d.counts.listen + d.counts.inbound) / 2 + 1]).range([0, ch]);
+
+ d.forces = {
+ total: d.counts.total / max.total,
+ local: d.counts.local / max.local,
+ listen: d.counts.listen / max.listen,
+ private: d.counts.private / max.private,
+ public: d.counts.public / max.public,
+ inbound: d.counts.inbound / max.inbound,
+ outbound: d.counts.outbound / max.outbound,
+ }
+
+ d.pos = {
+ // we add 1 to avoid log(0)
+ right: logScaleRight(d.counts.public + 1),
+ left: logScaleLeft(d.counts.private + 1),
+ top: logScaleTop(d.counts.outbound + 1),
+ bottom: logScaleBottom(((config.listen ? d.counts.listen : 0) + d.counts.inbound) / (config.listen ? 2 : 1) + 1),
+ };
+
+ let x = borderPadding + cw + d.pos.right - d.pos.left;
+ let y = borderPadding + ch + d.pos.bottom - d.pos.top;
+ let size = circleSize(d.counts.total);
+
+ if(d.counts.listen === d.counts.total) {
+ // a listener
+ size = circleSize(1);
+
+ if(listenersAdded * listenerDelta > cw * 2 / 3) {
+ // too many listeners on this row
+ // let's start adding on another row above
+ listenersAdded = 0;
+ listenersAddedLeft = 0;
+ listenersAddedRight = 0;
+ listenersH -= 30;
+ }
+
+ if(!listenersAdded) {
+ x = cw;
+ y = listenersH - size;
+ }
+ else {
+ if(listenersAddedLeft >= listenersAddedRight) {
+ listenersAddedRight++;
+ x = cw + listenersAddedRight * listenerDelta;
+ y = listenersH - size - (listenersAddedRight % 2 === 0 ? 0 : 30);
+ }
+ else {
+ listenersAddedLeft++;
+ x = cw - listenersAddedLeft * listenerDelta;
+ y = listenersH - size - (listenersAddedLeft % 2 === 0 ? 0 : 30);
+ }
+ }
+
+ listenersAdded++;
+ }
+
+ const others = d.counts.total - (d.counts.public + d.counts.private + (config.listen ? d.counts.listen : 0) + d.counts.inbound + d.counts.outbound);
+ d.d3 = {
+ x: x,
+ y: y,
+ size: size,
+ pie: [
+ { value: d.counts.public },
+ { value: d.counts.private },
+ { value: (config.listen ? d.counts.listen : 0) + d.counts.inbound },
+ { value: d.counts.outbound },
+ { value: others > 0 ? others : 0 },
+ ]
+ }
+
+ if(d.d3.x - d.d3.size / 2 < borderPadding)
+ d.d3.x = borderPadding + d.d3.size * 2;
+
+ if(d.d3.x + d.d3.size / 2 > w)
+ d.d3.x = w - d.d3.size * 2;
+
+ if(d.d3.y - d.d3.size / 2 < borderPadding)
+ d.d3.y = borderPadding + d.d3.size * 2;
+
+ if(d.d3.y + d.d3.size / 2 > h)
+ d.d3.y = h - d.d3.size * 2;
+
+ if (d.name === 'upsd')
+ console.log("object", d, "cw", cw, "ch", ch);
+ });
+
+ return data;
+ }
+
+ const themes = {
+ dark: {
+ publicColor: "#bb9900",
+ privateColor: "#323299",
+ serverColor: "#008800",
+ clientColor: "#994433",
+ otherColor: "#454545",
+ backgroundColor: "black",
+ appFontColor: "#bbbbbb",
+ appFontFamily: 'IBM Plex Sans',
+ appFontSize: "12px",
+ appFontWeight: "regular",
+ borderFontColor: "#aaaaaa",
+ borderFontFamily: 'IBM Plex Sans',
+ borderFontSize: "14px",
+ borderFontWeight: "bold",
+ },
+ light: {
+ publicColor: "#bbaa00",
+ privateColor: "#5555ff",
+ serverColor: "#009900",
+ clientColor: "#990000",
+ otherColor: "#666666",
+ backgroundColor: "white",
+ appFontColor: "black",
+ appFontFamily: 'IBM Plex Sans',
+ appFontSize: "12px",
+ appFontWeight: "bold",
+ borderFontColor: "white",
+ borderFontFamily: 'IBM Plex Sans',
+ borderFontSize: "14px",
+ borderFontWeight: "bold",
+ }
+ }
+
+ function hexToHalfOpacityRGBA(hex) {
+ if (hex.length !== 7 || hex[0] !== '#')
+ throw new Error('Invalid hex color format');
+
+ 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 `rgba(${r}, ${g}, ${b}, 0.5)`;
+ }
+
+ function getRgbColor(hex, opacity = 1) {
+ if (hex.length !== 7 || hex[0] !== "#") throw new Error("Invalid hex color format");
+
+ // Parse the hex color components (red, green, blue)
+ const r = parseInt(hex.slice(1, 3), 16);
+ const g = parseInt(hex.slice(3, 5), 16);
+ const b = parseInt(hex.slice(5, 7), 16);
+
+ // Ensure opacity is within the valid range (0 to 1)
+ const validOpacity = Math.min(1, Math.max(0, opacity));
+
+ // Return the RGBA color
+ return `rgba(${r}, ${g}, ${b}, ${validOpacity})`;
+ }
+
+ function drawInitialChart(svg, data, w, h, borderPadding, theme) {
+ const cw = w / 2;
+ const ch = h / 2;
+
+ document.body.style.backgroundColor = theme.backgroundColor;
+
+ const clientsGradient = svg.append("defs")
+ .append("linearGradient")
+ .attr("id", "clientsGradient")
+ .attr("x1", "0%")
+ .attr("y1", "0%")
+ .attr("x2", "0%")
+ .attr("y2", "100%");
+
+ clientsGradient.append("stop")
+ .attr("offset", "0%")
+ .style("stop-color", getRgbColor(theme.clientColor, 1));
+
+ clientsGradient.append("stop")
+ .attr("offset", "100%")
+ .style("stop-color", getRgbColor(theme.clientColor, 0));
+
+ svg.append("rect")
+ .attr("id", "clientsGradientRect")
+ .attr("x", 0)
+ .attr("y", 0)
+ .attr("width", "100%")
+ .attr("height", borderPadding / 2)
+ .style("fill", "url(#clientsGradient)");
+
+ svg.append("text")
+ .attr("id", "clientsText")
+ .text("Clients")
+ .attr("x", "50%")
+ .attr("y", borderPadding / 2 - 4)
+ .attr("text-anchor", "middle")
+ .style("font-family", theme.borderFontFamily)
+ .style("font-size", theme.borderFontSize)
+ .style("font-weight", theme.borderFontWeight)
+ .style("fill", theme.borderFontColor);
+
+ const serversGradient = svg.append("defs")
+ .append("linearGradient")
+ .attr("id", "serversGradient")
+ .attr("x1", "0%")
+ .attr("y1", "100%") // Start from the bottom
+ .attr("x2", "0%")
+ .attr("y2", "0%") // End at the top
+
+ serversGradient.append("stop")
+ .attr("offset", "0%")
+ .style("stop-color", getRgbColor(theme.serverColor, 1));
+
+ serversGradient.append("stop")
+ .attr("offset", "100%")
+ .style("stop-color", getRgbColor(theme.serverColor, 0));
+
+ svg.append("rect")
+ .attr("id", "serversGradientRect")
+ .attr("x", 0)
+ .attr("y", h - borderPadding / 2)
+ .attr("width", "100%")
+ .attr("height", borderPadding / 2)
+ .style("fill", "url(#serversGradient)"); // Use the reversed gradient fill
+
+ svg.append("text")
+ .attr("id", "serversText")
+ .text("Servers")
+ .attr("x", "50%")
+ .attr("y", h - borderPadding / 2 + 16)
+ .attr("text-anchor", "middle")
+ .style("font-family", theme.borderFontFamily)
+ .style("font-size", theme.borderFontSize)
+ .style("font-weight", theme.borderFontWeight)
+ .style("fill", theme.borderFontColor);
+
+ const publicGradient = svg.append("defs")
+ .append("linearGradient")
+ .attr("id", "publicGradient")
+ .attr("x1", "100%") // Start from the right
+ .attr("y1", "0%")
+ .attr("x2", "0%") // End at the left
+ .attr("y2", "0%");
+
+ publicGradient.append("stop")
+ .attr("offset", "0%")
+ .style("stop-color", getRgbColor(theme.publicColor, 1));
+
+ publicGradient.append("stop")
+ .attr("offset", "100%")
+ .style("stop-color", getRgbColor(theme.publicColor, 0));
+
+ svg.append("rect")
+ .attr("id", "publicGradientRect")
+ .attr("x", w - borderPadding / 2)
+ .attr("y", 0)
+ .attr("width", borderPadding / 2)
+ .attr("height", "100%")
+ .style("fill", "url(#publicGradient)");
+
+ svg.append("text")
+ .attr("id", "publicText")
+ .text("Public")
+ .attr("x", w - (borderPadding / 2))
+ .attr("y", ch - 10)
+ .attr("text-anchor", "middle")
+ .attr("dominant-baseline", "middle")
+ .attr("transform", `rotate(90, ${w - (borderPadding / 2)}, ${ch})`)
+ .style("font-family", theme.borderFontFamily)
+ .style("font-size", theme.borderFontSize)
+ .style("font-weight", theme.borderFontWeight)
+ .style("fill", theme.borderFontColor);
+
+ const privateGradient = svg.append("defs")
+ .append("linearGradient")
+ .attr("id", "privateGradient")
+ .attr("x1", "0%") // Start from the left
+ .attr("y1", "0%")
+ .attr("x2", "100%") // End at the right
+ .attr("y2", "0%");
+
+ privateGradient.append("stop")
+ .attr("offset", "0%")
+ .style("stop-color", getRgbColor(theme.privateColor, 1));
+
+ privateGradient.append("stop")
+ .attr("offset", "100%")
+ .style("stop-color", getRgbColor(theme.privateColor, 0));
+
+ svg.append("rect")
+ .attr("id", "privateGradientRect")
+ .attr("x", 0)
+ .attr("y", 0)
+ .attr("width", borderPadding / 2)
+ .attr("height", "100%")
+ .style("fill", "url(#privateGradient)");
+
+ svg.append("text")
+ .attr("id", "privateText")
+ .text("Private")
+ .attr("x", borderPadding / 2)
+ .attr("y", ch)
+ .attr("text-anchor", "middle")
+ .attr("dominant-baseline", "middle")
+ .attr("transform", `rotate(-90, ${borderPadding / 2 - 10}, ${ch})`)
+ .style("font-family", theme.borderFontFamily)
+ .style("font-size", theme.borderFontSize)
+ .style("font-weight", theme.borderFontWeight)
+ .style("fill", theme.borderFontColor);
+ }
+
+ function updateGradientBoxes(svg, w, h, borderPadding, theme) {
+ const cw = w / 2;
+ const ch = h / 2;
+
+ // Update clientsGradient (top gradient) - mainly adjusting width
+ svg.select("rect#clientsGradientRect")
+ .attr("width", w); // Stretch across the new width
+
+ // Update text position if necessary
+ svg.select("text#clientsText")
+ .attr("x", w / 2); // Center text
+
+ // Update serversGradient (bottom gradient) - adjust Y position and width
+ svg.select("rect#serversGradientRect")
+ .attr("y", h - borderPadding / 2) // Move to the new bottom position
+ .attr("width", w) // Stretch across the new width
+ .attr("fill", "red");
+
+ // Update text position if necessary
+ svg.select("text#serversText")
+ .attr("x", w / 2) // Center text
+ .attr("y", h - borderPadding / 2 + 16); // Adjust based on your specific offset
+
+ // Update publicGradient (right gradient) - adjust X position and height
+ svg.select("rect#publicGradientRect")
+ .attr("x", w - borderPadding / 2) // Move to the new right side position
+ .attr("height", h); // Stretch across the new height
+
+ // Update text position if necessary, adjusting for rotation
+ svg.select("text#publicText")
+ .attr("x", w - (borderPadding / 2))
+ .attr("y", ch - 10)
+ .attr("transform", `rotate(90, ${w - (borderPadding / 2)}, ${ch})`);
+
+ // Update privateGradient (left gradient) - height adjustment only as it's already at x = 0
+ svg.select("rect#privateGradientRect")
+ .attr("height", h); // Stretch across the new height
+
+ // Update text position if necessary, adjusting for rotation
+ svg.select("text#privateText")
+ .attr("x", borderPadding / 2)
+ .attr("y", ch)
+ .attr("transform", `rotate(-90, ${borderPadding / 2 - 10}, ${ch})`);
+
+ }
+
+ let positionsMap = new Map();
+ function saveCurrentPositions(svg) {
+ svg.selectAll('.app').each(function(d) {
+ if (d) {
+ positionsMap.set(d.name, { x: d.x, y: d.y });
+ }
+ });
+ }
+
+ function updateApps(svg, data, w, h, borderPadding, theme) {
+ const cw = w / 2;
+ const ch = h / 2;
+
+ saveCurrentPositions(svg);
+ //svg.selectAll('.app').remove();
+
+ const pieColors = d3.scaleOrdinal()
+ .domain(["public", "private", "listenInbound", "outbound", "others"])
+ .range([theme.publicColor, theme.privateColor, theme.serverColor, theme.clientColor, theme.otherColor]);
+
+ const pie = d3.pie().value(d => d.value);
+ const arc = d3.arc();
+
+ // Binding data with key function
+ const app = svg.selectAll('.app')
+ .data(data, d => d.name);
+
+ // Remove any elements that no longer have data associated with them
+ app.exit()
+ .transition()
+ .style("opacity", 0)
+ .remove();
+
+ // Enter selection for new data points
+ const appEnter = app.enter()
+ .append('g')
+ .attr('class', 'app')
+ .attr('transform', `translate(${cw}, ${ch})`); // Start from center
+
+ // Initialize new elements
+ appEnter.each(function (d) {
+ const group = d3.select(this);
+ const pieData = pie(d.d3.pie);
+
+ group.selectAll('path')
+ .data(pieData)
+ .enter().append('path')
+ .transition()
+ .attr('fill', (d, i) => pieColors(i));
+
+ group.append('text')
+ .text(d => d.name)
+ .attr('text-anchor', 'middle')
+ .style('font-family', theme.appFontFamily)
+ .style('font-size', theme.appFontSize)
+ .style('font-weight', theme.appFontWeight)
+ .style('fill', theme.appFontColor);
+ });
+
+ // Transition for new elements
+ appEnter.transition()
+ .attr('transform', d => `translate(${d.d3.x}, ${d.d3.y})`);
+
+ // Merge the enter and update selections
+ const mergedApp = appEnter.merge(app);
+
+ // Update saved positions
+ mergedApp.each(function (d) {
+ const group = d3.select(this);
+ const oldPos = positionsMap.get(d.name) || { x: cw, y: ch };
+
+ d.x = oldPos.x;
+ d.y = oldPos.y;
+
+ group.selectAll('path')
+ .data(pie(d.d3.pie))
+ .transition()
+ .attr('d', arc.innerRadius(0).outerRadius(d.d3.size));
+
+ group.select('text')
+ .transition()
+ .attr('y', d.d3.size + 10);
+ });
+
+ // Apply the drag behavior to the merged selection
+ mergedApp.call(d3.drag()
+ .on('start', dragstarted)
+ .on('drag', dragged)
+ .on('end', dragended));
+
+ return mergedApp;
+ }
+
+ let forceInit = true;
+ let initial = true;
+ let simulation;
+
+ 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;
+ }
+
+ // Function to draw the circles and labels for each application with border forces
+ function drawApplications(data, w, h, borderPadding, theme) {
+ let svg = d3.select('#d3-canvas').select('svg');
+
+ if(svg.empty()) {
+ svg = d3.select('#d3-canvas').select('svg');
+ svg = d3.select('#d3-canvas').append('svg')
+ .attr('width', '100%')
+ .attr('height', '100%');
+
+ drawInitialChart(svg, data, w, h, borderPadding, theme);
+ forceInit = false;
+ }
+
+ updateGradientBoxes(svg, w, h, borderPadding, theme);
+
+ const app = updateApps(svg, data, w, h, borderPadding, theme);
+
+ app.transition().duration(5000)
+
+ simulation = d3.forceSimulation(data)
+ //.force('center', d3.forceCenter(cw, ch).strength(1))
+ .force("x", d3.forceX(d => d.d3.x).strength(d => {
+ if(d.counts.listen === d.counts.total)
+ return 0.5
+ else
+ return 0.05
+ }))
+ .force("y", d3.forceY(d => d.d3.y).strength(d => {
+ if(d.counts.listen === d.counts.total)
+ return 0.5
+ else
+ return 0.05
+ }))
+ //.force("charge", d3.forceManyBody().strength(-0.05))
+ .force("collide", d3.forceCollide(d => d.d3.size * 1.1 + 15).strength(1))
+ .on('tick', ticked);
+
+ function ticked() {
+ data.forEach(d => {
+ if(d.x > w - d.d3.size)
+ d.x = w - d.d3.size;
+ else if(d.x < 0)
+ d.x = 0;
+
+ if(d.y > h - d.d3.size)
+ d.y = h - d.d3.size;
+ else if(d.y < 0)
+ d.y = 0;
+ });
+
+ app.attr('transform', d => `translate(${d.x}, ${d.y})`);
+ }
+
+ initial = false;
+ }
+
+ let lastPayload = {};
+ function redrawChart() {
+ const transformed = transformData(lastPayload);
+ console.log(transformed);
+
+ const w = window.innerWidth;
+ const h = window.innerHeight;
+ const borderPadding = 40;
+
+ const normalized = normalizeData(transformed, w, h, borderPadding);
+ drawApplications(normalized, w, h, borderPadding, themes.dark);
+
+ // Update SVG dimensions
+ const svg = d3.select('#d3-canvas').select('svg')
+ .attr('width', w)
+ .attr('height', h);
+ }
+
+ // Debounce function to optimize performance
+ function debounce(func, timeout = 50) {
+ let timer;
+ return (...args) => {
+ clearTimeout(timer);
+ timer = setTimeout(() => { func.apply(this, args); }, timeout);
+ };
+ }
+
+ // Attach the event listener to the window resize event
+ window.addEventListener('resize', debounce(() => {
+ forceInit = true;
+ redrawChart();
+ }));
+
+ // Modify your fetchData function to call drawApplications after data transformation
+ function fetchDataAndUpdateChart() {
+ fetch('http://localhost:19999/api/v1/function?function=network-connections')
+ .then(response => response.json())
+ .then(data => {
+ lastPayload = data;
+ redrawChart();
+ })
+ .catch(error => console.error('Error fetching data:', error));
+ }
+
+ // Initial load
+ window.onload = () => {
+ fetchDataAndUpdateChart();
+ setInterval(fetchDataAndUpdateChart, 2000); // You may need to adjust this part
+ };
+</script>
+</body>
+</html>
diff --git a/src/collectors/network-viewer.plugin/network-viewer.c b/src/collectors/network-viewer.plugin/network-viewer.c
new file mode 100644
index 000000000..074535a0b
--- /dev/null
+++ b/src/collectors/network-viewer.plugin/network-viewer.c
@@ -0,0 +1,803 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include "collectors/all.h"
+#include "libnetdata/libnetdata.h"
+#include "libnetdata/required_dummies.h"
+
+#define ENABLE_DETAILED_VIEW
+
+#define LOCAL_SOCKETS_EXTENDED_MEMBERS struct { \
+ size_t count; \
+ const char *local_address_space; \
+ const char *remote_address_space; \
+ } network_viewer;
+
+#include "libnetdata/maps/local-sockets.h"
+#include "libnetdata/maps/system-users.h"
+
+#define NETWORK_CONNECTIONS_VIEWER_FUNCTION "network-connections"
+#define NETWORK_CONNECTIONS_VIEWER_HELP "Network connections explorer"
+
+#define SIMPLE_HASHTABLE_VALUE_TYPE LOCAL_SOCKET
+#define SIMPLE_HASHTABLE_NAME _AGGREGATED_SOCKETS
+#include "libnetdata/simple_hashtable.h"
+
+netdata_mutex_t stdout_mutex = NETDATA_MUTEX_INITIALIZER;
+static bool plugin_should_exit = false;
+static USERNAMES_CACHE *uc;
+
+ENUM_STR_MAP_DEFINE(SOCKET_DIRECTION) = {
+ { .id = SOCKET_DIRECTION_LISTEN, .name = "listen" },
+ { .id = SOCKET_DIRECTION_LOCAL_INBOUND, .name = "local" },
+ { .id = SOCKET_DIRECTION_LOCAL_OUTBOUND, .name = "local" },
+ { .id = SOCKET_DIRECTION_INBOUND, .name = "inbound" },
+ { .id = SOCKET_DIRECTION_OUTBOUND, .name = "outbound" },
+
+ // terminator
+ { . id = 0, .name = NULL }
+};
+ENUM_STR_DEFINE_FUNCTIONS(SOCKET_DIRECTION, SOCKET_DIRECTION_LISTEN, "unknown");
+
+typedef int TCP_STATE;
+ENUM_STR_MAP_DEFINE(TCP_STATE) = {
+ { .id = TCP_ESTABLISHED, .name = "established" },
+ { .id = TCP_SYN_SENT, .name = "syn-sent" },
+ { .id = TCP_SYN_RECV, .name = "syn-received" },
+ { .id = TCP_FIN_WAIT1, .name = "fin1-wait1" },
+ { .id = TCP_FIN_WAIT2, .name = "fin1-wait2" },
+ { .id = TCP_TIME_WAIT, .name = "time-wait" },
+ { .id = TCP_CLOSE, .name = "close" },
+ { .id = TCP_CLOSE_WAIT, .name = "close-wait" },
+ { .id = TCP_LAST_ACK, .name = "last-ack" },
+ { .id = TCP_LISTEN, .name = "listen" },
+ { .id = TCP_CLOSING, .name = "closing" },
+
+ // terminator
+ { . id = 0, .name = NULL }
+};
+ENUM_STR_DEFINE_FUNCTIONS(TCP_STATE, 0, "unknown");
+
+static void local_socket_to_json_array(BUFFER *wb, LOCAL_SOCKET *n, uint64_t proc_self_net_ns_inode, bool aggregated) {
+ char local_address[INET6_ADDRSTRLEN];
+ char remote_address[INET6_ADDRSTRLEN];
+ char *protocol;
+
+ if(n->local.family == AF_INET) {
+ ipv4_address_to_txt(n->local.ip.ipv4, local_address);
+ ipv4_address_to_txt(n->remote.ip.ipv4, remote_address);
+ protocol = n->local.protocol == IPPROTO_TCP ? "tcp4" : "udp4";
+ }
+ else if(n->local.family == AF_INET6) {
+ ipv6_address_to_txt(&n->local.ip.ipv6, local_address);
+ ipv6_address_to_txt(&n->remote.ip.ipv6, remote_address);
+ protocol = n->local.protocol == IPPROTO_TCP ? "tcp6" : "udp6";
+ }
+ else
+ return;
+
+ const char *type;
+ if(n->net_ns_inode == proc_self_net_ns_inode)
+ type = "system";
+ else if(n->net_ns_inode == 0)
+ type = "[unknown]";
+ else
+ type = "container";
+
+ buffer_json_add_array_item_array(wb);
+ {
+ buffer_json_add_array_item_string(wb, SOCKET_DIRECTION_2str(n->direction));
+ buffer_json_add_array_item_string(wb, protocol);
+ buffer_json_add_array_item_string(wb, type); // system or container
+ if(n->local.protocol == IPPROTO_TCP)
+ buffer_json_add_array_item_string(wb, TCP_STATE_2str(n->state));
+ else
+ buffer_json_add_array_item_string(wb, "stateless");
+
+ buffer_json_add_array_item_uint64(wb, n->pid);
+
+ if(!n->comm[0])
+ buffer_json_add_array_item_string(wb, "[unknown]");
+ else
+ buffer_json_add_array_item_string(wb, n->comm);
+
+ // buffer_json_add_array_item_string(wb, string2str(n->cmdline));
+
+ if(n->uid == UID_UNSET) {
+ // buffer_json_add_array_item_uint64(wb, n->uid);
+ buffer_json_add_array_item_string(wb, "[unknown]");
+ }
+ else {
+ // buffer_json_add_array_item_uint64(wb, n->uid);
+ STRING *u = system_usernames_cache_lookup_uid(uc, n->uid);
+ buffer_json_add_array_item_string(wb, string2str(u));
+ string_freez(u);
+ }
+
+ if(!aggregated) {
+ buffer_json_add_array_item_string(wb, local_address);
+ buffer_json_add_array_item_uint64(wb, n->local.port);
+ }
+ buffer_json_add_array_item_string(wb, n->network_viewer.local_address_space);
+
+ if(!aggregated) {
+ buffer_json_add_array_item_string(wb, remote_address);
+ buffer_json_add_array_item_uint64(wb, n->remote.port);
+ }
+ buffer_json_add_array_item_string(wb, n->network_viewer.remote_address_space);
+
+ uint16_t server_port;
+ const char *server_address;
+ const char *client_address_space;
+ const char *server_address_space;
+ switch (n->direction) {
+ case SOCKET_DIRECTION_LISTEN:
+ case SOCKET_DIRECTION_INBOUND:
+ case SOCKET_DIRECTION_LOCAL_INBOUND:
+ server_port = n->local.port;
+ server_address = local_address;
+ server_address_space = n->network_viewer.local_address_space;
+ client_address_space = n->network_viewer.remote_address_space;
+ break;
+
+ case SOCKET_DIRECTION_OUTBOUND:
+ case SOCKET_DIRECTION_LOCAL_OUTBOUND:
+ server_port = n->remote.port;
+ server_address = remote_address;
+ server_address_space = n->network_viewer.remote_address_space;
+ client_address_space = n->network_viewer.local_address_space;
+ break;
+
+ case SOCKET_DIRECTION_NONE:
+ break;
+ }
+ if(aggregated)
+ buffer_json_add_array_item_string(wb, server_address);
+
+ buffer_json_add_array_item_uint64(wb, server_port);
+
+ if(aggregated) {
+ buffer_json_add_array_item_string(wb, client_address_space);
+ buffer_json_add_array_item_string(wb, server_address_space);
+ }
+
+ // buffer_json_add_array_item_uint64(wb, n->inode);
+ // buffer_json_add_array_item_uint64(wb, n->net_ns_inode);
+ buffer_json_add_array_item_uint64(wb, n->network_viewer.count);
+ }
+ buffer_json_array_close(wb);
+}
+
+static void local_sockets_cb_to_json(LS_STATE *ls, LOCAL_SOCKET *n, void *data) {
+ n->network_viewer.count = 1;
+ n->network_viewer.local_address_space = local_sockets_address_space(&n->local);
+ n->network_viewer.remote_address_space = local_sockets_address_space(&n->remote);
+ local_socket_to_json_array(data, n, ls->proc_self_net_ns_inode, false);
+}
+
+static void local_sockets_cb_to_aggregation(LS_STATE *ls __maybe_unused, LOCAL_SOCKET *n, void *data) {
+ SIMPLE_HASHTABLE_AGGREGATED_SOCKETS *ht = data;
+ n->network_viewer.count = 1;
+ n->network_viewer.local_address_space = local_sockets_address_space(&n->local);
+ n->network_viewer.remote_address_space = local_sockets_address_space(&n->remote);
+
+ switch(n->direction) {
+ case SOCKET_DIRECTION_INBOUND:
+ case SOCKET_DIRECTION_LOCAL_INBOUND:
+ case SOCKET_DIRECTION_LISTEN:
+ memset(&n->remote.ip, 0, sizeof(n->remote.ip));
+ n->remote.port = 0;
+ break;
+
+ case SOCKET_DIRECTION_OUTBOUND:
+ case SOCKET_DIRECTION_LOCAL_OUTBOUND:
+ memset(&n->local.ip, 0, sizeof(n->local.ip));
+ n->local.port = 0;
+ break;
+
+ case SOCKET_DIRECTION_NONE:
+ return;
+ }
+
+ n->inode = 0;
+ n->local_ip_hash = 0;
+ n->remote_ip_hash = 0;
+ n->local_port_hash = 0;
+ n->timer = 0;
+ n->retransmits = 0;
+ n->expires = 0;
+ n->rqueue = 0;
+ n->wqueue = 0;
+ memset(&n->local_port_key, 0, sizeof(n->local_port_key));
+
+ XXH64_hash_t hash = XXH3_64bits(n, sizeof(*n));
+ SIMPLE_HASHTABLE_SLOT_AGGREGATED_SOCKETS *sl = simple_hashtable_get_slot_AGGREGATED_SOCKETS(ht, hash, n, true);
+ LOCAL_SOCKET *t = SIMPLE_HASHTABLE_SLOT_DATA(sl);
+ if(t) {
+ t->network_viewer.count++;
+ }
+ else {
+ t = mallocz(sizeof(*t));
+ memcpy(t, n, sizeof(*t));
+ t->cmdline = string_dup(t->cmdline);
+ simple_hashtable_set_slot_AGGREGATED_SOCKETS(ht, sl, hash, t);
+ }
+}
+
+static int local_sockets_compar(const void *a, const void *b) {
+ LOCAL_SOCKET *n1 = *(LOCAL_SOCKET **)a, *n2 = *(LOCAL_SOCKET **)b;
+ return strcmp(n1->comm, n2->comm);
+}
+
+void network_viewer_function(const char *transaction, char *function __maybe_unused, usec_t *stop_monotonic_ut __maybe_unused,
+ bool *cancelled __maybe_unused, BUFFER *payload __maybe_unused, HTTP_ACCESS access __maybe_unused,
+ const char *source __maybe_unused, void *data __maybe_unused) {
+
+ time_t now_s = now_realtime_sec();
+ bool aggregated = false;
+
+ CLEAN_BUFFER *wb = buffer_create(0, NULL);
+ buffer_flush(wb);
+ wb->content_type = CT_APPLICATION_JSON;
+ buffer_json_initialize(wb, "\"", "\"", 0, true, BUFFER_JSON_OPTIONS_MINIFY);
+
+ buffer_json_member_add_uint64(wb, "status", HTTP_RESP_OK);
+ buffer_json_member_add_string(wb, "type", "table");
+ buffer_json_member_add_time_t(wb, "update_every", 5);
+ buffer_json_member_add_boolean(wb, "has_history", false);
+ buffer_json_member_add_string(wb, "help", NETWORK_CONNECTIONS_VIEWER_HELP);
+
+#ifdef ENABLE_DETAILED_VIEW
+ buffer_json_member_add_array(wb, "accepted_params");
+ {
+ buffer_json_add_array_item_string(wb, "sockets");
+ }
+ buffer_json_array_close(wb); // accepted_params
+ buffer_json_member_add_array(wb, "required_params");
+ {
+ buffer_json_add_array_item_object(wb);
+ {
+ buffer_json_member_add_string(wb, "id", "sockets");
+ buffer_json_member_add_string(wb, "name", "Sockets");
+ buffer_json_member_add_string(wb, "help", "Select the source type to query");
+ buffer_json_member_add_boolean(wb, "unique_view", true);
+ buffer_json_member_add_string(wb, "type", "select");
+ buffer_json_member_add_array(wb, "options");
+ {
+ buffer_json_add_array_item_object(wb);
+ {
+ buffer_json_member_add_string(wb, "id", "aggregated");
+ buffer_json_member_add_string(wb, "name", "Aggregated view of sockets");
+ }
+ buffer_json_object_close(wb);
+ buffer_json_add_array_item_object(wb);
+ {
+ buffer_json_member_add_string(wb, "id", "detailed");
+ buffer_json_member_add_string(wb, "name", "Detailed view of all sockets");
+ }
+ buffer_json_object_close(wb);
+ }
+ buffer_json_array_close(wb); // options array
+ }
+ buffer_json_object_close(wb);
+ }
+ buffer_json_array_close(wb); // required_params
+#endif
+
+ char function_copy[strlen(function) + 1];
+ memcpy(function_copy, function, sizeof(function_copy));
+ char *words[1024];
+ size_t num_words = quoted_strings_splitter_pluginsd(function_copy, words, 1024);
+ for(size_t i = 1; i < num_words ;i++) {
+ char *param = get_word(words, num_words, i);
+ if(strcmp(param, "sockets:aggregated") == 0) {
+ aggregated = true;
+ }
+ else if(strcmp(param, "sockets:detailed") == 0) {
+ aggregated = false;
+ }
+ else if(strcmp(param, "info") == 0) {
+ goto close_and_send;
+ }
+ }
+
+ if(aggregated) {
+ buffer_json_member_add_object(wb, "aggregated_view");
+ {
+ buffer_json_member_add_string(wb, "column", "Count");
+ buffer_json_member_add_string(wb, "results_label", "unique combinations");
+ buffer_json_member_add_string(wb, "aggregated_label", "sockets");
+ }
+ buffer_json_object_close(wb);
+ }
+
+ {
+ buffer_json_member_add_array(wb, "data");
+
+ LS_STATE ls = {
+ .config = {
+ .listening = true,
+ .inbound = true,
+ .outbound = true,
+ .local = true,
+ .tcp4 = true,
+ .tcp6 = true,
+ .udp4 = true,
+ .udp6 = true,
+ .pid = true,
+ .uid = true,
+ .cmdline = true,
+ .comm = true,
+ .namespaces = true,
+
+ .max_errors = 10,
+ },
+ .stats = { 0 },
+ .sockets_hashtable = { 0 },
+ .local_ips_hashtable = { 0 },
+ .listening_ports_hashtable = { 0 },
+ };
+
+ SIMPLE_HASHTABLE_AGGREGATED_SOCKETS ht = { 0 };
+ if(aggregated) {
+ simple_hashtable_init_AGGREGATED_SOCKETS(&ht, 1024);
+ ls.config.cb = local_sockets_cb_to_aggregation;
+ ls.config.data = &ht;
+ }
+ else {
+ ls.config.cb = local_sockets_cb_to_json;
+ ls.config.data = wb;
+ }
+
+ local_sockets_process(&ls);
+
+ if(aggregated) {
+ LOCAL_SOCKET *array[ht.used];
+ size_t added = 0;
+ uint64_t proc_self_net_ns_inode = ls.proc_self_net_ns_inode;
+ for(SIMPLE_HASHTABLE_SLOT_AGGREGATED_SOCKETS *sl = simple_hashtable_first_read_only_AGGREGATED_SOCKETS(&ht);
+ sl;
+ sl = simple_hashtable_next_read_only_AGGREGATED_SOCKETS(&ht, sl)) {
+ LOCAL_SOCKET *n = SIMPLE_HASHTABLE_SLOT_DATA(sl);
+ if(!n || added >= ht.used) continue;
+
+ array[added++] = n;
+ }
+
+ qsort(array, added, sizeof(LOCAL_SOCKET *), local_sockets_compar);
+
+ for(size_t i = 0; i < added ;i++) {
+ local_socket_to_json_array(wb, array[i], proc_self_net_ns_inode, true);
+ string_freez(array[i]->cmdline);
+ freez(array[i]);
+ }
+
+ simple_hashtable_destroy_AGGREGATED_SOCKETS(&ht);
+ }
+
+ buffer_json_array_close(wb);
+ buffer_json_member_add_object(wb, "columns");
+ {
+ size_t field_id = 0;
+
+ // Direction
+ buffer_rrdf_table_add_field(wb, field_id++, "Direction", "Socket Direction",
+ RRDF_FIELD_TYPE_STRING, RRDF_FIELD_VISUAL_VALUE, RRDF_FIELD_TRANSFORM_NONE,
+ 0, NULL, NAN, RRDF_FIELD_SORT_ASCENDING, NULL,
+ RRDF_FIELD_SUMMARY_COUNT, RRDF_FIELD_FILTER_MULTISELECT,
+ RRDF_FIELD_OPTS_VISIBLE|RRDF_FIELD_OPTS_STICKY,
+ NULL);
+
+ // Protocol
+ buffer_rrdf_table_add_field(wb, field_id++, "Protocol", "Socket Protocol",
+ RRDF_FIELD_TYPE_STRING, RRDF_FIELD_VISUAL_VALUE, RRDF_FIELD_TRANSFORM_NONE,
+ 0, NULL, NAN, RRDF_FIELD_SORT_ASCENDING, NULL,
+ RRDF_FIELD_SUMMARY_COUNT, RRDF_FIELD_FILTER_MULTISELECT,
+ RRDF_FIELD_OPTS_VISIBLE,
+ NULL);
+
+ // Type
+ buffer_rrdf_table_add_field(wb, field_id++, "Namespace", "Namespace",
+ RRDF_FIELD_TYPE_STRING, RRDF_FIELD_VISUAL_VALUE, RRDF_FIELD_TRANSFORM_NONE,
+ 0, NULL, NAN, RRDF_FIELD_SORT_ASCENDING, NULL,
+ RRDF_FIELD_SUMMARY_COUNT, RRDF_FIELD_FILTER_MULTISELECT,
+ RRDF_FIELD_OPTS_VISIBLE,
+ NULL);
+
+ // State
+ buffer_rrdf_table_add_field(wb, field_id++, "State", "Socket State",
+ RRDF_FIELD_TYPE_STRING, RRDF_FIELD_VISUAL_VALUE, RRDF_FIELD_TRANSFORM_NONE,
+ 0, NULL, NAN, RRDF_FIELD_SORT_ASCENDING, NULL,
+ RRDF_FIELD_SUMMARY_COUNT, RRDF_FIELD_FILTER_MULTISELECT,
+ RRDF_FIELD_OPTS_VISIBLE,
+ NULL);
+
+ // Pid
+ buffer_rrdf_table_add_field(wb, field_id++, "PID", "Process ID",
+ RRDF_FIELD_TYPE_INTEGER, RRDF_FIELD_VISUAL_VALUE, RRDF_FIELD_TRANSFORM_NONE,
+ 0, NULL, NAN, RRDF_FIELD_SORT_ASCENDING, NULL,
+ RRDF_FIELD_SUMMARY_COUNT, RRDF_FIELD_FILTER_NONE,
+ RRDF_FIELD_OPTS_VISIBLE,
+ NULL);
+
+ // Comm
+ buffer_rrdf_table_add_field(wb, field_id++, "Process", "Process Name",
+ RRDF_FIELD_TYPE_STRING, RRDF_FIELD_VISUAL_VALUE, RRDF_FIELD_TRANSFORM_NONE,
+ 0, NULL, NAN, RRDF_FIELD_SORT_ASCENDING, NULL,
+ RRDF_FIELD_SUMMARY_COUNT, RRDF_FIELD_FILTER_MULTISELECT,
+ RRDF_FIELD_OPTS_VISIBLE|RRDF_FIELD_OPTS_FULL_WIDTH,
+ NULL);
+
+// // Cmdline
+// buffer_rrdf_table_add_field(wb, field_id++, "CommandLine", "Command Line",
+// RRDF_FIELD_TYPE_STRING, RRDF_FIELD_VISUAL_VALUE, RRDF_FIELD_TRANSFORM_NONE,
+// 0, NULL, NAN, RRDF_FIELD_SORT_ASCENDING, NULL,
+// RRDF_FIELD_SUMMARY_COUNT, RRDF_FIELD_FILTER_NONE,
+// RRDF_FIELD_OPTS_NONE|RRDF_FIELD_OPTS_FULL_WIDTH,
+// NULL);
+
+// // Uid
+// buffer_rrdf_table_add_field(wb, field_id++, "UID", "User ID",
+// RRDF_FIELD_TYPE_INTEGER, RRDF_FIELD_VISUAL_VALUE, RRDF_FIELD_TRANSFORM_NONE,
+// 0, NULL, NAN, RRDF_FIELD_SORT_ASCENDING, NULL,
+// RRDF_FIELD_SUMMARY_COUNT, RRDF_FIELD_FILTER_NONE,
+// RRDF_FIELD_OPTS_NONE,
+// NULL);
+
+ // Username
+ buffer_rrdf_table_add_field(wb, field_id++, "User", "Username",
+ RRDF_FIELD_TYPE_STRING, RRDF_FIELD_VISUAL_VALUE, RRDF_FIELD_TRANSFORM_NONE,
+ 0, NULL, NAN, RRDF_FIELD_SORT_ASCENDING, NULL,
+ RRDF_FIELD_SUMMARY_COUNT, RRDF_FIELD_FILTER_MULTISELECT,
+ RRDF_FIELD_OPTS_VISIBLE,
+ NULL);
+
+ if(!aggregated) {
+ // Local Address
+ buffer_rrdf_table_add_field(wb, field_id++, "LocalIP", "Local IP Address",
+ RRDF_FIELD_TYPE_STRING, RRDF_FIELD_VISUAL_VALUE, RRDF_FIELD_TRANSFORM_NONE,
+ 0, NULL, NAN, RRDF_FIELD_SORT_ASCENDING, NULL,
+ RRDF_FIELD_SUMMARY_COUNT, RRDF_FIELD_FILTER_NONE,
+ RRDF_FIELD_OPTS_VISIBLE|RRDF_FIELD_OPTS_FULL_WIDTH,
+ NULL);
+
+ // Local Port
+ buffer_rrdf_table_add_field(wb, field_id++, "LocalPort", "Local Port",
+ RRDF_FIELD_TYPE_INTEGER, RRDF_FIELD_VISUAL_VALUE, RRDF_FIELD_TRANSFORM_NONE,
+ 0, NULL, NAN, RRDF_FIELD_SORT_ASCENDING, NULL,
+ RRDF_FIELD_SUMMARY_COUNT, RRDF_FIELD_FILTER_NONE,
+ RRDF_FIELD_OPTS_VISIBLE,
+ NULL);
+ }
+
+ // Local Address Space
+ buffer_rrdf_table_add_field(wb, field_id++, "LocalAddressSpace", "Local IP Address Space",
+ RRDF_FIELD_TYPE_STRING, RRDF_FIELD_VISUAL_VALUE, RRDF_FIELD_TRANSFORM_NONE,
+ 0, NULL, NAN, RRDF_FIELD_SORT_ASCENDING, NULL,
+ RRDF_FIELD_SUMMARY_COUNT, RRDF_FIELD_FILTER_MULTISELECT,
+ RRDF_FIELD_OPTS_NONE,
+ NULL);
+
+ if(!aggregated) {
+ // Remote Address
+ buffer_rrdf_table_add_field(wb, field_id++, "RemoteIP", "Remote IP Address",
+ RRDF_FIELD_TYPE_STRING, RRDF_FIELD_VISUAL_VALUE, RRDF_FIELD_TRANSFORM_NONE,
+ 0, NULL, NAN, RRDF_FIELD_SORT_ASCENDING, NULL,
+ RRDF_FIELD_SUMMARY_COUNT, RRDF_FIELD_FILTER_NONE,
+ RRDF_FIELD_OPTS_VISIBLE|RRDF_FIELD_OPTS_FULL_WIDTH,
+ NULL);
+
+ // Remote Port
+ buffer_rrdf_table_add_field(wb, field_id++, "RemotePort", "Remote Port",
+ RRDF_FIELD_TYPE_INTEGER, RRDF_FIELD_VISUAL_VALUE, RRDF_FIELD_TRANSFORM_NONE,
+ 0, NULL, NAN, RRDF_FIELD_SORT_ASCENDING, NULL,
+ RRDF_FIELD_SUMMARY_COUNT, RRDF_FIELD_FILTER_NONE,
+ RRDF_FIELD_OPTS_VISIBLE,
+ NULL);
+ }
+
+ // Remote Address Space
+ buffer_rrdf_table_add_field(wb, field_id++, "RemoteAddressSpace", "Remote IP Address Space",
+ RRDF_FIELD_TYPE_STRING, RRDF_FIELD_VISUAL_VALUE, RRDF_FIELD_TRANSFORM_NONE,
+ 0, NULL, NAN, RRDF_FIELD_SORT_ASCENDING, NULL,
+ RRDF_FIELD_SUMMARY_COUNT, RRDF_FIELD_FILTER_MULTISELECT,
+ RRDF_FIELD_OPTS_NONE,
+ NULL);
+
+ if(aggregated) {
+ // Server IP
+ buffer_rrdf_table_add_field(wb, field_id++, "ServerIP", "Server IP Address",
+ RRDF_FIELD_TYPE_STRING, RRDF_FIELD_VISUAL_VALUE, RRDF_FIELD_TRANSFORM_NONE,
+ 0, NULL, NAN, RRDF_FIELD_SORT_ASCENDING, NULL,
+ RRDF_FIELD_SUMMARY_COUNT, RRDF_FIELD_FILTER_NONE,
+ RRDF_FIELD_OPTS_FULL_WIDTH | (aggregated ? RRDF_FIELD_OPTS_VISIBLE : RRDF_FIELD_OPTS_NONE),
+ NULL);
+ }
+
+ // Server Port
+ buffer_rrdf_table_add_field(wb, field_id++, "ServerPort", "Server Port",
+ RRDF_FIELD_TYPE_INTEGER, RRDF_FIELD_VISUAL_VALUE, RRDF_FIELD_TRANSFORM_NONE,
+ 0, NULL, NAN, RRDF_FIELD_SORT_ASCENDING, NULL,
+ RRDF_FIELD_SUMMARY_COUNT, RRDF_FIELD_FILTER_MULTISELECT,
+ aggregated ? RRDF_FIELD_OPTS_VISIBLE : RRDF_FIELD_OPTS_NONE,
+ NULL);
+
+ if(aggregated) {
+ // Client Address Space
+ buffer_rrdf_table_add_field(wb, field_id++, "ClientAddressSpace", "Client IP Address Space",
+ RRDF_FIELD_TYPE_STRING, RRDF_FIELD_VISUAL_VALUE, RRDF_FIELD_TRANSFORM_NONE,
+ 0, NULL, NAN, RRDF_FIELD_SORT_ASCENDING, NULL,
+ RRDF_FIELD_SUMMARY_COUNT, RRDF_FIELD_FILTER_MULTISELECT,
+ RRDF_FIELD_OPTS_VISIBLE,
+ NULL);
+
+ // Server Address Space
+ buffer_rrdf_table_add_field(wb, field_id++, "ServerAddressSpace", "Server IP Address Space",
+ RRDF_FIELD_TYPE_STRING, RRDF_FIELD_VISUAL_VALUE, RRDF_FIELD_TRANSFORM_NONE,
+ 0, NULL, NAN, RRDF_FIELD_SORT_ASCENDING, NULL,
+ RRDF_FIELD_SUMMARY_COUNT, RRDF_FIELD_FILTER_MULTISELECT,
+ RRDF_FIELD_OPTS_VISIBLE,
+ NULL);
+ }
+
+// // inode
+// buffer_rrdf_table_add_field(wb, field_id++, "Inode", "Socket Inode",
+// RRDF_FIELD_TYPE_INTEGER, RRDF_FIELD_VISUAL_VALUE, RRDF_FIELD_TRANSFORM_NONE,
+// 0, NULL, NAN, RRDF_FIELD_SORT_ASCENDING, NULL,
+// RRDF_FIELD_SUMMARY_COUNT, RRDF_FIELD_FILTER_NONE,
+// RRDF_FIELD_OPTS_NONE,
+// NULL);
+
+// // Namespace inode
+// buffer_rrdf_table_add_field(wb, field_id++, "Namespace Inode", "Namespace Inode",
+// RRDF_FIELD_TYPE_INTEGER, RRDF_FIELD_VISUAL_VALUE, RRDF_FIELD_TRANSFORM_NONE,
+// 0, NULL, NAN, RRDF_FIELD_SORT_ASCENDING, NULL,
+// RRDF_FIELD_SUMMARY_COUNT, RRDF_FIELD_FILTER_NONE,
+// RRDF_FIELD_OPTS_NONE,
+// NULL);
+
+ // Count
+ buffer_rrdf_table_add_field(wb, field_id++, "Count", "Number of sockets like this",
+ RRDF_FIELD_TYPE_INTEGER, RRDF_FIELD_VISUAL_VALUE, RRDF_FIELD_TRANSFORM_NONE,
+ 0, NULL, NAN, RRDF_FIELD_SORT_DESCENDING, NULL,
+ RRDF_FIELD_SUMMARY_COUNT, RRDF_FIELD_FILTER_NONE,
+ aggregated ? (RRDF_FIELD_OPTS_VISIBLE | RRDF_FIELD_OPTS_STICKY) : RRDF_FIELD_OPTS_NONE,
+ NULL);
+ }
+ buffer_json_object_close(wb); // columns
+ buffer_json_member_add_string(wb, "default_sort_column", aggregated ? "Count" : "Direction");
+
+ buffer_json_member_add_object(wb, "custom_charts");
+ {
+ buffer_json_member_add_object(wb, "Network Map");
+ {
+ buffer_json_member_add_string(wb, "type", "network-viewer");
+ }
+ buffer_json_object_close(wb);
+ }
+ buffer_json_object_close(wb); // custom_charts
+
+ buffer_json_member_add_object(wb, "charts");
+ {
+ // Data Collection Age chart
+ buffer_json_member_add_object(wb, "Count");
+ {
+ buffer_json_member_add_string(wb, "type", "stacked-bar");
+ buffer_json_member_add_array(wb, "columns");
+ {
+ buffer_json_add_array_item_string(wb, "Direction");
+ }
+ buffer_json_array_close(wb);
+ }
+ buffer_json_object_close(wb);
+
+ // Streaming Age chart
+ buffer_json_member_add_object(wb, "Count");
+ {
+ buffer_json_member_add_string(wb, "type", "stacked-bar");
+ buffer_json_member_add_array(wb, "columns");
+ {
+ buffer_json_add_array_item_string(wb, "Process");
+ }
+ buffer_json_array_close(wb);
+ }
+ buffer_json_object_close(wb);
+
+ // DB Duration
+ buffer_json_member_add_object(wb, "Count");
+ {
+ buffer_json_member_add_string(wb, "type", "stacked-bar");
+ buffer_json_member_add_array(wb, "columns");
+ {
+ buffer_json_add_array_item_string(wb, "Protocol");
+ }
+ buffer_json_array_close(wb);
+ }
+ buffer_json_object_close(wb);
+ }
+ buffer_json_object_close(wb); // charts
+
+ buffer_json_member_add_array(wb, "default_charts");
+ {
+ buffer_json_add_array_item_array(wb);
+ buffer_json_add_array_item_string(wb, "Count");
+ buffer_json_add_array_item_string(wb, "Direction");
+ buffer_json_array_close(wb);
+
+ buffer_json_add_array_item_array(wb);
+ buffer_json_add_array_item_string(wb, "Count");
+ buffer_json_add_array_item_string(wb, "Process");
+ buffer_json_array_close(wb);
+ }
+ buffer_json_array_close(wb);
+
+ buffer_json_member_add_object(wb, "group_by");
+ {
+ buffer_json_member_add_object(wb, "Direction");
+ {
+ buffer_json_member_add_string(wb, "name", "Direction");
+ buffer_json_member_add_array(wb, "columns");
+ {
+ buffer_json_add_array_item_string(wb, "Direction");
+ }
+ buffer_json_array_close(wb);
+ }
+ buffer_json_object_close(wb);
+
+ buffer_json_member_add_object(wb, "Protocol");
+ {
+ buffer_json_member_add_string(wb, "name", "Protocol");
+ buffer_json_member_add_array(wb, "columns");
+ {
+ buffer_json_add_array_item_string(wb, "Protocol");
+ }
+ buffer_json_array_close(wb);
+ }
+ buffer_json_object_close(wb);
+
+ buffer_json_member_add_object(wb, "Namespace");
+ {
+ buffer_json_member_add_string(wb, "name", "Namespace");
+ buffer_json_member_add_array(wb, "columns");
+ {
+ buffer_json_add_array_item_string(wb, "Namespace");
+ }
+ buffer_json_array_close(wb);
+ }
+ buffer_json_object_close(wb);
+
+ buffer_json_member_add_object(wb, "Process");
+ {
+ buffer_json_member_add_string(wb, "name", "Process");
+ buffer_json_member_add_array(wb, "columns");
+ {
+ buffer_json_add_array_item_string(wb, "Process");
+ }
+ buffer_json_array_close(wb);
+ }
+ buffer_json_object_close(wb);
+
+ if(!aggregated) {
+ buffer_json_member_add_object(wb, "LocalIP");
+ {
+ buffer_json_member_add_string(wb, "name", "Local IP");
+ buffer_json_member_add_array(wb, "columns");
+ {
+ buffer_json_add_array_item_string(wb, "LocalIP");
+ }
+ buffer_json_array_close(wb);
+ }
+ buffer_json_object_close(wb);
+
+ buffer_json_member_add_object(wb, "LocalPort");
+ {
+ buffer_json_member_add_string(wb, "name", "Local Port");
+ buffer_json_member_add_array(wb, "columns");
+ {
+ buffer_json_add_array_item_string(wb, "LocalPort");
+ }
+ buffer_json_array_close(wb);
+ }
+ buffer_json_object_close(wb);
+
+ buffer_json_member_add_object(wb, "RemoteIP");
+ {
+ buffer_json_member_add_string(wb, "name", "Remote IP");
+ buffer_json_member_add_array(wb, "columns");
+ {
+ buffer_json_add_array_item_string(wb, "RemoteIP");
+ }
+ buffer_json_array_close(wb);
+ }
+ buffer_json_object_close(wb);
+
+ buffer_json_member_add_object(wb, "RemotePort");
+ {
+ buffer_json_member_add_string(wb, "name", "Remote Port");
+ buffer_json_member_add_array(wb, "columns");
+ {
+ buffer_json_add_array_item_string(wb, "RemotePort");
+ }
+ buffer_json_array_close(wb);
+ }
+ buffer_json_object_close(wb);
+ }
+ }
+ buffer_json_object_close(wb); // group_by
+ }
+
+close_and_send:
+ buffer_json_member_add_time_t(wb, "expires", now_s + 1);
+ buffer_json_finalize(wb);
+
+ netdata_mutex_lock(&stdout_mutex);
+ pluginsd_function_result_to_stdout(transaction, HTTP_RESP_OK, "application/json", now_s + 1, wb);
+ netdata_mutex_unlock(&stdout_mutex);
+}
+
+// ----------------------------------------------------------------------------------------------------------------
+// main
+
+int main(int argc __maybe_unused, char **argv __maybe_unused) {
+ clocks_init();
+ netdata_thread_set_tag("NETWORK-VIEWER");
+ nd_log_initialize_for_external_plugins("network-viewer.plugin");
+
+ netdata_configured_host_prefix = getenv("NETDATA_HOST_PREFIX");
+ if(verify_netdata_host_prefix(true) == -1) exit(1);
+
+ uc = system_usernames_cache_init();
+
+ // ----------------------------------------------------------------------------------------------------------------
+
+ if(argc == 2 && strcmp(argv[1], "debug") == 0) {
+ bool cancelled = false;
+ usec_t stop_monotonic_ut = now_monotonic_usec() + 600 * USEC_PER_SEC;
+ char buf[] = "network-connections sockets:aggregated";
+ network_viewer_function("123", buf, &stop_monotonic_ut, &cancelled,
+ NULL, HTTP_ACCESS_ALL, NULL, NULL);
+
+ char buf2[] = "network-connections sockets:detailed";
+ network_viewer_function("123", buf2, &stop_monotonic_ut, &cancelled,
+ NULL, HTTP_ACCESS_ALL, NULL, NULL);
+ exit(1);
+ }
+
+ // ----------------------------------------------------------------------------------------------------------------
+
+ fprintf(stdout, PLUGINSD_KEYWORD_FUNCTION " GLOBAL \"%s\" %d \"%s\" \"top\" "HTTP_ACCESS_FORMAT" %d\n",
+ NETWORK_CONNECTIONS_VIEWER_FUNCTION, 60,
+ NETWORK_CONNECTIONS_VIEWER_HELP,
+ (HTTP_ACCESS_FORMAT_CAST)(HTTP_ACCESS_SIGNED_ID | HTTP_ACCESS_SAME_SPACE | HTTP_ACCESS_SENSITIVE_DATA),
+ RRDFUNCTIONS_PRIORITY_DEFAULT);
+
+ // ----------------------------------------------------------------------------------------------------------------
+
+ struct functions_evloop_globals *wg =
+ functions_evloop_init(5, "Network-Viewer", &stdout_mutex, &plugin_should_exit);
+
+ functions_evloop_add_function(wg, NETWORK_CONNECTIONS_VIEWER_FUNCTION,
+ network_viewer_function,
+ PLUGINS_FUNCTIONS_TIMEOUT_DEFAULT,
+ NULL);
+
+ // ----------------------------------------------------------------------------------------------------------------
+
+ usec_t step_ut = 100 * USEC_PER_MS;
+ usec_t send_newline_ut = 0;
+ bool tty = isatty(fileno(stdout)) == 1;
+
+ heartbeat_t hb;
+ heartbeat_init(&hb);
+ while(!plugin_should_exit) {
+
+ usec_t dt_ut = heartbeat_next(&hb, step_ut);
+ send_newline_ut += dt_ut;
+
+ if(!tty && send_newline_ut > USEC_PER_SEC) {
+ send_newline_and_flush(&stdout_mutex);
+ send_newline_ut = 0;
+ }
+ }
+
+ return 0;
+}