feat: switch monitor client to Docker-first runtime
- remove install.sh-based deployment path - add multi-stage Dockerfile for HarborForge.Monitor - support HF_MONITER_* env vars and keep HF_MONITOR_* compatibility - add rootfs-aware host metric collection for Docker deployment
This commit is contained in:
5
.dockerignore
Normal file
5
.dockerignore
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
harborforge-monitor
|
||||||
|
bin
|
||||||
|
dist
|
||||||
20
Dockerfile
Normal file
20
Dockerfile
Normal file
@@ -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"]
|
||||||
56
README.md
56
README.md
@@ -29,8 +29,7 @@ HarborForge.Monitor/
|
|||||||
├── cmd/harborforge-monitor/ # 程序入口
|
├── cmd/harborforge-monitor/ # 程序入口
|
||||||
├── internal/config/ # 配置加载
|
├── internal/config/ # 配置加载
|
||||||
├── internal/telemetry/ # 指标采集与上报
|
├── internal/telemetry/ # 指标采集与上报
|
||||||
├── scripts/install.sh # 安装脚本
|
├── Dockerfile # 容器化运行
|
||||||
├── systemd/harborforge-monitor.service
|
|
||||||
├── config.example.json
|
├── config.example.json
|
||||||
└── README.md
|
└── README.md
|
||||||
```
|
```
|
||||||
@@ -51,13 +50,16 @@ HarborForge.Monitor/
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
也支持环境变量覆盖:
|
也支持环境变量覆盖。为了兼容你的命名,这里优先支持:
|
||||||
|
|
||||||
- `HF_MONITOR_BACKEND_URL`
|
- `HF_MONITER_BACKEND_URL`
|
||||||
- `HF_MONITOR_IDENTIFIER`
|
- `HF_MONITER_IDENTIFIER`
|
||||||
- `HF_MONITOR_API_KEY`
|
- `HF_MONITER_API_KEY`
|
||||||
- `HF_MONITOR_REPORT_INTERVAL`
|
- `HF_MONITER_REPORT_INTERVAL`
|
||||||
- `HF_MONITOR_LOG_LEVEL`
|
- `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
|
./harborforge-monitor -config ./config.example.json -dry-run -once
|
||||||
```
|
```
|
||||||
|
|
||||||
## 安装
|
## Docker 运行
|
||||||
|
|
||||||
|
构建镜像:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo ./scripts/install.sh
|
docker build -t harborforge-monitor .
|
||||||
```
|
```
|
||||||
|
|
||||||
安装脚本会:
|
推荐以**宿主机 rootfs 只读挂载**方式运行,这样容器里采集到的是宿主机信息而不是容器自身:
|
||||||
|
|
||||||
- 构建二进制 `harborforge-monitor`
|
```bash
|
||||||
- 安装到 `/usr/local/bin/`
|
docker run -d \
|
||||||
- 安装 systemd service
|
--name harborforge-monitor \
|
||||||
- 初始化 `/etc/harborforge-monitor/config.json`
|
--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`
|
- 如果机器没有安装 Nginx,会回报 `nginx_installed = false`
|
||||||
- 该客户端不会尝试读取 OpenClaw 信息,`agents` 默认为空,`openclaw_version` 不上报
|
- 该客户端不会尝试读取 OpenClaw 信息,`agents` 默认为空,`openclaw_version` 不上报
|
||||||
|
|||||||
@@ -3,5 +3,6 @@
|
|||||||
"identifier": "vps-01",
|
"identifier": "vps-01",
|
||||||
"apiKey": "replace-with-server-api-key",
|
"apiKey": "replace-with-server-api-key",
|
||||||
"reportIntervalSec": 30,
|
"reportIntervalSec": 30,
|
||||||
"logLevel": "info"
|
"logLevel": "info",
|
||||||
|
"rootFs": "/host"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
@@ -12,15 +13,17 @@ type Config struct {
|
|||||||
APIKey string `json:"apiKey"`
|
APIKey string `json:"apiKey"`
|
||||||
ReportIntervalSec int `json:"reportIntervalSec"`
|
ReportIntervalSec int `json:"reportIntervalSec"`
|
||||||
LogLevel string `json:"logLevel"`
|
LogLevel string `json:"logLevel"`
|
||||||
|
RootFS string `json:"rootFs"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func Load(path string) (Config, error) {
|
func Load(path string) (Config, error) {
|
||||||
cfg := Config{
|
cfg := Config{
|
||||||
BackendURL: getenv("HF_MONITOR_BACKEND_URL", "https://monitor.hangman-lab.top"),
|
BackendURL: getenvAny([]string{"HF_MONITER_BACKEND_URL", "HF_MONITOR_BACKEND_URL"}, "https://monitor.hangman-lab.top"),
|
||||||
Identifier: getenv("HF_MONITOR_IDENTIFIER", hostnameOr("unknown-host")),
|
Identifier: getenvAny([]string{"HF_MONITER_IDENTIFIER", "HF_MONITOR_IDENTIFIER"}, hostnameOr("unknown-host")),
|
||||||
APIKey: os.Getenv("HF_MONITOR_API_KEY"),
|
APIKey: getenvAny([]string{"HF_MONITER_API_KEY", "HF_MONITOR_API_KEY"}, ""),
|
||||||
ReportIntervalSec: getenvInt("HF_MONITOR_REPORT_INTERVAL", 30),
|
ReportIntervalSec: getenvIntAny([]string{"HF_MONITER_REPORT_INTERVAL", "HF_MONITOR_REPORT_INTERVAL"}, 30),
|
||||||
LogLevel: getenv("HF_MONITOR_LOG_LEVEL", "info"),
|
LogLevel: getenvAny([]string{"HF_MONITER_LOG_LEVEL", "HF_MONITOR_LOG_LEVEL"}, "info"),
|
||||||
|
RootFS: getenvAny([]string{"HF_MONITER_ROOTFS", "HF_MONITOR_ROOTFS"}, ""),
|
||||||
}
|
}
|
||||||
|
|
||||||
if path != "" {
|
if path != "" {
|
||||||
@@ -32,13 +35,14 @@ func Load(path string) (Config, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// env always wins over file
|
// env always wins over file
|
||||||
cfg.BackendURL = getenv("HF_MONITOR_BACKEND_URL", cfg.BackendURL)
|
cfg.BackendURL = getenvAny([]string{"HF_MONITER_BACKEND_URL", "HF_MONITOR_BACKEND_URL"}, cfg.BackendURL)
|
||||||
cfg.Identifier = getenv("HF_MONITOR_IDENTIFIER", cfg.Identifier)
|
cfg.Identifier = getenvAny([]string{"HF_MONITER_IDENTIFIER", "HF_MONITOR_IDENTIFIER"}, cfg.Identifier)
|
||||||
if v := os.Getenv("HF_MONITOR_API_KEY"); v != "" {
|
if v := getenvAny([]string{"HF_MONITER_API_KEY", "HF_MONITOR_API_KEY"}, ""); v != "" {
|
||||||
cfg.APIKey = v
|
cfg.APIKey = v
|
||||||
}
|
}
|
||||||
cfg.ReportIntervalSec = getenvInt("HF_MONITOR_REPORT_INTERVAL", cfg.ReportIntervalSec)
|
cfg.ReportIntervalSec = getenvIntAny([]string{"HF_MONITER_REPORT_INTERVAL", "HF_MONITOR_REPORT_INTERVAL"}, cfg.ReportIntervalSec)
|
||||||
cfg.LogLevel = getenv("HF_MONITOR_LOG_LEVEL", cfg.LogLevel)
|
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 == "" {
|
if cfg.BackendURL == "" {
|
||||||
return cfg, fmt.Errorf("backendUrl is required")
|
return cfg, fmt.Errorf("backendUrl is required")
|
||||||
@@ -49,6 +53,7 @@ func Load(path string) (Config, error) {
|
|||||||
if cfg.ReportIntervalSec <= 0 {
|
if cfg.ReportIntervalSec <= 0 {
|
||||||
cfg.ReportIntervalSec = 30
|
cfg.ReportIntervalSec = 30
|
||||||
}
|
}
|
||||||
|
applyHostFSEnv(cfg.RootFS)
|
||||||
return cfg, nil
|
return cfg, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,22 +85,29 @@ func merge(dst *Config, src Config) {
|
|||||||
if src.LogLevel != "" {
|
if src.LogLevel != "" {
|
||||||
dst.LogLevel = src.LogLevel
|
dst.LogLevel = src.LogLevel
|
||||||
}
|
}
|
||||||
|
if src.RootFS != "" {
|
||||||
|
dst.RootFS = src.RootFS
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func getenv(key, fallback string) string {
|
func getenvAny(keys []string, fallback string) string {
|
||||||
|
for _, key := range keys {
|
||||||
if v := os.Getenv(key); v != "" {
|
if v := os.Getenv(key); v != "" {
|
||||||
return v
|
return v
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return fallback
|
return fallback
|
||||||
}
|
}
|
||||||
|
|
||||||
func getenvInt(key string, fallback int) int {
|
func getenvIntAny(keys []string, fallback int) int {
|
||||||
|
for _, key := range keys {
|
||||||
if v := os.Getenv(key); v != "" {
|
if v := os.Getenv(key); v != "" {
|
||||||
var out int
|
var out int
|
||||||
if _, err := fmt.Sscanf(v, "%d", &out); err == nil && out > 0 {
|
if _, err := fmt.Sscanf(v, "%d", &out); err == nil && out > 0 {
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return fallback
|
return fallback
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,3 +118,20 @@ func hostnameOr(fallback string) string {
|
|||||||
}
|
}
|
||||||
return name
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -55,7 +55,11 @@ func BuildPayload(ctx context.Context, cfg config.Config) (Payload, error) {
|
|||||||
payload.MemPct = round1(vm.UsedPercent)
|
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 {
|
if err == nil {
|
||||||
payload.DiskPct = round1(diskUsage.UsedPercent)
|
payload.DiskPct = round1(diskUsage.UsedPercent)
|
||||||
}
|
}
|
||||||
@@ -75,7 +79,7 @@ func BuildPayload(ctx context.Context, cfg config.Config) (Payload, error) {
|
|||||||
payload.UptimeSeconds = hostInfo.Uptime
|
payload.UptimeSeconds = hostInfo.Uptime
|
||||||
}
|
}
|
||||||
|
|
||||||
nginxInstalled, nginxSites, err := detectNginx()
|
nginxInstalled, nginxSites, err := detectNginx(cfg.RootFS)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
payload.NginxInstalled = nginxInstalled
|
payload.NginxInstalled = nginxInstalled
|
||||||
payload.NginxSites = nginxSites
|
payload.NginxSites = nginxSites
|
||||||
@@ -109,12 +113,20 @@ func Send(ctx context.Context, client *http.Client, cfg config.Config, payload P
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func detectNginx() (bool, []string, error) {
|
func detectNginx(rootFS string) (bool, []string, error) {
|
||||||
installed := false
|
installed := false
|
||||||
|
if rootFS == "" {
|
||||||
if _, err := exec.LookPath("nginx"); err == nil {
|
if _, err := exec.LookPath("nginx"); err == nil {
|
||||||
installed = true
|
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 {
|
if _, err := os.Stat(path); err == nil {
|
||||||
installed = true
|
installed = true
|
||||||
break
|
break
|
||||||
@@ -124,7 +136,7 @@ func detectNginx() (bool, []string, error) {
|
|||||||
return false, []string{}, nil
|
return false, []string{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
dir := "/etc/nginx/sites-enabled"
|
dir := rootPath(rootFS, "/etc/nginx/sites-enabled")
|
||||||
entries, err := os.ReadDir(dir)
|
entries, err := os.ReadDir(dir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
@@ -155,3 +167,10 @@ func round1(v float64) float64 {
|
|||||||
func round2(v float64) float64 {
|
func round2(v float64) float64 {
|
||||||
return float64(int(v*100+0.5)) / 100
|
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, "/"))
|
||||||
|
}
|
||||||
|
|||||||
@@ -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."
|
|
||||||
Reference in New Issue
Block a user