diff --git a/docs/widgets/info/resources.md b/docs/widgets/info/resources.md index 7fcf9c5cd..08ab95abf 100644 --- a/docs/widgets/info/resources.md +++ b/docs/widgets/info/resources.md @@ -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 +``` diff --git a/src/__tests__/pages/api/widgets/resources.test.js b/src/__tests__/pages/api/widgets/resources.test.js index 3ff5151ca..74506d0a4 100644 --- a/src/__tests__/pages/api/widgets/resources.test.js +++ b/src/__tests__/pages/api/widgets/resources.test.js @@ -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"); diff --git a/src/pages/api/widgets/resources.js b/src/pages/api/widgets/resources.js index 19db010f5..812d3e7c6 100644 --- a/src/pages/api/widgets/resources.js +++ b/src/pages/api/widgets/resources.js @@ -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",