feat: add Go-based HarborForge monitor client
- collect CPU, memory, disk, swap, load, and uptime telemetry - detect nginx and list /etc/nginx/sites-enabled entries - send heartbeat-v2 payload with API key auth - provide install script, config example, and systemd unit
This commit is contained in:
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
bin/
|
||||||
|
dist/
|
||||||
|
harborforge-monitor
|
||||||
|
*.exe
|
||||||
|
*.out
|
||||||
|
*.test
|
||||||
88
README.md
Normal file
88
README.md
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
# HarborForge.Monitor
|
||||||
|
|
||||||
|
轻量级 Go 遥测客户端,用于把服务器硬件状态上报到 HarborForge Monitor。
|
||||||
|
|
||||||
|
它**不依赖 OpenClaw**,适合普通 Linux 主机、VPS、Nginx 机器等。
|
||||||
|
|
||||||
|
## 采集内容
|
||||||
|
|
||||||
|
- CPU 使用率
|
||||||
|
- 内存使用率
|
||||||
|
- 磁盘使用率
|
||||||
|
- Swap 使用率
|
||||||
|
- Load Average
|
||||||
|
- Uptime
|
||||||
|
- Nginx 是否安装
|
||||||
|
- `/etc/nginx/sites-enabled` 列表
|
||||||
|
|
||||||
|
## 上报接口
|
||||||
|
|
||||||
|
客户端调用:
|
||||||
|
|
||||||
|
- `POST /monitor/server/heartbeat-v2`
|
||||||
|
- Header: `X-API-Key`
|
||||||
|
|
||||||
|
## 项目结构
|
||||||
|
|
||||||
|
```text
|
||||||
|
HarborForge.Monitor/
|
||||||
|
├── cmd/harborforge-monitor/ # 程序入口
|
||||||
|
├── internal/config/ # 配置加载
|
||||||
|
├── internal/telemetry/ # 指标采集与上报
|
||||||
|
├── scripts/install.sh # 安装脚本
|
||||||
|
├── systemd/harborforge-monitor.service
|
||||||
|
├── config.example.json
|
||||||
|
└── README.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## 配置
|
||||||
|
|
||||||
|
先在 HarborForge Monitor 中注册服务器并生成 API Key。
|
||||||
|
|
||||||
|
然后准备配置文件,例如 `/etc/harborforge-monitor/config.json`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"backendUrl": "https://monitor.hangman-lab.top",
|
||||||
|
"identifier": "vps-nginx-01",
|
||||||
|
"apiKey": "your-api-key",
|
||||||
|
"reportIntervalSec": 30,
|
||||||
|
"logLevel": "info"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
也支持环境变量覆盖:
|
||||||
|
|
||||||
|
- `HF_MONITOR_BACKEND_URL`
|
||||||
|
- `HF_MONITOR_IDENTIFIER`
|
||||||
|
- `HF_MONITOR_API_KEY`
|
||||||
|
- `HF_MONITOR_REPORT_INTERVAL`
|
||||||
|
- `HF_MONITOR_LOG_LEVEL`
|
||||||
|
|
||||||
|
## 本地开发
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go mod tidy
|
||||||
|
go build ./cmd/harborforge-monitor
|
||||||
|
./harborforge-monitor -config ./config.example.json -dry-run -once
|
||||||
|
```
|
||||||
|
|
||||||
|
## 安装
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo ./scripts/install.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
安装脚本会:
|
||||||
|
|
||||||
|
- 构建二进制 `harborforge-monitor`
|
||||||
|
- 安装到 `/usr/local/bin/`
|
||||||
|
- 安装 systemd service
|
||||||
|
- 初始化 `/etc/harborforge-monitor/config.json`
|
||||||
|
- 自动启用并启动服务
|
||||||
|
|
||||||
|
## 注意
|
||||||
|
|
||||||
|
- 当前 Nginx site 列表读取的是 `/etc/nginx/sites-enabled`
|
||||||
|
- 如果机器没有安装 Nginx,会回报 `nginx_installed = false`
|
||||||
|
- 该客户端不会尝试读取 OpenClaw 信息,`agents` 默认为空,`openclaw_version` 不上报
|
||||||
7
config.example.json
Normal file
7
config.example.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"backendUrl": "https://monitor.hangman-lab.top",
|
||||||
|
"identifier": "vps-01",
|
||||||
|
"apiKey": "replace-with-server-api-key",
|
||||||
|
"reportIntervalSec": 30,
|
||||||
|
"logLevel": "info"
|
||||||
|
}
|
||||||
16
go.mod
Normal file
16
go.mod
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
module git.hangman-lab.top/zhi/HarborForge.Monitor
|
||||||
|
|
||||||
|
go 1.22
|
||||||
|
|
||||||
|
require github.com/shirou/gopsutil/v4 v4.25.2
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/ebitengine/purego v0.8.2 // indirect
|
||||||
|
github.com/go-ole/go-ole v1.2.6 // indirect
|
||||||
|
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
|
||||||
|
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
|
||||||
|
github.com/tklauser/go-sysconf v0.3.12 // indirect
|
||||||
|
github.com/tklauser/numcpus v0.6.1 // indirect
|
||||||
|
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||||
|
golang.org/x/sys v0.28.0 // indirect
|
||||||
|
)
|
||||||
34
go.sum
Normal file
34
go.sum
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/ebitengine/purego v0.8.2 h1:jPPGWs2sZ1UgOSgD2bClL0MJIqu58nOmIcBuXr62z1I=
|
||||||
|
github.com/ebitengine/purego v0.8.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||||
|
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
|
||||||
|
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||||
|
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
|
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
|
||||||
|
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
|
||||||
|
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||||
|
github.com/shirou/gopsutil/v4 v4.25.2 h1:NMscG3l2CqtWFS86kj3vP7soOczqrQYIEhO/pMvvQkk=
|
||||||
|
github.com/shirou/gopsutil/v4 v4.25.2/go.mod h1:34gBYJzyqCDT11b6bMHP0XCvWeU3J61XRT7a2EmCRTA=
|
||||||
|
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||||
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
|
github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
|
||||||
|
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
|
||||||
|
github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
|
||||||
|
github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
|
||||||
|
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||||
|
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||||
|
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
|
||||||
|
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
108
internal/config/config.go
Normal file
108
internal/config/config.go
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
BackendURL string `json:"backendUrl"`
|
||||||
|
Identifier string `json:"identifier"`
|
||||||
|
APIKey string `json:"apiKey"`
|
||||||
|
ReportIntervalSec int `json:"reportIntervalSec"`
|
||||||
|
LogLevel string `json:"logLevel"`
|
||||||
|
}
|
||||||
|
|
||||||
|
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"),
|
||||||
|
}
|
||||||
|
|
||||||
|
if path != "" {
|
||||||
|
if fileCfg, err := loadFile(path); err == nil {
|
||||||
|
merge(&cfg, fileCfg)
|
||||||
|
} else if !os.IsNotExist(err) {
|
||||||
|
return cfg, fmt.Errorf("load config file: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.APIKey = v
|
||||||
|
}
|
||||||
|
cfg.ReportIntervalSec = getenvInt("HF_MONITOR_REPORT_INTERVAL", cfg.ReportIntervalSec)
|
||||||
|
cfg.LogLevel = getenv("HF_MONITOR_LOG_LEVEL", cfg.LogLevel)
|
||||||
|
|
||||||
|
if cfg.BackendURL == "" {
|
||||||
|
return cfg, fmt.Errorf("backendUrl is required")
|
||||||
|
}
|
||||||
|
if cfg.Identifier == "" {
|
||||||
|
return cfg, fmt.Errorf("identifier is required")
|
||||||
|
}
|
||||||
|
if cfg.ReportIntervalSec <= 0 {
|
||||||
|
cfg.ReportIntervalSec = 30
|
||||||
|
}
|
||||||
|
return cfg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadFile(path string) (Config, error) {
|
||||||
|
var cfg Config
|
||||||
|
buf, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return cfg, err
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(buf, &cfg); err != nil {
|
||||||
|
return cfg, err
|
||||||
|
}
|
||||||
|
return cfg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func merge(dst *Config, src Config) {
|
||||||
|
if src.BackendURL != "" {
|
||||||
|
dst.BackendURL = src.BackendURL
|
||||||
|
}
|
||||||
|
if src.Identifier != "" {
|
||||||
|
dst.Identifier = src.Identifier
|
||||||
|
}
|
||||||
|
if src.APIKey != "" {
|
||||||
|
dst.APIKey = src.APIKey
|
||||||
|
}
|
||||||
|
if src.ReportIntervalSec > 0 {
|
||||||
|
dst.ReportIntervalSec = src.ReportIntervalSec
|
||||||
|
}
|
||||||
|
if src.LogLevel != "" {
|
||||||
|
dst.LogLevel = src.LogLevel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getenv(key, fallback string) string {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
func hostnameOr(fallback string) string {
|
||||||
|
name, err := os.Hostname()
|
||||||
|
if err != nil || name == "" {
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
return name
|
||||||
|
}
|
||||||
158
internal/telemetry/telemetry.go
Normal file
158
internal/telemetry/telemetry.go
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
package telemetry
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/shirou/gopsutil/v4/cpu"
|
||||||
|
"github.com/shirou/gopsutil/v4/disk"
|
||||||
|
"github.com/shirou/gopsutil/v4/host"
|
||||||
|
gopsload "github.com/shirou/gopsutil/v4/load"
|
||||||
|
"github.com/shirou/gopsutil/v4/mem"
|
||||||
|
|
||||||
|
"git.hangman-lab.top/zhi/HarborForge.Monitor/internal/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
const Version = "0.1.0"
|
||||||
|
|
||||||
|
type Payload struct {
|
||||||
|
Identifier string `json:"identifier"`
|
||||||
|
PluginVersion string `json:"plugin_version,omitempty"`
|
||||||
|
Agents []any `json:"agents"`
|
||||||
|
NginxInstalled bool `json:"nginx_installed"`
|
||||||
|
NginxSites []string `json:"nginx_sites"`
|
||||||
|
CPUPct float64 `json:"cpu_pct,omitempty"`
|
||||||
|
MemPct float64 `json:"mem_pct,omitempty"`
|
||||||
|
DiskPct float64 `json:"disk_pct,omitempty"`
|
||||||
|
SwapPct float64 `json:"swap_pct,omitempty"`
|
||||||
|
LoadAvg []float64 `json:"load_avg,omitempty"`
|
||||||
|
UptimeSeconds uint64 `json:"uptime_seconds,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func BuildPayload(ctx context.Context, cfg config.Config) (Payload, error) {
|
||||||
|
payload := Payload{
|
||||||
|
Identifier: cfg.Identifier,
|
||||||
|
PluginVersion: Version,
|
||||||
|
Agents: []any{},
|
||||||
|
NginxSites: []string{},
|
||||||
|
}
|
||||||
|
|
||||||
|
cpuPct, err := cpu.PercentWithContext(ctx, time.Second, false)
|
||||||
|
if err == nil && len(cpuPct) > 0 {
|
||||||
|
payload.CPUPct = round1(cpuPct[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
vm, err := mem.VirtualMemoryWithContext(ctx)
|
||||||
|
if err == nil {
|
||||||
|
payload.MemPct = round1(vm.UsedPercent)
|
||||||
|
}
|
||||||
|
|
||||||
|
diskUsage, err := disk.UsageWithContext(ctx, "/")
|
||||||
|
if err == nil {
|
||||||
|
payload.DiskPct = round1(diskUsage.UsedPercent)
|
||||||
|
}
|
||||||
|
|
||||||
|
swapUsage, err := mem.SwapMemoryWithContext(ctx)
|
||||||
|
if err == nil {
|
||||||
|
payload.SwapPct = round1(swapUsage.UsedPercent)
|
||||||
|
}
|
||||||
|
|
||||||
|
avg, err := gopsload.AvgWithContext(ctx)
|
||||||
|
if err == nil {
|
||||||
|
payload.LoadAvg = []float64{round2(avg.Load1), round2(avg.Load5), round2(avg.Load15)}
|
||||||
|
}
|
||||||
|
|
||||||
|
hostInfo, err := host.InfoWithContext(ctx)
|
||||||
|
if err == nil {
|
||||||
|
payload.UptimeSeconds = hostInfo.Uptime
|
||||||
|
}
|
||||||
|
|
||||||
|
nginxInstalled, nginxSites, err := detectNginx()
|
||||||
|
if err != nil {
|
||||||
|
return payload, err
|
||||||
|
}
|
||||||
|
payload.NginxInstalled = nginxInstalled
|
||||||
|
payload.NginxSites = nginxSites
|
||||||
|
|
||||||
|
return payload, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func Send(ctx context.Context, client *http.Client, cfg config.Config, payload Payload) error {
|
||||||
|
body, err := json.Marshal(payload)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("marshal payload: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(cfg.BackendURL, "/")+"/monitor/server/heartbeat-v2", strings.NewReader(string(body)))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("build request: %w", err)
|
||||||
|
}
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
req.Header.Set("X-API-Key", cfg.APIKey)
|
||||||
|
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("send request: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||||
|
return fmt.Errorf("heartbeat failed with status %s", resp.Status)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func detectNginx() (bool, []string, error) {
|
||||||
|
installed := false
|
||||||
|
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"} {
|
||||||
|
if _, err := os.Stat(path); err == nil {
|
||||||
|
installed = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !installed {
|
||||||
|
return false, []string{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
dir := "/etc/nginx/sites-enabled"
|
||||||
|
entries, err := os.ReadDir(dir)
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return true, []string{}, nil
|
||||||
|
}
|
||||||
|
return true, nil, fmt.Errorf("read nginx sites-enabled: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
sites := make([]string, 0, len(entries))
|
||||||
|
for _, entry := range entries {
|
||||||
|
name := entry.Name()
|
||||||
|
fullPath := filepath.Join(dir, name)
|
||||||
|
if entry.Type()&os.ModeSymlink != 0 {
|
||||||
|
if target, err := os.Readlink(fullPath); err == nil {
|
||||||
|
name = fmt.Sprintf("%s -> %s", name, target)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sites = append(sites, name)
|
||||||
|
}
|
||||||
|
sort.Strings(sites)
|
||||||
|
return true, sites, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func round1(v float64) float64 {
|
||||||
|
return float64(int(v*10+0.5)) / 10
|
||||||
|
}
|
||||||
|
|
||||||
|
func round2(v float64) float64 {
|
||||||
|
return float64(int(v*100+0.5)) / 100
|
||||||
|
}
|
||||||
28
scripts/install.sh
Executable file
28
scripts/install.sh
Executable file
@@ -0,0 +1,28 @@
|
|||||||
|
#!/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."
|
||||||
15
systemd/harborforge-monitor.service
Normal file
15
systemd/harborforge-monitor.service
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=HarborForge Monitor Telemetry Client
|
||||||
|
After=network-online.target
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=root
|
||||||
|
WorkingDirectory=/etc/harborforge-monitor
|
||||||
|
ExecStart=/usr/local/bin/harborforge-monitor -config /etc/harborforge-monitor/config.json
|
||||||
|
Restart=always
|
||||||
|
RestartSec=10
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
Reference in New Issue
Block a user