forgejo-runner-optimiser/internal/receiver/handler.go
Manuel Ganter c309bd810d
All checks were successful
ci / build (push) Successful in 2m33s
feat(receiver): add HTTP metrics receiver with SQLite storage
Add a new receiver application under cmd/receiver that accepts metrics
via HTTP POST and stores them in SQLite using GORM. The receiver expects
GitHub Actions style execution context (org, repo, workflow, job, run_id).

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-06 11:40:03 +01:00

101 lines
3.1 KiB
Go

// ABOUTME: HTTP handlers for the metrics receiver service.
// ABOUTME: Provides endpoints for receiving and querying metrics.
package receiver
import (
"encoding/json"
"log/slog"
"net/http"
)
// Handler handles HTTP requests for the metrics receiver
type Handler struct {
store *Store
logger *slog.Logger
}
// NewHandler creates a new HTTP handler with the given store
func NewHandler(store *Store, logger *slog.Logger) *Handler {
return &Handler{store: store, logger: logger}
}
// RegisterRoutes registers all HTTP routes on the given mux
func (h *Handler) RegisterRoutes(mux *http.ServeMux) {
mux.HandleFunc("POST /api/v1/metrics", h.handleReceiveMetrics)
mux.HandleFunc("GET /api/v1/metrics/run/{runID}", h.handleGetByRunID)
mux.HandleFunc("GET /api/v1/metrics/repo/{org}/{repo}", h.handleGetByRepository)
mux.HandleFunc("GET /health", h.handleHealth)
}
func (h *Handler) handleReceiveMetrics(w http.ResponseWriter, r *http.Request) {
var payload MetricsPayload
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
h.logger.Error("failed to decode payload", slog.String("error", err.Error()))
http.Error(w, "invalid JSON payload", http.StatusBadRequest)
return
}
if payload.Execution.RunID == "" {
http.Error(w, "run_id is required", http.StatusBadRequest)
return
}
id, err := h.store.SaveMetric(&payload)
if err != nil {
h.logger.Error("failed to save metric", slog.String("error", err.Error()))
http.Error(w, "failed to save metric", http.StatusInternalServerError)
return
}
h.logger.Info("metric saved",
slog.Uint64("id", uint64(id)),
slog.String("run_id", payload.Execution.RunID),
slog.String("repository", payload.Execution.Repository),
)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
_ = json.NewEncoder(w).Encode(map[string]any{"id": id, "status": "created"})
}
func (h *Handler) handleGetByRunID(w http.ResponseWriter, r *http.Request) {
runID := r.PathValue("runID")
if runID == "" {
http.Error(w, "run_id is required", http.StatusBadRequest)
return
}
metrics, err := h.store.GetMetricsByRunID(runID)
if err != nil {
h.logger.Error("failed to get metrics", slog.String("error", err.Error()))
http.Error(w, "failed to get metrics", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(metrics)
}
func (h *Handler) handleGetByRepository(w http.ResponseWriter, r *http.Request) {
org := r.PathValue("org")
repo := r.PathValue("repo")
if org == "" || repo == "" {
http.Error(w, "org and repo are required", http.StatusBadRequest)
return
}
metrics, err := h.store.GetMetricsByRepository(org, repo)
if err != nil {
h.logger.Error("failed to get metrics", slog.String("error", err.Error()))
http.Error(w, "failed to get metrics", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(metrics)
}
func (h *Handler) handleHealth(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
}