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) } } } }