Enhancement: fallback for missing si network stats (#6367)

This commit is contained in:
shamoon 2026-02-27 10:39:02 -08:00 committed by GitHub
parent 1645c1b8a1
commit d529f81cb4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 97 additions and 3 deletions

View File

@ -7,13 +7,17 @@ You can include all or some of the available resources. If you do not want to se
The disk path is the path reported by `df` (Mounted On), or the mount point of the disk.
!!! note
Any disk you wish to access must be mounted to your container as a volume.
The cpu and memory resource information are the container's usage while [glances](glances.md) displays statistics for the host machine on which it is installed.
The resources widget primarily relies on a popular tool called [systeminformation](https://systeminformation.io). Thus, any limitiations of that software apply, for example, BRTFS RAID is not supported for the disk usage. In this case users may want to use the [glances widget](glances.md) instead.
_Note: unfortunately, the package used for getting CPU temp ([systeminformation](https://systeminformation.io)) is not compatible with some setups and will not report any value(s) for CPU temp._
!!! warning
**Any disk you wish to access must be mounted to your container as a volume.**
The package used for getting CPU temp ([systeminformation](https://systeminformation.io)) is not compatible with some setups and will not report any value(s) for CPU temp.
```yaml
- resources:
@ -75,3 +79,10 @@ You can additionally supply an optional `expanded` property set to true in order
```
![194136533-c4238c82-4d67-41a4-b3c8-18bf26d33ac2](https://user-images.githubusercontent.com/3441425/194728642-a9885274-922b-4027-acf5-a746f58fdfce.png)
To monitor a named host network interface in Docker (for example `network: eno1`), mount host `/sys` (read-only):
```yaml
volumes:
- /sys:/sys:ro
```

View File

@ -90,17 +90,74 @@ describe("pages/api/widgets/resources", () => {
});
it("returns 404 when requested network interface does not exist", async () => {
si.networkStats.mockResolvedValueOnce([{ iface: "en0" }]);
si.networkStats.mockResolvedValueOnce([{ iface: "en0" }]).mockResolvedValueOnce([
{
iface: "missing",
operstate: "unknown",
rx_bytes: 0,
rx_dropped: 0,
rx_errors: 0,
tx_bytes: 0,
tx_dropped: 0,
tx_errors: 0,
rx_sec: null,
tx_sec: null,
ms: 0,
},
]);
const req = { query: { type: "network", interfaceName: "missing" } };
const res = createMockRes();
await handler(req, res);
expect(si.networkStats).toHaveBeenNthCalledWith(1, "*");
expect(si.networkStats).toHaveBeenNthCalledWith(2, "missing");
expect(res.statusCode).toBe(404);
expect(res.body).toEqual({ error: "Interface not found" });
});
it("falls back to direct named interface query when wildcard enumeration misses it", async () => {
si.networkStats.mockResolvedValueOnce([{ iface: "eth0", rx_bytes: 1 }]).mockResolvedValueOnce([
{
iface: "eno1",
operstate: "up",
rx_bytes: 1000,
rx_dropped: 0,
rx_errors: 0,
tx_bytes: 500,
tx_dropped: 0,
tx_errors: 0,
rx_sec: null,
tx_sec: null,
ms: 0,
},
]);
const req = { query: { type: "network", interfaceName: "eno1" } };
const res = createMockRes();
await handler(req, res);
expect(si.networkStats).toHaveBeenNthCalledWith(1, "*");
expect(si.networkStats).toHaveBeenNthCalledWith(2, "eno1");
expect(res.statusCode).toBe(200);
expect(res.body.interface).toBe("eno1");
expect(res.body.network).toEqual({
iface: "eno1",
operstate: "up",
rx_bytes: 1000,
rx_dropped: 0,
rx_errors: 0,
tx_bytes: 500,
tx_dropped: 0,
tx_errors: 0,
rx_sec: null,
tx_sec: null,
ms: 0,
});
});
it("returns default interface network stats", async () => {
si.networkStats.mockResolvedValueOnce([{ iface: "en0", rx_bytes: 1 }]);
si.networkInterfaceDefault.mockResolvedValueOnce("en0");

View File

@ -4,6 +4,21 @@ import createLogger from "utils/logger";
const logger = createLogger("resources");
function isMissingNetworkStat(networkData, interfaceName) {
return (
networkData.operstate === "unknown" &&
networkData.rx_bytes === 0 &&
networkData.rx_dropped === 0 &&
networkData.rx_errors === 0 &&
networkData.tx_bytes === 0 &&
networkData.tx_dropped === 0 &&
networkData.tx_errors === 0 &&
networkData.rx_sec === null &&
networkData.tx_sec === null &&
networkData.ms === 0
);
}
export default async function handler(req, res) {
const { type, target, interfaceName = "default" } = req.query;
@ -64,6 +79,17 @@ export default async function handler(req, res) {
logger.debug("networkData:", JSON.stringify(networkData));
if (interfaceName && interfaceName !== "default") {
networkData = networkData.filter((network) => network.iface === interfaceName).at(0);
if (!networkData) {
// Fallback for e.g. docker where networkStats("*") may not return stats for host interfaces
const directNetworkData = await si.networkStats(interfaceName);
logger.debug("directNetworkData:", JSON.stringify(directNetworkData));
networkData = Array.isArray(directNetworkData) ? directNetworkData.at(0) : null;
// si returns unknown + zeroes when interface truly does not exist
if (!networkData || isMissingNetworkStat(networkData, interfaceName)) {
networkData = null;
}
}
if (!networkData) {
return res.status(404).json({
error: "Interface not found",