feat(api): serve felis_* metrics on the internal face (build-failure counter's only scrape path)
This commit is contained in:
4 files changed
+103
-1
No files matched your search
+5
-1
@@ -264,11 +264,15 @@ type apiRoute struct {
|
||||
}
|
||||
|
||||
// internalAPIRoutes is the internal face's served route table (spec §7, §14):
|
||||
// service-token auth, never Zero Trust. It carries both health probes.
|
||||
// service-token auth, never Zero Trust. It carries both health probes and the
|
||||
// metrics scrape.
|
||||
func (a *API) internalAPIRoutes() []apiRoute {
|
||||
return []apiRoute{
|
||||
{Method: "GET", Pattern: "/healthz", Public: true, h: a.handleHealthz},
|
||||
{Method: "GET", Pattern: "/readyz", Public: true, h: a.handleReadyz},
|
||||
// Prometheus scrape (felis_* collectors); public because a scrape carries
|
||||
// no token, internal-only so it is never exposed off-cluster.
|
||||
{Method: "GET", Pattern: "/metrics", Public: true, h: a.handleMetrics},
|
||||
|
||||
{Method: "GET", Pattern: "/api/v1/servers", h: a.handleListServers},
|
||||
// The build Pod's context-fetch initContainer streams a submission's stored
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"felis.lolicon.best/internal/metrics"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
)
|
||||
|
||||
// apiMetricsHandler serves the felis_* collectors from a process-local registry.
|
||||
// The API process is the only producer of felis_image_build_failures_total (the
|
||||
// build reconcile loop lives in cmd/felis.reconcileBuilds), so this endpoint is
|
||||
// that counter's sole scrape path; the operator's :8080 carries the fleet
|
||||
// gauges instead.
|
||||
var apiMetricsHandler = newAPIMetricsHandler()
|
||||
|
||||
func newAPIMetricsHandler() http.Handler {
|
||||
reg := prometheus.NewRegistry()
|
||||
// A fresh registry cannot already hold a collector, so Register's
|
||||
// AlreadyRegistered tolerance arm never triggers here.
|
||||
_ = metrics.Register(reg)
|
||||
return promhttp.HandlerFor(reg, promhttp.HandlerOpts{})
|
||||
}
|
||||
|
||||
// handleMetrics is mounted as a Public route on the internal face (the same
|
||||
// stance as the health probes): the internal listener is ClusterIP-only and a
|
||||
// Prometheus scrape carries no token. The external face never serves metrics.
|
||||
// See docs/troubleshooting.md §14 for what to scrape and the alert rules that
|
||||
// consume these series.
|
||||
func (a *API) handleMetrics(w http.ResponseWriter, r *http.Request) {
|
||||
apiMetricsHandler.ServeHTTP(w, r)
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"felis.lolicon.best/internal/metrics"
|
||||
)
|
||||
|
||||
// TestMetricsEndpoint locks the scrape surface: the internal face serves the
|
||||
// felis_* collectors (the API process is felis_image_build_failures_total's
|
||||
// only producer), and the external face never serves metrics.
|
||||
func TestMetricsEndpoint(t *testing.T) {
|
||||
// Give every collector a sample so the exposition carries all four families:
|
||||
// an empty vector family is omitted from the text output entirely. The bumps
|
||||
// stay inside this test binary — the collectors are per-process globals.
|
||||
metrics.SyncServerGauge([]string{"Running"})
|
||||
metrics.StartDurationSeconds.Observe(3)
|
||||
metrics.ImageBuildFailuresTotal.Inc()
|
||||
metrics.ReaperWorldsDeletedTotal.Inc()
|
||||
|
||||
a := &API{}
|
||||
|
||||
w := do(a.InternalHandler(), "GET", "/metrics", "", nil)
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("internal GET /metrics = %d, want 200", w.Code)
|
||||
}
|
||||
body := w.Body.String()
|
||||
for _, want := range []string{
|
||||
`felis_servers_total{state="Running"} 1`,
|
||||
"felis_start_duration_seconds_count 1",
|
||||
"felis_image_build_failures_total",
|
||||
"felis_reaper_worlds_deleted_total",
|
||||
} {
|
||||
if !strings.Contains(body, want) {
|
||||
t.Errorf("metrics exposition missing %q", want)
|
||||
}
|
||||
}
|
||||
|
||||
if w := do(a.ExternalHandler(), "GET", "/metrics", "", nil); w.Code != http.StatusNotFound {
|
||||
t.Fatalf("external GET /metrics = %d, want 404 (metrics stay internal)", w.Code)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user