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 ) 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.Parse() if showVersion { fmt.Println(telemetry.Version) return } cfg, err := config.Load(configPath) 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) } } } }