package runner // metrics.go — lightweight HTTP metrics endpoint for the runner. // // GET /metrics returns a JSON document with live task counters and uptime. // No external dependency (no Prometheus); plain net/http only. import ( "context" "encoding/json" "fmt" "log/slog" "net" "net/http" "time" ) // metricsServer serves GET /metrics for the runner process. type metricsServer struct { addr string r *Runner log *slog.Logger } func newMetricsServer(addr string, r *Runner, log *slog.Logger) *metricsServer { return &metricsServer{addr: addr, r: r, log: log} } // ListenAndServe starts the HTTP server and blocks until ctx is cancelled or // a fatal listen error occurs. func (ms *metricsServer) ListenAndServe(ctx context.Context) error { mux := http.NewServeMux() mux.HandleFunc("GET /metrics", ms.handleMetrics) mux.HandleFunc("GET /health", ms.handleHealth) srv := &http.Server{ Addr: ms.addr, Handler: mux, ReadTimeout: 5 * time.Second, WriteTimeout: 5 * time.Second, BaseContext: func(_ net.Listener) context.Context { return ctx }, } errCh := make(chan error, 1) go func() { ms.log.Info("runner: metrics server listening", "addr", ms.addr) errCh <- srv.ListenAndServe() }() select { case <-ctx.Done(): shutCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() _ = srv.Shutdown(shutCtx) return nil case err := <-errCh: return fmt.Errorf("runner: metrics server: %w", err) } } // handleMetrics handles GET /metrics. // Response shape (JSON): // // { // "tasks_running": N, // "tasks_completed": N, // "tasks_failed": N, // "uptime_seconds": N // } func (ms *metricsServer) handleMetrics(w http.ResponseWriter, _ *http.Request) { uptimeSec := int64(time.Since(ms.r.startedAt).Seconds()) metricsWriteJSON(w, 0, map[string]int64{ "tasks_running": ms.r.tasksRunning.Load(), "tasks_completed": ms.r.tasksCompleted.Load(), "tasks_failed": ms.r.tasksFailed.Load(), "uptime_seconds": uptimeSec, }) } // handleHealth handles GET /health — simple liveness probe for the metrics server. func (ms *metricsServer) handleHealth(w http.ResponseWriter, _ *http.Request) { metricsWriteJSON(w, 0, map[string]string{"status": "ok"}) } // metricsWriteJSON writes v as a JSON response with the given status code. func metricsWriteJSON(w http.ResponseWriter, status int, v any) { w.Header().Set("Content-Type", "application/json") if status != 0 { w.WriteHeader(status) } _ = json.NewEncoder(w).Encode(v) }