Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
721cec18e7 | ||
|
|
09afd4397c | ||
|
|
90478e5408 | ||
|
|
0b0e18de07 | ||
|
|
84d9a270dd | ||
|
|
f5e668d545 | ||
|
|
c947cbe934 | ||
|
|
93d5f0cdd7 | ||
|
|
d6df234154 | ||
|
|
d856b42e92 | ||
|
|
d3f79ebe8a | ||
|
|
f576114db1 | ||
|
|
6a66a48a51 | ||
|
|
20c781c750 | ||
|
|
d206ae4390 |
@@ -2,3 +2,4 @@ __pycache__/
|
||||
*.pyc
|
||||
dist/
|
||||
.DS_Store
|
||||
.data/
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
# 树莓派接入插件 v1.2.0
|
||||
|
||||
2026-10-14 已部署。主网络卡片主文字改为“有线网络 · 链路速率”;接口/IP/状态与上下行速率独立两行,与群晖 v1.4.0 一致。Wi-Fi 信号与告警语义保留。Agent v3.8.0 提供 rx_bytes_per_second/tx_bytes_per_second,映射到标准历史指标;旧 Agent 缺少数值时不伪造速率,非数值、布尔和非有限值不写入历史。两项指标回归测试通过,核心全页三尺寸、中英文主题浏览器回归通过;真实生产快照额外验证两处网络卡片三尺寸与两语言,桌面截图已查看。
|
||||
|
||||
生产实际插件 1.2.0、Agent 3.8.0,两设备在线,树莓派上下行采集与标准历史指标通过认证 API 核验,临时会话删除。停服一致性备份 `/volume1/homes/way/pigway-console-backup-network-align-20261014/data.tar.gz`,目录 0700;旧插件在数据卷 adapter-backups/network-align-20261014。Console 镜像保持原版。
|
||||
@@ -0,0 +1,5 @@
|
||||
# 网络卡片 v1.2.1
|
||||
|
||||
2026-10-14 已部署。网络主文字改为下载/上传速率,删除 ETH/有线网络文字;下方保留 eth0、1000 Mbps、IP 和状态,不重复显示速率。两处网络卡片使用一致布局。模拟及真实设备快照三尺寸、中英文浏览器检查通过,无溢出,桌面截图已查看。生产认证 API 确认两设备在线、树莓派实时流量与历史标准指标有效。Agent 保持 v3.8.0,未再次升级。
|
||||
|
||||
停服一致性数据备份 /volume1/homes/way/pigway-console-backup-network-rates-layout-20261014/data.tar.gz,目录 0700;旧插件在数据卷 adapter-backups/network-rates-layout-20261014。Console 镜像及 NAS 每日存储采集保持。
|
||||
@@ -0,0 +1,5 @@
|
||||
# 树莓派接入插件 v1.2.2
|
||||
|
||||
2026-10-14 网络卡片改为按 network.link 显示已连接/已断开,字段缺失显示状态未确认,与 NAS 的接口连接语义一致。页面不再把 network.internet 用作接口状态文案。Agent 的既有互联网探测和告警逻辑不修改。两项后端测试及全页三尺寸中英文主题浏览器验证通过。部署使用停服一致性数据备份 /volume1/homes/way/pigway-console-backup-synology-vpi-link-v1.2.2-20261014/data.tar.gz,目录 0700;旧树莓派插件保存在数据卷 adapter-backups/vpi-link-v1.2.2/pigway-agent。
|
||||
|
||||
生产安装并启用 1.2.2,实际 UI 已使用 network.link 且不存在“互联网正常”文案;认证 API 两设备在线,实时流量和历史指标保留。验证会话删除。Tea ZIP 与 SHA256 附件下载校验一致。
|
||||
@@ -0,0 +1,5 @@
|
||||
# pigway-console-adapter-raspberry-pi v1.3.0
|
||||
|
||||
2026-10-15 已与 Console v2.4.0 部署。声明每秒缓存读取并透传 Agent 的 system/network 真实采样时间。树莓派网络仍每 5 秒采样,历史去重验证通过。
|
||||
|
||||
生产两设备在线。认证 API 连续观测确认树莓派缓存每秒、网络 5 秒、NAS 实时约 10 秒;最近三个网络 10 秒历史桶各 2 个样本,没有每秒重复入库。Console 保留原名称、证书、存储缓存和一年历史策略。部署详情见 Console [生产记录](https://tea.pigway.com/way/pigway-device-console/src/branch/main/docs/DEPLOYMENT-v2.4.0-2026-10-15.md)。
|
||||
@@ -0,0 +1,7 @@
|
||||
# pigway-console-adapter-raspberry-pi v1.3.2 实测调优
|
||||
|
||||
2026-10-15 生产调优结论:统一 4 秒读取 CPU、内存、温度及网络速率。曾试 2 秒,但这台 DSM 的 utilization 实际采样时间通常每 4 秒更新,连续细粒度观察 CPU 时间为 7853/7857/7861/7865 等相差 4 秒的样本;不能把两次相同样本当成更快采样。只读接口测量 System.info 约 0.05–0.07 秒、Utilization.get 约 1.03–1.09 秒,请求耗时与真实样本周期不同。
|
||||
|
||||
最终 40 秒连续观测:两设备各 10 个独立实时样本、10 个网络样本,中位间隔均 4 秒;无离线或缺失采集组,缓存 API 最大耗时 0.032 秒(这是 Console 缓存读取,不是 DSM 接口耗时)。Pi 本地 CPU 等继续 1 秒,网络配置已从 5 秒改为 2 秒;Console 每 4 秒读取,覆盖更快缓存。NAS 网络配置 60 秒、存储 24 小时保持。
|
||||
|
||||
插件生产版本 Pi 1.3.2、NAS 1.6.1,Console 仍 2.4.0、Agent 仍 3.9.0。3 项 Pi 回归和 14 项 NAS 回归通过。停服一致性数据备份 /volume1/homes/way/pigway-console-backup-balanced-4s-20261015/data.tar.gz,目录 0700;旧插件在数据卷 adapter-backups/balanced-4s-20261015。Pi 原配置备份 /home/way/pigway-timing-backup-20261015.conf,权限 0600。仅保留 Console 原 latest 镜像,没有新增镜像。
|
||||
@@ -0,0 +1,7 @@
|
||||
# v1.3.3 指标语义统一
|
||||
|
||||
2026-10-15 已部署。运行状态与历史趋势统一处理器负载、内存占用、磁盘占用及英文名称;主网络英文统一 Primary network,无告警与状态未确认提示一致。缺失百分比显示 --,不显示孤立百分号或假零值进度条。温度、内存可用量/存储剩余量按数据来源保留,不改用户资源名称。
|
||||
|
||||
全页、历史五范围和三尺寸中英文主题浏览器验证通过;真实设备公开快照在本地渲染两处卡片,三尺寸两语言名称检查和缺失读数检查通过。生产认证 API 核对 Console 2.4.1、Pi 插件 1.3.3、NAS 插件 1.6.2,两设备在线、原名称保持,4 秒声明不变,验证会话删除。生产实际 UI 和历史名称代码核对通过。
|
||||
|
||||
停服一致性备份 /volume1/homes/way/pigway-console-backup-v2.4.1-20261015/{program,data}.tar.gz,目录 0700;旧插件在数据卷 adapter-backups/semantics-v2.4.1。实际镜像 sha256:127d80530abc728add1eb947e304387f7f941f8fa570386d951c6f9ae6eabc0e,仅保留 latest 标签,候选标签与旧镜像清理。没有主动刷新磁盘。Tea 三个 Release 附件下载校验一致。
|
||||
@@ -1,6 +1,6 @@
|
||||
# PIGWay Agent Console Plugin
|
||||
# PIGWay Raspberry Pi Console Plugin
|
||||
|
||||
独立维护的树莓派接入插件:连接现有 PIGWay Device Agent,不在树莓派安装新进程。设备 API、状态转换、配置字段、服务/进程监控、硬件界面及其翻译均由本仓库维护。
|
||||
独立维护的树莓派接入插件:连接现有 PIGWay Raspberry Pi Agent,不在树莓派安装新进程。设备 API、状态转换、配置字段、服务/进程监控、硬件界面及其翻译均由本仓库维护。
|
||||
|
||||
依赖 Python 标准库;接入 Console JSON-RPC v1 与可信前端扩展 `console-ui-v1`。插件 ID 保持 `pigway-agent`,已有设备可直接继续使用。
|
||||
|
||||
@@ -8,11 +8,33 @@
|
||||
|
||||
```sh
|
||||
mkdir -p dist
|
||||
python3 package.py plugin dist/pigway-agent-1.0.0.zip
|
||||
python3 package.py plugin dist/pigway-agent-1.1.0.zip
|
||||
```
|
||||
|
||||
在控制中心的机器管理页面安装 ZIP,再启用。开发时可用 `PIGWAY_ADAPTER_PATH=/absolute/path/pigway-console-adapter-agent/plugin` 加载源码;Docker 推荐使用持久化数据卷中的安装包。
|
||||
在控制中心的机器管理页面安装 ZIP,再启用。开发时可用 `PIGWAY_ADAPTER_PATH=/absolute/path/pigway-console-adapter-raspberry-pi/plugin` 加载源码;Docker 推荐使用持久化数据卷中的安装包。
|
||||
|
||||
包根目录只有 manifest、worker 与 ui;打包工具、文档和 Git 元数据不进入插件包。前端扩展通过 Console 的共享状态、请求、翻译与基础显示函数接入,不注入额外安装步骤。只应安装可信插件:前端模块与主页面共享 JavaScript 执行环境,加载失败、同步异常及 Promise 拒绝可降级,但不能隔离死循环或恶意代码;后端独立进程的隔离也不是安全沙箱。
|
||||
|
||||
正式仓库:https://tea.pigway.com/way/pigway-console-adapter-agent 。首次独立版本为 v1.0.0,需要 Console v2.0.0。与 Console 的集成回归使用后者 tests 下的模拟 Agent,不操作真实设备。
|
||||
正式仓库:https://tea.pigway.com/way/pigway-console-adapter-raspberry-pi 。首次独立版本为 v1.0.0,需要 Console v2.0.0。与 Console 的集成回归使用后者 tests 下的模拟 Agent,不操作真实设备。
|
||||
|
||||
## Naming and compatibility
|
||||
|
||||
This project targets Raspberry Pi. Other Linux platforms are not covered by its support claim and may have separately maintained adapters. Repository naming does not change the existing `pigway-agent` adapter ID or `pigway-pi-control` service, configuration and API identifiers.
|
||||
|
||||
## v1.1.0 历史指标
|
||||
|
||||
将现有 Agent API 的内存/磁盘字节数、开机时长与链路速度补充到标准 metrics,Console v2.1.0 可直接统一记录历史。不需要更新或新增树莓派进程。现有设备端没有上传下载流量计数,因此本版没有实际流量曲线;链路速度仅代表协商速率。
|
||||
|
||||
v1.1.0 已与 Console v2.1.0 部署,真实树莓派在线并开始记录历史;设备端 Agent 保持原版本。[部署验收记录](https://tea.pigway.com/way/pigway-device-console/src/branch/main/docs/DEPLOYMENT-v2.1.0-2026-10-11.md)。
|
||||
|
||||
生产已升级至 v1.2.0,网络卡片与上下行历史指标见 [部署记录](DEPLOYMENT-v1.2.0-2026-10-14.md)。
|
||||
|
||||
当前网络卡片以实时上下行速率为主文字,详见 [v1.2.1 部署记录](DEPLOYMENT-v1.2.1-2026-10-14.md)。
|
||||
|
||||
网络连接文案统一为接口链路状态,见 [v1.2.2](DEPLOYMENT-v1.2.2-2026-10-14.md)。
|
||||
|
||||
生产 v1.3.0 与按设备周期采集的核验见 [部署记录](DEPLOYMENT-v1.3.0-2026-10-15.md)。
|
||||
|
||||
当前生产实时指标统一 4 秒读取,实测及版本 v1.3.2 见 [调优记录](DEPLOYMENT-v1.3.2-2026-10-15.md)。
|
||||
|
||||
当前生产 v1.3.3 的指标用词、状态与缺失百分比统一见 [部署记录](DEPLOYMENT-v1.3.3-2026-10-15.md)。
|
||||
|
||||
+5
-4
@@ -1,7 +1,7 @@
|
||||
7d856a7ec552a841097cd14f2f3e357efcd05c13f70eb98a415789e615f6fbf2 .gitignore
|
||||
f577e72ab33e2cc2dcb2fde5f598adab9dd0ae0602cef517c8281eb39bf00eb6 README.md
|
||||
3cfd361f7663a9a9c93b507d17bb1c7e98003d61316826f0a4ba4a93594f72d4 .gitignore
|
||||
1f960195978886b318953a6dbc7f13a551d9bb2bab44aa88b1599c69c8a9ced5 README.md
|
||||
d081c599d821149853368b1a8637c57a16995344406ff5ec9d7619b66e0479b3 package.py
|
||||
b7c560b9f529ef8dcdf74f1bc74a36745fc005c2432ff80325e1c9e87573e7b2 plugin/manifest.json
|
||||
cf40d1302a8398497e4d6724ec0a22b24420e32ed2697ba3f4d039d193cf38ea plugin/manifest.json
|
||||
e5c04001dbd8ed8bba614f7b341237fc6487a9a8fe2f28ccb6f59278725b0a69 plugin/ui/config.js
|
||||
c98f34119436e3020ef20b325f91617943aefe662119cf76738e0226c2e91cbb plugin/ui/device.js
|
||||
89b9d7994e7477e18c3f43e6f0975e04d5106056ba581b2964db0f39183d6f8a plugin/ui/hardware.js
|
||||
@@ -11,4 +11,5 @@ fadb6bf24d6fcf569fc4157847a3585a1008aad99ce5a0e2b87c9c0658f6d225 plugin/ui/over
|
||||
5d967b5d4e62eed7022bc04fe29e4ed670f2e288cf20adae0c25233f94a29079 plugin/ui/services.js
|
||||
56e67d2839cbb865133e3b5f796a07f36595c0f6c30480f8e2fac79e7d51e03f plugin/ui/styles.css
|
||||
0b79b9c5b5f23727164e05b127caca72255416309166ba200ccbd15a9667473c plugin/ui/translations.js
|
||||
c12c7b8a3c2fdb5eaef8ca535fcae04d960d4c89c86740529166f3e25cd8c7c9 plugin/worker.py
|
||||
0c37e4604a30c13923b45316d21c841164c48e8b632c3a3f8febd778b434c2f8 plugin/worker.py
|
||||
9ffd557bb01ccda24970af1cfc5ec395721d5b1e613bfc013feb27d074582b7e tests/test_metrics.py
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "pigway-agent",
|
||||
"name": "PIGWay Agent",
|
||||
"version": "1.0.0",
|
||||
"name": "PIGWay Raspberry Pi Agent",
|
||||
"version": "1.4.0",
|
||||
"protocol_version": 1,
|
||||
"entrypoint": "worker.py",
|
||||
"runtime": "python3",
|
||||
@@ -38,5 +38,8 @@
|
||||
"contract": "console-ui-v1",
|
||||
"entrypoint": "ui/index.js"
|
||||
},
|
||||
"legacy_connection_fields": true
|
||||
"legacy_connection_fields": true,
|
||||
"collection": {
|
||||
"interval_seconds": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,5 +20,7 @@ export function agentHostCard(entry) {
|
||||
memTotal = y.memory_total_bytes,
|
||||
diskUsed = y.disk_used_bytes,
|
||||
diskTotal = y.disk_total_bytes;
|
||||
return `<article class="panel host-panel"><div class="host-head"><div><div class="host-title"><h2>${esc(m.name || h.name || m.id)}</h2><span class="uptime-badge">${esc(formatUptime(s.uptime_seconds))}</span></div><div class="device-details"><span>ID ${esc(m.id || h.id)}</span><span>${appState.LANG === "zh" ? "主机名" : "Hostname"} ${esc(m.hostname || v.hostname || "--")}</span><span>${esc(v.manufacturer || "")} ${esc(v.model || "--")}</span><span>${appState.LANG === "zh" ? "序列号" : "Serial"} ${esc(v.serial || "--")}</span><span>${appState.LANG === "zh" ? "硬件版本" : "Revision"} ${esc(v.revision || "--")}</span><span>${esc(v.os || "--")}</span><span>${appState.LANG === "zh" ? "内核" : "Kernel"} ${esc(v.kernel || "--")}</span><span>${esc(v.architecture || "--")} · CPU ${esc(v.cpu_count || "--")}</span><span>${appState.LANG === "zh" ? "程序版本" : "Agent"} ${esc(s.version || "--")}</span></div></div><span class="badge good">${tr("在线")}</span></div><div class="host-grid"><div class="metric"><div class="muted">${tr("处理器负载")}</div><div class="value">${esc(y.cpu_percent)}%</div>${meter(y.cpu_percent)}<div class="muted">${appState.LANG === "zh" ? "频率" : "Frequency"} ${esc(y.cpu_freq_mhz)} MHz · ${esc(y.temperature_c)}°C${fanMetric(s.plugins || {})}</div></div><div class="metric"><div class="muted">${tr("内存占用")}</div><div class="value">${esc(y.memory_percent)}%</div>${meter(y.memory_percent)}<div class="muted">${bytes(memUsed)} / ${bytes(memTotal)} · ${appState.LANG === "zh" ? "可用" : "Available"} ${bytes(y.memory_available_bytes)}</div></div><div class="metric"><div class="muted">${tr("磁盘占用")}</div><div class="value">${esc(y.disk_percent)}%</div>${meter(y.disk_percent)}<div class="muted">${bytes(diskUsed)} / ${bytes(diskTotal)} · ${appState.LANG === "zh" ? "剩余" : "Free"} ${bytes(y.disk_free_bytes)}</div></div><div class="metric"><div class="muted">${tr("主网络")}</div><div class="value">${n.kind === "WIF" && signal !== null && signal !== undefined ? `${esc(signal)}%` : esc(n.kind || "--")}</div>${n.kind === "WIF" ? meter(signal) : ""}<div class="muted">${esc(n.interface || "--")} · ${n.kind === "WIF" ? `${esc(n.metric)} dBm · ` : n.kind === "ETH" && n.link_speed_mbps ? `${esc(n.link_speed_mbps)} Mbps · ` : ""}${esc(n.ip_label)} ${esc(n.ip)} · ${n.internet ? (appState.LANG === "zh" ? "互联网正常" : "Internet online") : appState.LANG === "zh" ? "互联网断开" : "Internet down"}</div></div><div class="metric"><div class="muted">${tr("显示状态")}</div><div class="value">${esc(ownerName(d.owner))}</div><div class="muted">${appState.LANG === "zh" ? "告警状态独立于硬件插件" : "Alert state is independent of hardware plugins"}</div></div><div class="metric"><div class="muted">${tr("当前告警")}</div><div class="value">${(d.alerts || []).length}</div><div class="muted">${(d.alerts || []).map((a) => esc(ownerName(a.id))).join(" · ") || tr("无告警")}</div></div></div>${pluginSummary(s.plugins || {}, h.id)}<div class="group"><h3>${tr("服务与进程监控")}</h3><div class="rows">${(s.watches || []).map((w) => `<div class="row"><span>${esc(w.name)} · ${esc(w.target)}</span><span class="badge ${w.up ? "good" : "bad"}">${tr(w.up ? "正常" : "停止")}</span></div>`).join("") || `<div class="muted">${tr("暂无数据")}</div>`}</div></div></article>`;
|
||||
const percent = n => Number.isFinite(n) ? `${n.toFixed(1)}%` : "--";
|
||||
const progress = n => Number.isFinite(n) ? meter(n) : "";
|
||||
return `<article class="panel host-panel"><div class="host-head"><div><div class="host-title"><h2>${esc(m.name || h.name || m.id)}</h2><span class="uptime-badge">${esc(formatUptime(s.uptime_seconds))}</span></div><div class="device-details"><span>ID ${esc(m.id || h.id)}</span><span>${appState.LANG === "zh" ? "主机名" : "Hostname"} ${esc(m.hostname || v.hostname || "--")}</span><span>${esc(v.manufacturer || "")} ${esc(v.model || "--")}</span><span>${appState.LANG === "zh" ? "序列号" : "Serial"} ${esc(v.serial || "--")}</span><span>${appState.LANG === "zh" ? "硬件版本" : "Revision"} ${esc(v.revision || "--")}</span><span>${esc(v.os || "--")}</span><span>${appState.LANG === "zh" ? "内核" : "Kernel"} ${esc(v.kernel || "--")}</span><span>${esc(v.architecture || "--")} · CPU ${esc(v.cpu_count || "--")}</span><span>${appState.LANG === "zh" ? "程序版本" : "Agent"} ${esc(s.version || "--")}</span></div></div><span class="badge good">${tr("在线")}</span></div><div class="host-grid"><div class="metric" data-refresh-group="system"><div class="muted">${tr("处理器负载")}</div><div class="value">${percent(y.cpu_percent)}</div>${progress(y.cpu_percent)}<div class="muted">${appState.LANG === "zh" ? "频率" : "Frequency"} ${esc(y.cpu_freq_mhz)} MHz · ${esc(y.temperature_c)}°C${fanMetric(s.plugins || {})}</div></div><div class="metric" data-refresh-group="system"><div class="muted">${tr("内存占用")}</div><div class="value">${percent(y.memory_percent)}</div>${progress(y.memory_percent)}<div class="muted">${bytes(memUsed)} / ${bytes(memTotal)} · ${appState.LANG === "zh" ? "可用" : "Available"} ${bytes(y.memory_available_bytes)}</div></div><div class="metric" data-refresh-group="storage"><div class="muted">${tr("磁盘占用")}</div><div class="value">${percent(y.disk_percent)}</div>${progress(y.disk_percent)}<div class="muted">${bytes(diskUsed)} / ${bytes(diskTotal)} · ${appState.LANG === "zh" ? "剩余" : "Free"} ${bytes(y.disk_free_bytes)}</div></div><div class="metric" data-refresh-group="network"><div class="muted">${tr("主网络")}</div><div class="value">↓ ${bytes(n.rx_bytes_per_second)}/s · ↑ ${bytes(n.tx_bytes_per_second)}/s</div><div class="muted">${esc(n.interface || "--")} · ${n.kind === "WIF" ? `${esc(n.metric)} dBm · ` : n.link_speed_mbps ? `${esc(n.link_speed_mbps)} Mbps · ` : ""}${esc(n.ip_label)} ${esc(n.ip)} · ${n.link === true ? (appState.LANG === "zh" ? "已连接" : "Connected") : n.link === false ? (appState.LANG === "zh" ? "已断开" : "Disconnected") : (appState.LANG === "zh" ? "状态未确认" : "Unconfirmed")}</div></div><div class="metric" data-refresh-group="display"><div class="muted">${tr("显示状态")}</div><div class="value">${esc(ownerName(d.owner))}</div><div class="muted">${appState.LANG === "zh" ? "告警状态独立于硬件插件" : "Alert state is independent of hardware plugins"}</div></div><div class="metric" data-refresh-group="alerts"><div class="muted">${tr("当前告警")}</div><div class="value">${(d.alerts || []).length}</div><div class="muted">${(d.alerts || []).map((a) => esc(ownerName(a.id))).join(" · ") || tr("无告警")}</div></div></div>${pluginSummary(s.plugins || {}, h.id)}<div class="group"><h3>${tr("服务与进程监控")}</h3><div class="rows">${(s.watches || []).map((w) => `<div class="row"><span>${esc(w.name)} · ${esc(w.target)}</span><span class="badge ${w.up ? "good" : "bad"}">${tr(w.up ? "正常" : "停止")}</span></div>`).join("") || `<div class="muted">${tr("暂无数据")}</div>`}</div></div></article>`;
|
||||
}
|
||||
|
||||
+34
-3
@@ -1,7 +1,8 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Official Agent adapter. This process alone knows Agent endpoints/authentication."""
|
||||
"""Official Raspberry Pi adapter. This process alone knows Agent endpoints/authentication."""
|
||||
import concurrent.futures
|
||||
import json
|
||||
import math
|
||||
import sys
|
||||
import threading
|
||||
from pathlib import Path
|
||||
@@ -55,24 +56,54 @@ def request(device, endpoint, method='GET', payload=None, query=None):
|
||||
raise AdapterError('RESULT_UNKNOWN' if method == 'PUT' else 'DEVICE_UNAVAILABLE') from None
|
||||
|
||||
|
||||
def collection_interval(value):
|
||||
declaration = value.get('collection', {})
|
||||
interval = declaration.get('interval_seconds') if isinstance(declaration, dict) else None
|
||||
if not isinstance(interval, (int, float)) or isinstance(interval, bool) or not math.isfinite(interval) or not 1 <= interval <= 3600:
|
||||
interval = MANIFEST['collection']['interval_seconds']
|
||||
return max(MANIFEST['collection']['interval_seconds'], interval)
|
||||
|
||||
|
||||
def collect(device):
|
||||
value = request(device, '/status')
|
||||
if not isinstance(value.get('system'), dict) or not isinstance(value.get('machine'), dict):
|
||||
raise AdapterError('INVALID_RESPONSE')
|
||||
declaration = value.get('collection') if isinstance(value.get('collection'),dict) else {}
|
||||
plans = declaration.get('refresh') if isinstance(declaration.get('refresh'),dict) else {}
|
||||
storage_plan = plans.get('storage') if isinstance(plans.get('storage'),dict) else {}
|
||||
metrics = []
|
||||
units = {'cpu_percent': 'percent', 'memory_percent': 'percent', 'disk_percent': 'percent',
|
||||
'temperature_c': 'celsius', 'cpu_freq_mhz': 'megahertz'}
|
||||
'temperature_c': 'celsius', 'cpu_freq_mhz': 'megahertz',
|
||||
'memory_total_bytes': 'bytes', 'memory_used_bytes': 'bytes',
|
||||
'disk_total_bytes': 'bytes', 'disk_used_bytes': 'bytes'}
|
||||
for key, unit in units.items():
|
||||
number = value['system'].get(key)
|
||||
if isinstance(number, (int, float)) and not isinstance(number, bool):
|
||||
metrics.append({'resource_id': 'host', 'key': key, 'value': number, 'unit': unit})
|
||||
uptime = value.get('uptime_seconds')
|
||||
if isinstance(uptime, (int, float)) and not isinstance(uptime, bool):
|
||||
metrics.append({'resource_id': 'host', 'key': 'uptime_seconds', 'value': uptime, 'unit': 'seconds'})
|
||||
speed = value.get('network', {}).get('link_speed_mbps')
|
||||
if isinstance(speed, (int, float)) and not isinstance(speed, bool):
|
||||
metrics.append({'resource_id': 'host', 'key': 'network_link_speed_mbps', 'value': speed, 'unit': 'megabits_per_second'})
|
||||
for field, key in (('rx_bytes_per_second', 'network_rx_bytes_per_second'),
|
||||
('tx_bytes_per_second', 'network_tx_bytes_per_second')):
|
||||
rate = value.get('network', {}).get(field)
|
||||
if isinstance(rate, (int, float)) and not isinstance(rate, bool) and math.isfinite(rate) and rate >= 0:
|
||||
metrics.append({'resource_id': 'host', 'key': key, 'value': rate, 'unit': 'bytes_per_second'})
|
||||
for metric in metrics:
|
||||
source = value.get('network', {}) if metric['key'].startswith('network_') else value.get('system', {})
|
||||
at = source.get('storage_observed_at') if metric['key'].startswith('disk_') else source.get('observed_at') if metric['key'] != 'uptime_seconds' else value.get('timestamp')
|
||||
if isinstance(at, (int, float)) and not isinstance(at, bool) and math.isfinite(at) and at > 0:
|
||||
metric['observed_at'] = at
|
||||
return {'schema_version': 1, 'observed_at': value.get('timestamp'),
|
||||
'identity': {'reported_name': value['machine'].get('name'), 'category': 'host',
|
||||
'details': value.get('device', {})},
|
||||
'collection': {**declaration, 'interval_seconds': collection_interval(value)},
|
||||
'capabilities': MANIFEST['capabilities'], 'metrics': metrics,
|
||||
'resources': [{'id': 'host', 'type': 'host'}],
|
||||
'alerts': value.get('display', {}).get('alerts', []),
|
||||
'alerts_complete': isinstance(value.get('display', {}).get('alerts'), list),
|
||||
'alerts_complete': isinstance(value.get('display', {}).get('alerts'), list) and not storage_plan.get('error',False),
|
||||
'view': value}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import sys
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
sys.path.insert(0,str(Path(__file__).resolve().parents[1]/'plugin'))
|
||||
import worker
|
||||
|
||||
|
||||
class MetricsTests(unittest.TestCase):
|
||||
def test_existing_agent_fields_become_standard_history_metrics(self):
|
||||
value = {'timestamp': 123, 'machine': {'name':'Pi'}, 'system': {
|
||||
'cpu_percent': 10, 'memory_total_bytes': 8000, 'memory_used_bytes': 4000,
|
||||
'disk_total_bytes': 16000, 'disk_used_bytes': 5000},
|
||||
'uptime_seconds': 1000, 'network': {'observed_at':120,'link_speed_mbps':1000,'rx_bytes_per_second':0,'tx_bytes_per_second':1234},
|
||||
'display': {'alerts': []}}
|
||||
with patch.object(worker,'request',return_value=value): result=worker.collect({})
|
||||
values = {m['key']:m['value'] for m in result['metrics']}
|
||||
self.assertEqual(values['memory_used_bytes'],4000)
|
||||
self.assertEqual(values['uptime_seconds'],1000)
|
||||
self.assertEqual(values['network_link_speed_mbps'],1000)
|
||||
self.assertEqual(values['network_rx_bytes_per_second'],0)
|
||||
self.assertEqual(values['network_tx_bytes_per_second'],1234)
|
||||
self.assertEqual(next(m['observed_at'] for m in result['metrics'] if m['key']=='network_tx_bytes_per_second'),120)
|
||||
self.assertEqual(result['collection']['interval_seconds'],1)
|
||||
self.assertTrue(result['alerts_complete'])
|
||||
|
||||
def test_collection_minimum_and_slower_device(self):
|
||||
self.assertEqual(worker.collection_interval({'collection':{'interval_seconds':1}}),1)
|
||||
self.assertEqual(worker.collection_interval({'collection':{'interval_seconds':5}}),5)
|
||||
self.assertEqual(worker.collection_interval({'collection':{'interval_seconds':True}}),1)
|
||||
|
||||
def test_absent_or_boolean_fields_not_invented(self):
|
||||
value={'system': {'cpu_percent': True}, 'machine':{}, 'network': {'link_speed_mbps': None, 'rx_bytes_per_second':float('nan'), 'tx_bytes_per_second':True}}
|
||||
with patch.object(worker,'request',return_value=value): result=worker.collect({})
|
||||
self.assertEqual(result['metrics'],[])
|
||||
|
||||
if __name__=='__main__': unittest.main()
|
||||
Reference in New Issue
Block a user