diff --git a/client/src/Components/Charts/Utils/chartUtils.jsx b/client/src/Components/Charts/Utils/chartUtils.jsx index af5f27c66..e2560aa2e 100644 --- a/client/src/Components/Charts/Utils/chartUtils.jsx +++ b/client/src/Components/Charts/Utils/chartUtils.jsx @@ -91,6 +91,49 @@ const getFormattedPercentage = (value) => { return `${(value * 100).toFixed(2)}%`; }; +/** + * Custom tick component for rendering network bytes per second. + * + * @param {Object} props - The properties object. + * @param {number} props.x - The x-coordinate for the tick. + * @param {number} props.y - The y-coordinate for the tick. + * @param {Object} props.payload - The payload object containing tick data. + * @param {number} props.index - The index of the tick. + * @returns {JSX.Element|null} The rendered tick component or null for the first tick. + */ +export const NetworkTick = ({ x, y, payload, index }) => { + const theme = useTheme(); + if (index === 0) return null; + + const formatBytes = (bytes) => { + if (bytes >= 1_000_000_000) return `${(bytes / 1_000_000_000).toFixed(1)} GB/s`; + if (bytes >= 1_000_000) return `${(bytes / 1_000_000).toFixed(1)} MB/s`; + if (bytes >= 1_000) return `${(bytes / 1_000).toFixed(1)} KB/s`; + return `${bytes} B/s`; + }; + + return ( + + {formatBytes(payload?.value)} + + ); +}; + +NetworkTick.propTypes = { + x: PropTypes.number, + y: PropTypes.number, + payload: PropTypes.object, + index: PropTypes.number, +}; + + /** * Custom tooltip component for displaying infrastructure data. * diff --git a/client/src/Pages/Infrastructure/Details/Components/NetworkStats/NetworkCharts.jsx b/client/src/Pages/Infrastructure/Details/Components/NetworkStats/NetworkCharts.jsx index 7aad494ab..74ddb6bae 100644 --- a/client/src/Pages/Infrastructure/Details/Components/NetworkStats/NetworkCharts.jsx +++ b/client/src/Pages/Infrastructure/Details/Components/NetworkStats/NetworkCharts.jsx @@ -7,6 +7,7 @@ import InfraAreaChart from "../../../../../Pages/Infrastructure/Details/Componen import { TzTick, InfrastructureTooltip, + NetworkTick, } from "../../../../../Components/Charts/Utils/chartUtils"; import { useTheme } from "@emotion/react"; import { useTranslation } from "react-i18next"; @@ -23,6 +24,7 @@ const NetworkCharts = ({ eth0Data, dateRange }) => { const theme = useTheme(); const { t } = useTranslation(); + const configs = [ { type: "network-bytes", @@ -33,6 +35,7 @@ const NetworkCharts = ({ eth0Data, dateRange }) => { gradientStartColor: theme.palette.info.main, yLabel: t("bytesPerSecond"), xTick: , + yTick: , toolTip: ( { - const eth0Data = getEth0TimeSeries(checks); + const eth0Data = (checks || []) + .map((check) => { + const en0 = (check.net || []).find((iface) => iface.name === "en0"); + if (!en0) return null; + + return { + _id: check._id, + bytesPerSec: en0.avgBytesRecv, + packetsPerSec: en0.avgPacketsRecv, + errors: (en0.avgErrOut ?? 0), + drops: (en0.avgDropOut ?? 0) + }; + }) + .filter(Boolean); + + console.log(eth0Data); return ( <> - + - + ); }; @@ -34,65 +43,3 @@ Network.propTypes = { }; export default Network; - -/* ---------- Helper functions ---------- */ -function getEth0TimeSeries(checks) { - const sorted = [...(checks || [])].sort((a, b) => new Date(a._id) - new Date(b._id)); - const series = []; - let prev = null; - - for (const check of sorted) { - const eth = (check.net || []).find((iface) => iface.name === "en0"); - if (!eth) { - prev = check; - continue; - } - - if (prev) { - const prevEth = (prev.net || []).find((iface) => iface.name === "en0"); - const t1 = new Date(check._id); - const t0 = new Date(prev._id); - - if (!prevEth || isNaN(t1) || isNaN(t0)) { - prev = check; - continue; - } - - const dt = (t1 - t0) / 1000; - - if (dt > 0) { - const bytesField = eth.avgBytesSent; - const prevBytesField = prevEth.avgBytesSent; - - if (bytesField !== undefined && prevBytesField !== undefined) { - const dataPoint = { - _id: check._id, - bytesPerSec: (bytesField - prevBytesField) / dt, - packetsPerSec: (eth.avgPacketsSent - prevEth.avgPacketsSent) / dt, - errors: (eth.avgErrIn ?? 0) + (eth.avgErrOut ?? 0), - drops: 0, - }; - series.push(dataPoint); - } - } - } - prev = check; - } - - // If we only have one check, create a single data point with absolute values - if (series.length === 0 && sorted.length === 1) { - const check = sorted[0]; - const eth = (check.net || []).find((iface) => iface.name === "en0"); - if (eth) { - series.push({ - _id: check._id, - bytesPerSec: eth.avgBytesSent || 0, - packetsPerSec: eth.avgPacketsSent || 0, - errors: (eth.avgErrIn ?? 0) + (eth.avgErrOut ?? 0), - drops: 0, - }); - } - } - - return series; -} diff --git a/server/src/db/mongo/modules/monitorModule.js b/server/src/db/mongo/modules/monitorModule.js index 76f055f94..4cd28ea9d 100755 --- a/server/src/db/mongo/modules/monitorModule.js +++ b/server/src/db/mongo/modules/monitorModule.js @@ -326,10 +326,53 @@ class MonitorModule { } }; + processNetworkRates = (checks) => { + if (!Array.isArray(checks) || checks.length === 0) return []; + + const sorted = [...checks].sort((a, b) => new Date(a._id) - new Date(b._id)); + const lastSeen = {}; + + for (const check of sorted) { + if (!Array.isArray(check.net)) { + check.net = []; + continue; + } + + const newNet = []; + + for (const iface of check.net) { + const prev = lastSeen[iface.name]; + const t1 = new Date(check._id); + + if (prev) { + const t0 = new Date(prev._id); + const dt = (t1 - t0) / 1000; + + if (dt > 0) { + newNet.push({ + name: iface.name, + avgBytesRecv: (iface.avgBytesRecv - prev.avgBytesRecv), + avgPacketsRecv: (iface.avgPacketsRecv - prev.avgPacketsRecv), + avgErrOut: iface.avgErrOut - prev.avgErrOut, + avgDropOut: iface.avgDropOut, + }); + } + } + + lastSeen[iface.name] = { ...iface, _id: check._id }; + } + + check.net = newNet; + } + + return sorted; + }; + getHardwareDetailsById = async ({ monitorId, dateRange }) => { try { const monitor = await this.Monitor.findById(monitorId); const dates = this.getDateRange(dateRange); + const formatLookup = { recent: "%Y-%m-%dT%H:%M:00Z", day: "%Y-%m-%dT%H:00:00Z", @@ -337,13 +380,19 @@ class MonitorModule { month: "%Y-%m-%dT00:00:00Z", }; const dateString = formatLookup[dateRange]; + const hardwareStats = await this.HardwareCheck.aggregate(buildHardwareDetailsPipeline(monitor, dates, dateString)); - const monitorStats = { + const stats = hardwareStats[0]; + if (stats && stats.checks) { + // Replace net with per-second rates + stats.checks = this.processNetworkRates(stats.checks); + } + + return { ...monitor.toObject(), - stats: hardwareStats[0], + stats, }; - return monitorStats; } catch (error) { error.service = SERVICE_NAME; error.method = "getHardwareDetailsById"; diff --git a/server/src/db/mongo/modules/monitorModuleQueries.js b/server/src/db/mongo/modules/monitorModuleQueries.js index df15593e2..79804f183 100755 --- a/server/src/db/mongo/modules/monitorModuleQueries.js +++ b/server/src/db/mongo/modules/monitorModuleQueries.js @@ -393,7 +393,7 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => { 0, ], }, - bytesSent: { + avgBytesSent: { $avg: { $map: { input: "$net", @@ -404,7 +404,7 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => { }, }, }, - bytesRecv: { + avgBytesRecv: { $avg: { $map: { input: "$net", @@ -415,7 +415,7 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => { }, }, }, - packetsSent: { + avgPacketsSent: { $avg: { $map: { input: "$net", @@ -426,7 +426,7 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => { }, }, }, - packetsRecv: { + avgPacketsRecv: { $avg: { $map: { input: "$net", @@ -437,7 +437,7 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => { }, }, }, - errIn: { + avgErrIn: { $avg: { $map: { input: "$net", @@ -448,7 +448,7 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => { }, }, }, - errOut: { + avgErrOut: { $avg: { $map: { input: "$net", @@ -459,6 +459,28 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => { }, }, }, + avgDropIn: { + $avg: { + $map: { + input: "$net", + as: "netArray", + in: { + $arrayElemAt: ["$$netArray.drop_in", "$$netIndex"], + }, + }, + }, + }, + avgDropOut: { + $avg: { + $map: { + input: "$net", + as: "netArray", + in: { + $arrayElemAt: ["$$netArray.drop_out", "$$netIndex"], + }, + }, + }, + }, }, }, },