diff --git a/client/src/Components/Charts/Utils/chartUtils.jsx b/client/src/Components/Charts/Utils/chartUtils.jsx index 414e1ff9c..e24881c1c 100644 --- a/client/src/Components/Charts/Utils/chartUtils.jsx +++ b/client/src/Components/Charts/Utils/chartUtils.jsx @@ -88,7 +88,51 @@ PercentTick.propTypes = { */ const getFormattedPercentage = (value) => { if (typeof value !== "number") return value; - return `${(value * 100).toFixed(2)}.%`; + 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, formatter }) => { + const theme = useTheme(); + if (index === 0) return null; + + if (formatter === undefined) { + formatter = (value, space = false) => { + if (typeof value !== "number") return value; + // need to add space between value and unit + return `${(value / 1024).toFixed(1)}${space ? " " : ""}Kbps`; + }; + } + + return ( + + {formatter(payload?.value, true)} + + ); +}; + +NetworkTick.propTypes = { + x: PropTypes.number, + y: PropTypes.number, + payload: PropTypes.object, + index: PropTypes.number, + formatter: PropTypes.func, }; /** @@ -112,6 +156,7 @@ export const InfrastructureTooltip = ({ yLabel, dotColor, dateRange, + formatter = getFormattedPercentage, }) => { const uiTimezone = useSelector((state) => state.ui.timezone); const theme = useTheme(); @@ -166,8 +211,8 @@ export const InfrastructureTooltip = ({ sx={{ opacity: 0.8 }} > {yIdx >= 0 - ? `${yLabel} ${getFormattedPercentage(payload[0].payload[hardwareType][yIdx][metric])}` - : `${yLabel} ${getFormattedPercentage(payload[0].payload[yKey])}`} + ? `${yLabel} ${formatter(payload[0].payload[hardwareType][yIdx][metric])}` + : `${yLabel} ${formatter(payload[0].payload[yKey])}`} diff --git a/client/src/Pages/Infrastructure/Details/Components/NetworkStats/NetworkCharts.jsx b/client/src/Pages/Infrastructure/Details/Components/NetworkStats/NetworkCharts.jsx new file mode 100644 index 000000000..9157a3432 --- /dev/null +++ b/client/src/Pages/Infrastructure/Details/Components/NetworkStats/NetworkCharts.jsx @@ -0,0 +1,132 @@ +import PropTypes from "prop-types"; +import { Stack, Typography } from "@mui/material"; +import InfraAreaChart from "../../../../../Pages/Infrastructure/Details/Components/AreaChartBoxes/InfraAreaChart"; + +import { + TzTick, + InfrastructureTooltip, + NetworkTick, +} from "../../../../../Components/Charts/Utils/chartUtils"; +import { useTheme } from "@emotion/react"; +import { useTranslation } from "react-i18next"; +import { useHardwareUtils } from "../../Hooks/useHardwareUtils"; + +const NetworkCharts = ({ ethernetData, dateRange }) => { + const theme = useTheme(); + const { t } = useTranslation(); + + const { formatBytesPerSecondString, formatPacketsPerSecondString } = useHardwareUtils(); + + if (!ethernetData?.length) { + return {t("noNetworkStatsAvailable")}; + } + + const configs = [ + { + type: "network-bytes", + data: ethernetData, + dataKeys: ["bytesPerSec"], + heading: t("dataReceived"), + strokeColor: theme.palette.info.main, + gradientStartColor: theme.palette.info.main, + yLabel: t("rate"), + xTick: , + yTick: , + toolTip: ( + + ), + }, + { + type: "network-packets", + data: ethernetData, + dataKeys: ["packetsPerSec"], + heading: t("packetsReceivedRate"), + strokeColor: theme.palette.success.main, + gradientStartColor: theme.palette.success.main, + yLabel: t("rate"), + xTick: , + yTick: , + toolTip: ( + + ), + }, + { + type: "network-errors", + data: ethernetData, + dataKeys: ["errors"], + heading: t("networkErrors"), + strokeColor: theme.palette.error.main, + gradientStartColor: theme.palette.error.main, + yLabel: t("errors"), + xTick: , + toolTip: ( + Math.round(value).toLocaleString()} + /> + ), + }, + { + type: "network-drops", + data: ethernetData, + dataKeys: ["drops"], + heading: t("networkDrops"), + strokeColor: theme.palette.warning.main, + gradientStartColor: theme.palette.warning.main, + yLabel: t("drops"), + xTick: , + toolTip: ( + Math.round(value).toLocaleString()} + /> + ), + }, + ]; + + return ( + *": { + flexBasis: `calc(50% - ${theme.spacing(8)})`, + maxWidth: `calc(50% - ${theme.spacing(8)})`, + }, + }} + > + {configs.map((config) => ( + + ))} + + ); +}; + +NetworkCharts.propTypes = { + ethernetData: PropTypes.array.isRequired, + dateRange: PropTypes.string.isRequired, +}; + +export default NetworkCharts; diff --git a/client/src/Pages/Infrastructure/Details/Components/NetworkStats/NetworkStatBoxes.jsx b/client/src/Pages/Infrastructure/Details/Components/NetworkStats/NetworkStatBoxes.jsx new file mode 100644 index 000000000..98ba90dae --- /dev/null +++ b/client/src/Pages/Infrastructure/Details/Components/NetworkStats/NetworkStatBoxes.jsx @@ -0,0 +1,81 @@ +import PropTypes from "prop-types"; +import StatusBoxes from "../../../../../Components/StatusBoxes"; +import StatBox from "../../../../../Components/StatBox"; +import { Typography } from "@mui/material"; +import { useTranslation } from "react-i18next"; +import { useHardwareUtils } from "../../Hooks/useHardwareUtils"; + +function formatNumber(num) { + return num != null ? num.toLocaleString() : "0"; +} + +const NetworkStatBoxes = ({ shouldRender, net, ifaceName }) => { + const { t } = useTranslation(); + const { formatBytes } = useHardwareUtils(); + + const filtered = net?.filter((iface) => iface.name === ifaceName) || []; + + if (!net?.length) { + return {t("noNetworkStatsAvailable")}; + } + + return ( + + {filtered + .map((iface) => [ + , + , + , + , + , + , + ]) + .flat()} + + ); +}; + +NetworkStatBoxes.propTypes = { + shouldRender: PropTypes.bool.isRequired, + net: PropTypes.arrayOf( + PropTypes.shape({ + name: PropTypes.string.isRequired, + bytes_sent: PropTypes.number, + bytes_recv: PropTypes.number, + packets_sent: PropTypes.number, + packets_recv: PropTypes.number, + err_in: PropTypes.number, + err_out: PropTypes.number, + }) + ), + ifaceName: PropTypes.string.isRequired, +}; + +export default NetworkStatBoxes; diff --git a/client/src/Pages/Infrastructure/Details/Components/NetworkStats/index.jsx b/client/src/Pages/Infrastructure/Details/Components/NetworkStats/index.jsx new file mode 100644 index 000000000..227523439 --- /dev/null +++ b/client/src/Pages/Infrastructure/Details/Components/NetworkStats/index.jsx @@ -0,0 +1,110 @@ +import PropTypes from "prop-types"; +import { useState, useEffect } from "react"; +import { FormControl, InputLabel, Select, MenuItem, Box } from "@mui/material"; +import { useTranslation } from "react-i18next"; +import { useTheme } from "@emotion/react"; +import NetworkStatBoxes from "./NetworkStatBoxes"; +import NetworkCharts from "./NetworkCharts"; +import MonitorTimeFrameHeader from "../../../../../Components/MonitorTimeFrameHeader"; + +const getAvailableInterfaces = (net) => { + return (net || []).map((iface) => iface.name).filter(Boolean); +}; + +const getNetworkInterfaceData = (checks, ifaceName) => { + if (!ifaceName) return []; + + // Transform backend data structure for the selected interface + // Backend already calculates deltas, we just reshape the data + return (checks || []) + .map((check) => { + const networkInterface = (check.net || []).find( + (iface) => iface.name === ifaceName + ); + if (!networkInterface) return null; + return { + _id: check._id, + bytesPerSec: networkInterface.deltaBytesRecv, + packetsPerSec: networkInterface.deltaPacketsRecv, + errors: networkInterface.deltaErrOut ?? 0, + drops: networkInterface.deltaDropOut ?? 0, + }; + }) + .filter(Boolean); +}; + +const Network = ({ net, checks, isLoading, dateRange, setDateRange }) => { + const { t } = useTranslation(); + const theme = useTheme(); + + const availableInterfaces = getAvailableInterfaces(net); + const [selectedInterface, setSelectedInterface] = useState(""); + + // Set default interface when data loads + useEffect(() => { + if (availableInterfaces.length > 0 && !selectedInterface) { + setSelectedInterface(availableInterfaces[0]); + } + }, [availableInterfaces, selectedInterface]); + + const ethernetData = getNetworkInterfaceData(checks, selectedInterface); + + return ( + <> + + + {availableInterfaces.length > 0 && ( + + {t("networkInterface")} + + + )} + + + + + ); +}; + +Network.propTypes = { + net: PropTypes.array, + checks: PropTypes.array, + isLoading: PropTypes.bool.isRequired, + dateRange: PropTypes.string.isRequired, + setDateRange: PropTypes.func.isRequired, +}; + +export default Network; diff --git a/client/src/Pages/Infrastructure/Details/Components/NetworkStats/skeleton.jsx b/client/src/Pages/Infrastructure/Details/Components/NetworkStats/skeleton.jsx new file mode 100644 index 000000000..52fcfad87 --- /dev/null +++ b/client/src/Pages/Infrastructure/Details/Components/NetworkStats/skeleton.jsx @@ -0,0 +1,56 @@ +import { + Card, + CardContent, + Skeleton, + Table, + TableHead, + TableRow, + TableCell, + TableBody, +} from "@mui/material"; + +const SkeletonLayout = () => { + return ( + + + + + + + Name + Bytes Sent + Bytes Received + Packets Sent + Packets Received + Errors In + Errors Out + Drops In + Drops Out + + + + {Array.from({ length: 5 }).map((_, idx) => ( + + {Array.from({ length: 9 }).map((__, colIdx) => ( + + + + ))} + + ))} + +
+
+
+ ); +}; + +export default SkeletonLayout; diff --git a/client/src/Pages/Infrastructure/Details/Hooks/useHardwareUtils.jsx b/client/src/Pages/Infrastructure/Details/Hooks/useHardwareUtils.jsx index a2e4c5153..8edb6fb87 100644 --- a/client/src/Pages/Infrastructure/Details/Hooks/useHardwareUtils.jsx +++ b/client/src/Pages/Infrastructure/Details/Hooks/useHardwareUtils.jsx @@ -57,7 +57,7 @@ const useHardwareUtils = () => { if (GB >= 1) { return ( <> - {Number(GB.toFixed(0))} + {Number(GB.toFixed(2))} {space ? " " : ""} {t("gb")} @@ -65,7 +65,7 @@ const useHardwareUtils = () => { } else { return ( <> - {Number(MB.toFixed(0))} + {Number(MB.toFixed(2))} {space ? " " : ""} {t("mb")} @@ -73,6 +73,53 @@ const useHardwareUtils = () => { } }; + const formatBytesPerSecondString = (bytesPerSec, space = false) => { + if ( + bytesPerSec === undefined || + bytesPerSec === null || + typeof bytesPerSec !== "number" || + bytesPerSec === 0 + ) { + return `0${space ? " " : ""}B/s`; + } + + const GB = bytesPerSec / (1024 * 1024 * 1024); + const MB = bytesPerSec / (1024 * 1024); + const KB = bytesPerSec / 1024; + + if (GB >= 1) { + return `${Number(GB.toFixed(1))}${space ? " " : ""}GB/s`; + } else if (MB >= 1) { + return `${Number(MB.toFixed(1))}${space ? " " : ""}MB/s`; + } else if (KB >= 1) { + return `${Number(KB.toFixed(1))}${space ? " " : ""}KB/s`; + } else { + return `${Number(bytesPerSec.toFixed(1))}${space ? " " : ""}B/s`; + } + }; + + const formatPacketsPerSecondString = (packetsPerSec, space = false) => { + if ( + packetsPerSec === undefined || + packetsPerSec === null || + typeof packetsPerSec !== "number" || + packetsPerSec === 0 + ) { + return `0${space ? " " : ""}pps`; + } + + const M = packetsPerSec / (1000 * 1000); + const K = packetsPerSec / 1000; + + if (M >= 1) { + return `${Number(M.toFixed(1))}${space ? " " : ""}Mpps`; + } else if (K >= 1) { + return `${Number(K.toFixed(1))}${space ? " " : ""}Kpps`; + } else { + return `${Math.round(packetsPerSec)}${space ? " " : ""}pps`; + } + }; + /** * Converts a decimal value to a percentage * @@ -134,6 +181,8 @@ const useHardwareUtils = () => { decimalToPercentage, buildTemps, getDimensions, + formatBytesPerSecondString, + formatPacketsPerSecondString, }; }; diff --git a/client/src/Pages/Infrastructure/Details/index.jsx b/client/src/Pages/Infrastructure/Details/index.jsx index 30840851f..72f384101 100644 --- a/client/src/Pages/Infrastructure/Details/index.jsx +++ b/client/src/Pages/Infrastructure/Details/index.jsx @@ -1,5 +1,5 @@ // Components -import { Stack, Typography } from "@mui/material"; +import { Stack, Typography, Tab } from "@mui/material"; import Breadcrumbs from "../../../Components/Breadcrumbs"; import MonitorDetailsControlHeader from "../../../Components/MonitorDetailsControlHeader"; import MonitorTimeFrameHeader from "../../../Components/MonitorTimeFrameHeader"; @@ -7,6 +7,9 @@ import StatusBoxes from "./Components/StatusBoxes"; import GaugeBoxes from "./Components/GaugeBoxes"; import AreaChartBoxes from "./Components/AreaChartBoxes"; import GenericFallback from "../../../Components/GenericFallback"; +import NetworkStats from "./Components/NetworkStats"; +import CustomTabList from "../../../Components/Tab"; +import TabContext from "@mui/lab/TabContext"; // Utils import { useTheme } from "@emotion/react"; @@ -22,11 +25,10 @@ const BREADCRUMBS = [ { name: "details", path: "" }, ]; const InfrastructureDetails = () => { - // Redux state - // Local state const [dateRange, setDateRange] = useState("recent"); const [trigger, setTrigger] = useState(false); + const [tab, setTab] = useState("details"); // Utils const theme = useTheme(); @@ -87,23 +89,51 @@ const InfrastructureDetails = () => { monitor={monitor} triggerUpdate={triggerUpdate} /> - - - - + + setTab(v)} + > + + + + {tab === "details" && ( + <> + + + + + + )} + {tab === "network" && ( + + )} + ); }; diff --git a/client/src/locales/en.json b/client/src/locales/en.json index ecb080357..857825c9f 100644 --- a/client/src/locales/en.json +++ b/client/src/locales/en.json @@ -202,6 +202,13 @@ "avgCpuTemperature": "Average CPU Temperature", "bar": "Bar", "basicInformation": "Basic Information", + "bytesPerSecond": "Bytes per second", + "bytesReceived": "Bytes Received", + "bytesSent": "Bytes Sent", + "dataReceived": "Data Received", + "dataSent": "Data Sent", + "dataRate": "Data Rate", + "rate": "Rate", "bulkImport": { "fallbackPage": "Import a file to upload a list of servers in bulk", "invalidFileType": "Invalid file type", @@ -334,6 +341,15 @@ }, "disk": "Disk", "diskUsage": "Disk Usage", + "drops": "Drops", + "errors": "Errors", + "errorsIn": "Errors In", + "errorsOut": "Errors Out", + "networkErrors": "Network Errors", + "networkDrops": "Network Drops", + "networkInterface": "Network Interface", + "selectInterface": "Select Interface", + "details": "Details", "displayName": "Display name", "distributedRightCategoryTitle": "Monitor", "distributedStatusHeaderText": "Real-time, real-device coverage", @@ -639,7 +655,9 @@ }, "ms": "ms", "navControls": "Controls", + "network": "Network", "nextWindow": "Next window", + "noNetworkStatsAvailable": "No network stats available.", "notFoundButton": "Go to the main dashboard", "notificationConfig": { "description": "Select the notifications channels you want to use", @@ -751,6 +769,10 @@ "passwordRequirements": "New password must contain at least 8 characters and must have at least one uppercase letter, one lowercase letter, one number and one special character.", "saving": "Saving..." }, + "packetsPerSecond": "Packets per second", + "packetsReceived": "Packets Received", + "packetsReceivedRate": "Packets Received Rate", + "packetsSent": "Packets Sent", "pause": "Pause", "pingMonitoring": "Ping monitoring", "pingMonitoringDescription": "Check whether your server is available or not.", diff --git a/server/src/db/mongo/modules/monitorModule.js b/server/src/db/mongo/modules/monitorModule.js index c4a896030..0e301cdcf 100755 --- a/server/src/db/mongo/modules/monitorModule.js +++ b/server/src/db/mongo/modules/monitorModule.js @@ -330,6 +330,7 @@ class MonitorModule { 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 +338,15 @@ 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]; + + 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 a5a56ff9b..7753066fc 100755 --- a/server/src/db/mongo/modules/monitorModuleQueries.js +++ b/server/src/db/mongo/modules/monitorModuleQueries.js @@ -263,6 +263,9 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => { net: { $push: "$net", }, + updatedAts: { + $push: "$updatedAt", + }, }, }, { @@ -378,90 +381,383 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => { }, net: { $map: { - input: { $range: [0, "$$netCount"] }, + input: { $range: [0, { $size: { $arrayElemAt: ["$net", 0] } }] }, as: "netIndex", in: { name: { $arrayElemAt: [ { $map: { - input: "$net", - as: "netArray", - in: { $arrayElemAt: ["$$netArray.name", "$$netIndex"] }, + input: { $arrayElemAt: ["$net", 0] }, + as: "iface", + in: "$$iface.name", }, }, - 0, + "$$netIndex", ], }, - avgBytesSent: { - $avg: { - $map: { - input: "$net", - as: "netArray", - in: { - $arrayElemAt: ["$$netArray.bytes_sent", "$$netIndex"], + bytesSentPerSecond: { + $let: { + vars: { + first: { + $arrayElemAt: [ + { $map: { input: { $arrayElemAt: ["$net", 0] }, as: "iface", in: "$$iface.bytes_sent" } }, + "$$netIndex", + ], }, + last: { + $arrayElemAt: [ + { + $map: { + input: { $arrayElemAt: ["$net", { $subtract: [{ $size: "$net" }, 1] }] }, + as: "iface", + in: "$$iface.bytes_sent", + }, + }, + "$$netIndex", + ], + }, + tFirst: { $arrayElemAt: ["$updatedAts", 0] }, + tLast: { $arrayElemAt: ["$updatedAts", { $subtract: [{ $size: "$updatedAts" }, 1] }] }, + }, + in: { + $cond: [ + { $gt: [{ $subtract: ["$$tLast", "$$tFirst"] }, 0] }, + { + $divide: [{ $subtract: ["$$last", "$$first"] }, { $divide: [{ $subtract: ["$$tLast", "$$tFirst"] }, 1000] }], + }, + 0, + ], }, }, }, - avgBytesRecv: { - $avg: { - $map: { - input: "$net", - as: "netArray", - in: { - $arrayElemAt: ["$$netArray.bytes_recv", "$$netIndex"], + deltaBytesRecv: { + $let: { + vars: { + first: { + $arrayElemAt: [ + { $map: { input: { $arrayElemAt: ["$net", 0] }, as: "iface", in: "$$iface.bytes_recv" } }, + "$$netIndex", + ], }, + last: { + $arrayElemAt: [ + { + $map: { + input: { $arrayElemAt: ["$net", { $subtract: [{ $size: "$net" }, 1] }] }, + as: "iface", + in: "$$iface.bytes_recv", + }, + }, + "$$netIndex", + ], + }, + tFirst: { $arrayElemAt: ["$updatedAts", 0] }, + tLast: { $arrayElemAt: ["$updatedAts", { $subtract: [{ $size: "$updatedAts" }, 1] }] }, + }, + in: { + $cond: [ + { $gt: [{ $subtract: ["$$tLast", "$$tFirst"] }, 0] }, + { + $divide: [{ $subtract: ["$$last", "$$first"] }, { $divide: [{ $subtract: ["$$tLast", "$$tFirst"] }, 1000] }], + }, + 0, + ], }, }, }, - avgPacketsSent: { - $avg: { - $map: { - input: "$net", - as: "netArray", - in: { - $arrayElemAt: ["$$netArray.packets_sent", "$$netIndex"], + deltaPacketsSent: { + $let: { + vars: { + first: { + $arrayElemAt: [ + { $map: { input: { $arrayElemAt: ["$net", 0] }, as: "iface", in: "$$iface.packets_sent" } }, + "$$netIndex", + ], }, + last: { + $arrayElemAt: [ + { + $map: { + input: { $arrayElemAt: ["$net", { $subtract: [{ $size: "$net" }, 1] }] }, + as: "iface", + in: "$$iface.packets_sent", + }, + }, + "$$netIndex", + ], + }, + tFirst: { $arrayElemAt: ["$updatedAts", 0] }, + tLast: { $arrayElemAt: ["$updatedAts", { $subtract: [{ $size: "$updatedAts" }, 1] }] }, + }, + in: { + $cond: [ + { $gt: [{ $subtract: ["$$tLast", "$$tFirst"] }, 0] }, + { + $divide: [{ $subtract: ["$$last", "$$first"] }, { $divide: [{ $subtract: ["$$tLast", "$$tFirst"] }, 1000] }], + }, + 0, + ], }, }, }, - avgPacketsRecv: { - $avg: { - $map: { - input: "$net", - as: "netArray", - in: { - $arrayElemAt: ["$$netArray.packets_recv", "$$netIndex"], + deltaPacketsRecv: { + $let: { + vars: { + first: { + $arrayElemAt: [ + { + $map: { + input: { $arrayElemAt: ["$net", 0] }, + as: "iface", + in: "$$iface.packets_recv", + }, + }, + "$$netIndex", + ], }, + last: { + $arrayElemAt: [ + { + $map: { + input: { + $arrayElemAt: [ + "$net", + { + $subtract: [{ $size: "$net" }, 1], + }, + ], + }, + as: "iface", + in: "$$iface.packets_recv", + }, + }, + "$$netIndex", + ], + }, + tFirst: { $arrayElemAt: ["$updatedAts", 0] }, + tLast: { + $arrayElemAt: [ + "$updatedAts", + { + $subtract: [{ $size: "$updatedAts" }, 1], + }, + ], + }, + }, + in: { + $cond: [ + { $gt: [{ $subtract: ["$$tLast", "$$tFirst"] }, 0] }, + { + $divide: [{ $subtract: ["$$last", "$$first"] }, { $divide: [{ $subtract: ["$$tLast", "$$tFirst"] }, 1000] }], + }, + 0, + ], }, }, }, - avgErrIn: { - $avg: { - $map: { - input: "$net", - as: "netArray", - in: { - $arrayElemAt: ["$$netArray.err_in", "$$netIndex"], + deltaErrIn: { + $let: { + vars: { + first: { + $arrayElemAt: [{ $map: { input: { $arrayElemAt: ["$net", 0] }, as: "iface", in: "$$iface.err_in" } }, "$$netIndex"], }, + last: { + $arrayElemAt: [ + { + $map: { + input: { $arrayElemAt: ["$net", { $subtract: [{ $size: "$net" }, 1] }] }, + as: "iface", + in: "$$iface.err_in", + }, + }, + "$$netIndex", + ], + }, + tFirst: { $arrayElemAt: ["$updatedAts", 0] }, + tLast: { $arrayElemAt: ["$updatedAts", { $subtract: [{ $size: "$updatedAts" }, 1] }] }, + }, + in: { + $cond: [ + { $gt: [{ $subtract: ["$$tLast", "$$tFirst"] }, 0] }, + { + $divide: [{ $subtract: ["$$last", "$$first"] }, { $divide: [{ $subtract: ["$$tLast", "$$tFirst"] }, 1000] }], + }, + 0, + ], }, }, }, - avgErrOut: { - $avg: { - $map: { - input: "$net", - as: "netArray", - in: { - $arrayElemAt: ["$$netArray.err_out", "$$netIndex"], + deltaErrOut: { + $let: { + vars: { + first: { + $arrayElemAt: [ + { + $map: { + input: { $arrayElemAt: ["$net", 0] }, + as: "iface", + in: "$$iface.err_out", + }, + }, + "$$netIndex", + ], }, + last: { + $arrayElemAt: [ + { + $map: { + input: { + $arrayElemAt: [ + "$net", + { + $subtract: [{ $size: "$net" }, 1], + }, + ], + }, + as: "iface", + in: "$$iface.err_out", + }, + }, + "$$netIndex", + ], + }, + tFirst: { $arrayElemAt: ["$updatedAts", 0] }, + tLast: { + $arrayElemAt: [ + "$updatedAts", + { + $subtract: [{ $size: "$updatedAts" }, 1], + }, + ], + }, + }, + in: { + $cond: [ + { $gt: [{ $subtract: ["$$tLast", "$$tFirst"] }, 0] }, + { + $divide: [{ $subtract: ["$$last", "$$first"] }, { $divide: [{ $subtract: ["$$tLast", "$$tFirst"] }, 1000] }], + }, + 0, + ], + }, + }, + }, + deltaDropIn: { + $let: { + vars: { + first: { + $arrayElemAt: [ + { + $map: { + input: { $arrayElemAt: ["$net", 0] }, + as: "iface", + in: "$$iface.drop_in", + }, + }, + "$$netIndex", + ], + }, + last: { + $arrayElemAt: [ + { + $map: { + input: { + $arrayElemAt: [ + "$net", + { + $subtract: [{ $size: "$net" }, 1], + }, + ], + }, + as: "iface", + in: "$$iface.drop_in", + }, + }, + "$$netIndex", + ], + }, + tFirst: { $arrayElemAt: ["$updatedAts", 0] }, + tLast: { + $arrayElemAt: [ + "$updatedAts", + { + $subtract: [{ $size: "$updatedAts" }, 1], + }, + ], + }, + }, + in: { + $cond: [ + { $gt: [{ $subtract: ["$$tLast", "$$tFirst"] }, 0] }, + { + $divide: [{ $subtract: ["$$last", "$$first"] }, { $divide: [{ $subtract: ["$$tLast", "$$tFirst"] }, 1000] }], + }, + 0, + ], + }, + }, + }, + deltaDropOut: { + $let: { + vars: { + first: { + $arrayElemAt: [ + { + $map: { + input: { $arrayElemAt: ["$net", 0] }, + as: "iface", + in: "$$iface.drop_out", + }, + }, + "$$netIndex", + ], + }, + last: { + $arrayElemAt: [ + { + $map: { + input: { + $arrayElemAt: [ + "$net", + { + $subtract: [{ $size: "$net" }, 1], + }, + ], + }, + as: "iface", + in: "$$iface.drop_out", + }, + }, + "$$netIndex", + ], + }, + tFirst: { $arrayElemAt: ["$updatedAts", 0] }, + tLast: { + $arrayElemAt: [ + "$updatedAts", + { + $subtract: [{ $size: "$updatedAts" }, 1], + }, + ], + }, + }, + in: { + $cond: [ + { $gt: [{ $subtract: ["$$tLast", "$$tFirst"] }, 0] }, + { + $divide: [{ $subtract: ["$$last", "$$first"] }, { $divide: [{ $subtract: ["$$tLast", "$$tFirst"] }, 1000] }], + }, + 0, + ], }, }, }, }, }, }, + firstUpdatedAt: 1, + lastUpdatedAt: 1, }, }, ],