Files
HarborForge.Monitor/cmd/harborforge-monitor/main.go

154 lines
4.0 KiB
Go

package main
import (
"context"
"encoding/json"
"flag"
"fmt"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"git.hangman-lab.top/zhi/HarborForge.Monitor/internal/bridge"
"git.hangman-lab.top/zhi/HarborForge.Monitor/internal/config"
"git.hangman-lab.top/zhi/HarborForge.Monitor/internal/telemetry"
)
func main() {
var (
configPath string
runOnce bool
printPayload bool
dryRun bool
showVersion bool
backendURL string
identifier string
apiKey string
reportInt int
logLevel string
rootFS string
monitorPort int
)
flag.StringVar(&configPath, "config", "/etc/harborforge-monitor/config.json", "Path to config file")
flag.BoolVar(&runOnce, "once", false, "Collect and send telemetry once, then exit")
flag.BoolVar(&printPayload, "print-payload", false, "Print payload JSON before sending")
flag.BoolVar(&dryRun, "dry-run", false, "Collect telemetry but do not send it")
flag.BoolVar(&showVersion, "version", false, "Print version and exit")
flag.StringVar(&backendURL, "backend-url", "", "Override backend URL")
flag.StringVar(&identifier, "identifier", "", "Override identifier")
flag.StringVar(&apiKey, "api-key", "", "Override API key")
flag.IntVar(&reportInt, "report-interval", 0, "Override report interval in seconds")
flag.StringVar(&logLevel, "log-level", "", "Override log level")
flag.StringVar(&rootFS, "rootfs", "", "Override root filesystem path")
flag.IntVar(&monitorPort, "monitor-port", 0, "Override monitor bridge port")
flag.Parse()
if showVersion {
fmt.Println(telemetry.Version)
return
}
cfg, err := config.LoadWithOverrides(configPath, config.Overrides{
BackendURL: backendURL,
Identifier: identifier,
APIKey: apiKey,
ReportIntervalSec: reportInt,
LogLevel: logLevel,
RootFS: rootFS,
MonitorPort: monitorPort,
})
if err != nil {
log.Fatalf("load config: %v", err)
}
if cfg.APIKey == "" {
log.Fatalf("apiKey is required")
}
logger := log.New(os.Stdout, "[harborforge-monitor] ", log.LstdFlags)
client := &http.Client{Timeout: 20 * time.Second}
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop()
// Start the bridge server if MONITOR_PORT is configured.
// The bridge is independent of heartbeat — if it fails to start,
// heartbeat continues normally.
var bridgeSrv *bridge.Server
if cfg.MonitorPort > 0 {
bridgeSrv = bridge.New(cfg, logger)
go func() {
if err := bridgeSrv.Start(ctx); err != nil {
logger.Printf("bridge error (non-fatal): %v", err)
}
}()
}
sendOnce := func() error {
payload, err := telemetry.BuildPayload(ctx, cfg)
if err != nil {
return err
}
// Update bridge with latest telemetry
if bridgeSrv != nil {
bridgeSrv.UpdatePayload(payload)
// Enrich payload with OpenClaw metadata if available
if meta := bridgeSrv.GetOpenClawMeta(); meta != nil {
if meta.Version != "" {
payload.OpenClawVersion = meta.Version
}
if meta.PluginVersion != "" {
payload.PluginVersion = meta.PluginVersion
}
if len(meta.Agents) > 0 {
payload.Agents = meta.Agents
}
}
}
if printPayload || dryRun {
buf, _ := json.MarshalIndent(payload, "", " ")
fmt.Println(string(buf))
}
if dryRun {
logger.Printf("dry-run: telemetry collected for %s", cfg.Identifier)
return nil
}
if err := telemetry.Send(ctx, client, cfg, payload); err != nil {
return err
}
logger.Printf("heartbeat sent for %s", cfg.Identifier)
return nil
}
if runOnce {
if err := sendOnce(); err != nil {
log.Fatalf("send once failed: %v", err)
}
return
}
ticker := time.NewTicker(time.Duration(cfg.ReportIntervalSec) * time.Second)
defer ticker.Stop()
if err := sendOnce(); err != nil {
logger.Printf("initial heartbeat failed: %v", err)
}
for {
select {
case <-ctx.Done():
logger.Printf("shutting down")
return
case <-ticker.C:
if err := sendOnce(); err != nil {
logger.Printf("heartbeat failed: %v", err)
}
}
}
}