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