diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..1172c71 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,5 @@ +.git +.gitignore +harborforge-monitor +bin +dist diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..6184884 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,20 @@ +FROM golang:1.22-alpine AS builder +WORKDIR /src +COPY go.mod go.sum ./ +RUN go mod download +COPY . . +RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o /out/harborforge-monitor ./cmd/harborforge-monitor + +FROM alpine:3.20 +RUN apk add --no-cache ca-certificates tzdata +WORKDIR /app +COPY --from=builder /out/harborforge-monitor /usr/local/bin/harborforge-monitor + +ENV HF_MONITER_BACKEND_URL=https://monitor.hangman-lab.top \ + HF_MONITER_IDENTIFIER= \ + HF_MONITER_API_KEY= \ + HF_MONITER_REPORT_INTERVAL=30 \ + HF_MONITER_LOG_LEVEL=info \ + HF_MONITER_ROOTFS=/host + +ENTRYPOINT ["/usr/local/bin/harborforge-monitor"] diff --git a/README.md b/README.md index 61287b7..8cc075e 100644 --- a/README.md +++ b/README.md @@ -29,8 +29,7 @@ HarborForge.Monitor/ ├── cmd/harborforge-monitor/ # 程序入口 ├── internal/config/ # 配置加载 ├── internal/telemetry/ # 指标采集与上报 -├── scripts/install.sh # 安装脚本 -├── systemd/harborforge-monitor.service +├── Dockerfile # 容器化运行 ├── config.example.json └── README.md ``` @@ -51,13 +50,16 @@ HarborForge.Monitor/ } ``` -也支持环境变量覆盖: +也支持环境变量覆盖。为了兼容你的命名,这里优先支持: -- `HF_MONITOR_BACKEND_URL` -- `HF_MONITOR_IDENTIFIER` -- `HF_MONITOR_API_KEY` -- `HF_MONITOR_REPORT_INTERVAL` -- `HF_MONITOR_LOG_LEVEL` +- `HF_MONITER_BACKEND_URL` +- `HF_MONITER_IDENTIFIER` +- `HF_MONITER_API_KEY` +- `HF_MONITER_REPORT_INTERVAL` +- `HF_MONITER_LOG_LEVEL` +- `HF_MONITER_ROOTFS` + +同时也兼容旧的/正确拼写的 `HF_MONITOR_*` 变量名。 ## 本地开发 @@ -67,22 +69,42 @@ go build ./cmd/harborforge-monitor ./harborforge-monitor -config ./config.example.json -dry-run -once ``` -## 安装 +## Docker 运行 + +构建镜像: ```bash -sudo ./scripts/install.sh +docker build -t harborforge-monitor . ``` -安装脚本会: +推荐以**宿主机 rootfs 只读挂载**方式运行,这样容器里采集到的是宿主机信息而不是容器自身: -- 构建二进制 `harborforge-monitor` -- 安装到 `/usr/local/bin/` -- 安装 systemd service -- 初始化 `/etc/harborforge-monitor/config.json` -- 自动启用并启动服务 +```bash +docker run -d \ + --name harborforge-monitor \ + --restart unless-stopped \ + -e HF_MONITER_BACKEND_URL=https://monitor.hangman-lab.top \ + -e HF_MONITER_IDENTIFIER=vps-nginx-01 \ + -e HF_MONITER_API_KEY=your-api-key \ + -e HF_MONITER_REPORT_INTERVAL=30 \ + -e HF_MONITER_ROOTFS=/host \ + -v /:/host:ro \ + harborforge-monitor +``` + +`Dockerfile` 里已经预置了这些环境变量: + +- `HF_MONITER_BACKEND_URL` +- `HF_MONITER_IDENTIFIER` +- `HF_MONITER_API_KEY` +- `HF_MONITER_REPORT_INTERVAL` +- `HF_MONITER_LOG_LEVEL` +- `HF_MONITER_ROOTFS` ## 注意 -- 当前 Nginx site 列表读取的是 `/etc/nginx/sites-enabled` +- Docker 模式下,建议挂载 `-v /:/host:ro` 并设置 `HF_MONITER_ROOTFS=/host` +- 这样 CPU/MEM/LOAD/UPTIME 会通过 host proc/sys 视角采集,磁盘和 nginx 配置也会走宿主机路径 +- 当前 Nginx site 列表读取的是 `${ROOTFS}/etc/nginx/sites-enabled` - 如果机器没有安装 Nginx,会回报 `nginx_installed = false` - 该客户端不会尝试读取 OpenClaw 信息,`agents` 默认为空,`openclaw_version` 不上报 diff --git a/config.example.json b/config.example.json index dbda9c7..49f1e22 100644 --- a/config.example.json +++ b/config.example.json @@ -3,5 +3,6 @@ "identifier": "vps-01", "apiKey": "replace-with-server-api-key", "reportIntervalSec": 30, - "logLevel": "info" + "logLevel": "info", + "rootFs": "/host" } diff --git a/internal/config/config.go b/internal/config/config.go index b67f2b6..ee3471c 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "os" + "path/filepath" ) type Config struct { @@ -12,15 +13,17 @@ type Config struct { APIKey string `json:"apiKey"` ReportIntervalSec int `json:"reportIntervalSec"` LogLevel string `json:"logLevel"` + RootFS string `json:"rootFs"` } func Load(path string) (Config, error) { cfg := Config{ - BackendURL: getenv("HF_MONITOR_BACKEND_URL", "https://monitor.hangman-lab.top"), - Identifier: getenv("HF_MONITOR_IDENTIFIER", hostnameOr("unknown-host")), - APIKey: os.Getenv("HF_MONITOR_API_KEY"), - ReportIntervalSec: getenvInt("HF_MONITOR_REPORT_INTERVAL", 30), - LogLevel: getenv("HF_MONITOR_LOG_LEVEL", "info"), + BackendURL: getenvAny([]string{"HF_MONITER_BACKEND_URL", "HF_MONITOR_BACKEND_URL"}, "https://monitor.hangman-lab.top"), + Identifier: getenvAny([]string{"HF_MONITER_IDENTIFIER", "HF_MONITOR_IDENTIFIER"}, hostnameOr("unknown-host")), + APIKey: getenvAny([]string{"HF_MONITER_API_KEY", "HF_MONITOR_API_KEY"}, ""), + ReportIntervalSec: getenvIntAny([]string{"HF_MONITER_REPORT_INTERVAL", "HF_MONITOR_REPORT_INTERVAL"}, 30), + LogLevel: getenvAny([]string{"HF_MONITER_LOG_LEVEL", "HF_MONITOR_LOG_LEVEL"}, "info"), + RootFS: getenvAny([]string{"HF_MONITER_ROOTFS", "HF_MONITOR_ROOTFS"}, ""), } if path != "" { @@ -32,13 +35,14 @@ func Load(path string) (Config, error) { } // env always wins over file - cfg.BackendURL = getenv("HF_MONITOR_BACKEND_URL", cfg.BackendURL) - cfg.Identifier = getenv("HF_MONITOR_IDENTIFIER", cfg.Identifier) - if v := os.Getenv("HF_MONITOR_API_KEY"); v != "" { + cfg.BackendURL = getenvAny([]string{"HF_MONITER_BACKEND_URL", "HF_MONITOR_BACKEND_URL"}, cfg.BackendURL) + cfg.Identifier = getenvAny([]string{"HF_MONITER_IDENTIFIER", "HF_MONITOR_IDENTIFIER"}, cfg.Identifier) + if v := getenvAny([]string{"HF_MONITER_API_KEY", "HF_MONITOR_API_KEY"}, ""); v != "" { cfg.APIKey = v } - cfg.ReportIntervalSec = getenvInt("HF_MONITOR_REPORT_INTERVAL", cfg.ReportIntervalSec) - cfg.LogLevel = getenv("HF_MONITOR_LOG_LEVEL", cfg.LogLevel) + cfg.ReportIntervalSec = getenvIntAny([]string{"HF_MONITER_REPORT_INTERVAL", "HF_MONITOR_REPORT_INTERVAL"}, cfg.ReportIntervalSec) + cfg.LogLevel = getenvAny([]string{"HF_MONITER_LOG_LEVEL", "HF_MONITOR_LOG_LEVEL"}, cfg.LogLevel) + cfg.RootFS = getenvAny([]string{"HF_MONITER_ROOTFS", "HF_MONITOR_ROOTFS"}, cfg.RootFS) if cfg.BackendURL == "" { return cfg, fmt.Errorf("backendUrl is required") @@ -49,6 +53,7 @@ func Load(path string) (Config, error) { if cfg.ReportIntervalSec <= 0 { cfg.ReportIntervalSec = 30 } + applyHostFSEnv(cfg.RootFS) return cfg, nil } @@ -80,20 +85,27 @@ func merge(dst *Config, src Config) { if src.LogLevel != "" { dst.LogLevel = src.LogLevel } + if src.RootFS != "" { + dst.RootFS = src.RootFS + } } -func getenv(key, fallback string) string { - if v := os.Getenv(key); v != "" { - return v +func getenvAny(keys []string, fallback string) string { + for _, key := range keys { + if v := os.Getenv(key); v != "" { + return v + } } return fallback } -func getenvInt(key string, fallback int) int { - if v := os.Getenv(key); v != "" { - var out int - if _, err := fmt.Sscanf(v, "%d", &out); err == nil && out > 0 { - return out +func getenvIntAny(keys []string, fallback int) int { + for _, key := range keys { + if v := os.Getenv(key); v != "" { + var out int + if _, err := fmt.Sscanf(v, "%d", &out); err == nil && out > 0 { + return out + } } } return fallback @@ -106,3 +118,20 @@ func hostnameOr(fallback string) string { } return name } + +func applyHostFSEnv(rootFS string) { + if rootFS == "" { + return + } + setIfEmpty("HOST_PROC", filepath.Join(rootFS, "proc")) + setIfEmpty("HOST_SYS", filepath.Join(rootFS, "sys")) + setIfEmpty("HOST_ETC", filepath.Join(rootFS, "etc")) + setIfEmpty("HOST_VAR", filepath.Join(rootFS, "var")) + setIfEmpty("HOST_RUN", filepath.Join(rootFS, "run")) +} + +func setIfEmpty(key, value string) { + if os.Getenv(key) == "" { + _ = os.Setenv(key, value) + } +} diff --git a/internal/telemetry/telemetry.go b/internal/telemetry/telemetry.go index 8cb7702..f9c2a87 100644 --- a/internal/telemetry/telemetry.go +++ b/internal/telemetry/telemetry.go @@ -55,7 +55,11 @@ func BuildPayload(ctx context.Context, cfg config.Config) (Payload, error) { payload.MemPct = round1(vm.UsedPercent) } - diskUsage, err := disk.UsageWithContext(ctx, "/") + diskPath := cfg.RootFS + if diskPath == "" { + diskPath = "/" + } + diskUsage, err := disk.UsageWithContext(ctx, diskPath) if err == nil { payload.DiskPct = round1(diskUsage.UsedPercent) } @@ -75,7 +79,7 @@ func BuildPayload(ctx context.Context, cfg config.Config) (Payload, error) { payload.UptimeSeconds = hostInfo.Uptime } - nginxInstalled, nginxSites, err := detectNginx() + nginxInstalled, nginxSites, err := detectNginx(cfg.RootFS) if err == nil { payload.NginxInstalled = nginxInstalled payload.NginxSites = nginxSites @@ -109,12 +113,20 @@ func Send(ctx context.Context, client *http.Client, cfg config.Config, payload P return nil } -func detectNginx() (bool, []string, error) { +func detectNginx(rootFS string) (bool, []string, error) { installed := false - if _, err := exec.LookPath("nginx"); err == nil { - installed = true + if rootFS == "" { + if _, err := exec.LookPath("nginx"); err == nil { + installed = true + } } - for _, path := range []string{"/etc/nginx/nginx.conf", "/usr/local/etc/nginx/nginx.conf", "/opt/homebrew/etc/nginx/nginx.conf"} { + for _, path := range []string{ + rootPath(rootFS, "/etc/nginx/nginx.conf"), + rootPath(rootFS, "/usr/local/etc/nginx/nginx.conf"), + rootPath(rootFS, "/opt/homebrew/etc/nginx/nginx.conf"), + rootPath(rootFS, "/usr/sbin/nginx"), + rootPath(rootFS, "/usr/bin/nginx"), + } { if _, err := os.Stat(path); err == nil { installed = true break @@ -124,7 +136,7 @@ func detectNginx() (bool, []string, error) { return false, []string{}, nil } - dir := "/etc/nginx/sites-enabled" + dir := rootPath(rootFS, "/etc/nginx/sites-enabled") entries, err := os.ReadDir(dir) if err != nil { if os.IsNotExist(err) { @@ -155,3 +167,10 @@ func round1(v float64) float64 { func round2(v float64) float64 { return float64(int(v*100+0.5)) / 100 } + +func rootPath(rootFS, path string) string { + if rootFS == "" || rootFS == "/" { + return path + } + return filepath.Join(rootFS, strings.TrimPrefix(path, "/")) +} diff --git a/scripts/install.sh b/scripts/install.sh deleted file mode 100755 index d57104b..0000000 --- a/scripts/install.sh +++ /dev/null @@ -1,28 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -BIN_NAME="harborforge-monitor" -INSTALL_DIR="/usr/local/bin" -CONFIG_DIR="/etc/harborforge-monitor" -SERVICE_PATH="/etc/systemd/system/harborforge-monitor.service" - -if [[ "${EUID}" -ne 0 ]]; then - echo "Please run as root (or via sudo)." >&2 - exit 1 -fi - -mkdir -p "${CONFIG_DIR}" - -pushd "${ROOT_DIR}" >/dev/null -go build -o "${BIN_NAME}" ./cmd/harborforge-monitor -install -m 0755 "${BIN_NAME}" "${INSTALL_DIR}/${BIN_NAME}" -install -m 0644 systemd/harborforge-monitor.service "${SERVICE_PATH}" -if [[ ! -f "${CONFIG_DIR}/config.json" ]]; then - install -m 0644 config.example.json "${CONFIG_DIR}/config.json" -fi -systemctl daemon-reload -systemctl enable --now harborforge-monitor -popd >/dev/null - -echo "Installed ${BIN_NAME}. Edit ${CONFIG_DIR}/config.json and restart the service if needed."