Skip to content

feat: router Prometheus /metrics endpoint (JEP-0013 Phase 2) - #933

Open
RoddieKieley wants to merge 3 commits into
jumpstarter-dev:mainfrom
RoddieKieley:jep-0013-phase2-router-metrics
Open

feat: router Prometheus /metrics endpoint (JEP-0013 Phase 2)#933
RoddieKieley wants to merge 3 commits into
jumpstarter-dev:mainfrom
RoddieKieley:jep-0013-phase2-router-metrics

Conversation

@RoddieKieley

Copy link
Copy Markdown
  • Add HTTP GET /metrics on the Jumpstarter router (Prometheus/OpenMetrics text) via --metrics-bind-address (or equivalent).
  • Operator wires Router Deployment metrics bind address and container port.
  • Tests assert /metrics returns 200 and parseable exposition; no undocumented jumpstarter_* router series invented in this PR.

- Add HTTP `GET /metrics` on the Jumpstarter router (Prometheus/OpenMetrics text) via `--metrics-bind-address` (or equivalent).
- Operator wires Router Deployment metrics bind address and container port.
- Tests assert `/metrics` returns 200 and parseable exposition; no undocumented `jumpstarter_*` router series invented in this PR.
@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The router now supports an optional Prometheus metrics endpoint. The deployment binds it to port 8080. Build and run commands target the router package. Tests cover serving, disabling, shutdown, and deployment arguments.

Changes

Router metrics

Layer / File(s) Summary
Metrics server implementation
controller/cmd/router/metrics.go, controller/cmd/router/metrics_test.go
Adds an HTTP /metrics endpoint with Prometheus output. Empty and zero addresses disable the endpoint. Tests validate metric families and shutdown behavior.
Router startup integration
controller/cmd/router/main.go
Adds the -metrics-bind-address flag. Starts the metrics server and shuts it down with a five-second timeout.
Deployment and package build wiring
controller/deploy/operator/internal/controller/jumpstarter/..., controller/Containerfile, controller/Makefile
Configures the router Deployment to bind metrics on :8080 and expose the named metrics port. Container and Makefile commands build and run the router package.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant RouterDeployment
  participant RouterMain
  participant MetricsServer
  participant PrometheusClient
  RouterDeployment->>RouterMain: pass -metrics-bind-address=:8080
  RouterMain->>MetricsServer: start metrics server
  MetricsServer-->>RouterMain: bound :8080
  PrometheusClient->>MetricsServer: GET /metrics
  MetricsServer-->>PrometheusClient: Prometheus exposition text
  RouterMain->>MetricsServer: graceful shutdown on termination
Loading

Possibly related PRs

Suggested reviewers: mangelajo

Poem

A rabbit starts the metrics stream,
On port eight-zero-eight-zero’s gleam.
Package builds follow the route,
Tests check startup, stop, and output.
Prometheus hops through /metrics neat.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the primary change: adding a Prometheus /metrics endpoint to the router.
Description check ✅ Passed The description accurately covers the metrics endpoint, operator configuration, and related tests.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
controller/Makefile (1)

116-125: 📐 Maintainability & Code Quality | 🔵 Trivial

Run the repository-required validation.

Run:

make pkg-test-router
make pkg-test-operator
make pkg-ty-router
make pkg-ty-operator
make test
make lint-fix

As per coding guidelines: package tests/type checks, the complete suite, and make lint-fix are required for repository changes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@controller/Makefile` around lines 116 - 125, Run the repository-required
validation targets: pkg-test-router, pkg-test-operator, pkg-ty-router,
pkg-ty-operator, test, and lint-fix. Address any failures and leave the changes
passing all listed checks.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@controller/cmd/router/metrics_test.go`:
- Around line 58-65: The metrics test should validate the response as Prometheus
exposition rather than checking for metric-name substrings. Update the test
around the body read and existing strings.Contains assertion to parse text with
the project’s Prometheus exposition parser and fail the test when parsing
returns an error.

In `@controller/cmd/router/metrics.go`:
- Around line 40-45: Configure explicit ReadHeaderTimeout, ReadTimeout,
WriteTimeout, and IdleTimeout values on the http.Server created in the metrics
server setup before calling srv.Serve(ln), ensuring slow requests and idle
connections are bounded.

In
`@controller/deploy/operator/internal/controller/jumpstarter/router_metrics_bind_test.go`:
- Around line 27-44: The TestRouterDeploymentMetricsBind test should use the
operator’s existing envtest-based suite instead of directly invoking
JumpstarterReconciler.createRouterDeployment. Move the assertion there, or
create the Jumpstarter resource through the fake Kubernetes API and validate the
resulting Deployment through the client.

---

Nitpick comments:
In `@controller/Makefile`:
- Around line 116-125: Run the repository-required validation targets:
pkg-test-router, pkg-test-operator, pkg-ty-router, pkg-ty-operator, test, and
lint-fix. Address any failures and leave the changes passing all listed checks.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 45597983-e224-4fdc-8003-fe0dac4c48d8

📥 Commits

Reviewing files that changed from the base of the PR and between 7fb0364 and 790b209.

📒 Files selected for processing (7)
  • controller/Containerfile
  • controller/Makefile
  • controller/cmd/router/main.go
  • controller/cmd/router/metrics.go
  • controller/cmd/router/metrics_test.go
  • controller/deploy/operator/internal/controller/jumpstarter/jumpstarter_controller.go
  • controller/deploy/operator/internal/controller/jumpstarter/router_metrics_bind_test.go

Comment thread controller/cmd/router/metrics_test.go Outdated
Comment thread controller/cmd/router/metrics.go Outdated
Comment on lines +27 to +44
// Stdlib unit test (no envtest): asserts Router Deployment metrics bind for JEP-0013 Phase 2.
func TestRouterDeploymentMetricsBind(t *testing.T) {
r := &JumpstarterReconciler{}
js := &operatorv1alpha1.Jumpstarter{
ObjectMeta: metav1.ObjectMeta{
Name: "jumpstarter",
Namespace: "jumpstarter-lab",
},
Spec: operatorv1alpha1.JumpstarterSpec{
Routers: operatorv1alpha1.RoutersConfig{
Image: "example.com/router:test",
ImagePullPolicy: corev1.PullIfNotPresent,
Replicas: 1,
},
},
}

dep := r.createRouterDeployment(js, 0)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== candidate files =="
fd -a '_test\.go$' controller/deploy/operator/internal/controller 2>/dev/null | sed 's#^\./##' | head -200 || true

echo "== target file excerpt =="
target='controller/deploy/operator/internal/controller/jumpstarter/router_metrics_bind_test.go'
if [ -f "$target" ]; then
  wc -l "$target"
  sed -n '1,140p' "$target"
else
  echo "target missing"
fi

echo "== package declarations in target dir tests =="
rg -n "^(package|func Test|var Environment|NewTestEnvironment|Setup|Suite)" controller/deploy/operator/internal/controller -g '*_test.go' | head -200

echo "== helper/envtest related references =="
rg -n "envtest|NewTestEnvironment|Setup|fake|Fake|clientset|NewClient|JumpstarterReconciler|Reconcile\\(" controller/deploy/operator -g '*_test.go' | head -300

Repository: jumpstarter-dev/jumpstarter

Length of output: 14214


Use envtest for this operator test.

This file is under controller/deploy/operator/internal/controller/jumpstarter, and the test directly constructs JumpstarterReconciler objects and calls createRouterDeployment. Move the assertion into the existing envtest-based suite or exercise creation through the fake Kubernetes API instead.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@controller/deploy/operator/internal/controller/jumpstarter/router_metrics_bind_test.go`
around lines 27 - 44, The TestRouterDeploymentMetricsBind test should use the
operator’s existing envtest-based suite instead of directly invoking
JumpstarterReconciler.createRouterDeployment. Move the assertion there, or
create the Jumpstarter resource through the fake Kubernetes API and validate the
resulting Deployment through the client.

Source: Coding guidelines

Comment thread controller/cmd/router/metrics.go Outdated
Comment on lines +30 to +49
func startMetricsServer(addr string) (string, error) {
if addr == "" || addr == "0" {
return "", nil
}

ln, err := net.Listen("tcp", addr)
if err != nil {
return "", err
}

mux := http.NewServeMux()
mux.Handle("/metrics", promhttp.Handler())

srv := &http.Server{Handler: mux}
go func() {
_ = srv.Serve(ln)
}()

return ln.Addr().String(), nil
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

startMetricsServer returns only (string, error) but creates an http.Server and launches srv.Serve(ln) in a detached goroutine. The caller has no way to call srv.Shutdown(ctx) on SIGINT/SIGTERM, so the server never gets a graceful shutdown.

Comment on lines +44 to +46
go func() {
_ = srv.Serve(ln)
}()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

go func() {
    if err := srv.Serve(ln); err != nil && err != http.ErrServerClosed {
        ctrl.Log.WithName("metrics").Error(err, "metrics server stopped unexpectedly")
    }
}()

Comment thread controller/cmd/router/metrics.go Outdated
mux := http.NewServeMux()
mux.Handle("/metrics", promhttp.Handler())

srv := &http.Server{Handler: mux}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Set at least ReadHeaderTimeout and IdleTimeout:

srv := &http.Server{
    Handler:           mux,
    ReadHeaderTimeout: 10 * time.Second,
    IdleTimeout:       60 * time.Second,
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The idle timeout may need to be higher, i.e. it really depends on prometheus doing keepalive & the periodicity of checks, I'd give it 5 minutes perhaps.

Comment thread controller/cmd/router/metrics_test.go Outdated
}
text := string(body)
// Default Go process metrics should appear; do not require undocumented jumpstarter_* series.
if !strings.Contains(text, "go_") && !strings.Contains(text, "process_") && !strings.Contains(text, "promhttp_") {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If go_* metrics were removed but promhttp_* remained, the test would still pass. Assert each required family independently.

Bound router metrics HTTP server timeouts, parse /metrics with a Prometheus
exposition parser in tests, and fold the router metrics-bind assertion into
a Ginkgo Describe consistent with sibling operator unit tests.

Co-authored-by: Cursor <cursoragent@cursor.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@controller/cmd/router/metrics_test.go`:
- Around line 76-91: Update the metrics test around the families iteration and
assertions to explicitly reject any jumpstarter_* metric family, while
preserving the existing required go_* and process_* checks. Add a negative
assertion that fails with the discovered family names if a jumpstarter_* prefix
is present.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 004a2560-f5a9-4b70-af53-5e08741a9d8a

📥 Commits

Reviewing files that changed from the base of the PR and between 790b209 and 8ab7f05.

📒 Files selected for processing (3)
  • controller/cmd/router/metrics.go
  • controller/cmd/router/metrics_test.go
  • controller/deploy/operator/internal/controller/jumpstarter/router_metrics_bind_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • controller/cmd/router/metrics.go

Comment on lines +76 to +91
// Default Go process metrics should appear; do not require undocumented jumpstarter_* series.
hasGo, hasProcess := false, false
for name := range families {
switch {
case strings.HasPrefix(name, "go_"):
hasGo = true
case strings.HasPrefix(name, "process_"):
hasProcess = true
}
}
if !hasGo {
t.Fatalf("expected go_* metric family, got families: %v", familyNames(families))
}
if !hasProcess {
t.Fatalf("expected process_* metric family, got families: %v", familyNames(families))
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Enforce the no-jumpstarter_* metric contract.

The test requires go_* and process_* families, but it allows any jumpstarter_* family. The PR objective requires no undocumented jumpstarter_* router metrics. Add a negative assertion or an explicit allowlist.

Proposed assertion
 	for name := range families {
+		if strings.HasPrefix(name, "jumpstarter_") {
+			t.Fatalf("unexpected undocumented metric family %q", name)
+		}
 		switch {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Default Go process metrics should appear; do not require undocumented jumpstarter_* series.
hasGo, hasProcess := false, false
for name := range families {
switch {
case strings.HasPrefix(name, "go_"):
hasGo = true
case strings.HasPrefix(name, "process_"):
hasProcess = true
}
}
if !hasGo {
t.Fatalf("expected go_* metric family, got families: %v", familyNames(families))
}
if !hasProcess {
t.Fatalf("expected process_* metric family, got families: %v", familyNames(families))
}
// Default Go process metrics should appear; do not require undocumented jumpstarter_* series.
hasGo, hasProcess := false, false
for name := range families {
if strings.HasPrefix(name, "jumpstarter_") {
t.Fatalf("unexpected undocumented metric family %q", name)
}
switch {
case strings.HasPrefix(name, "go_"):
hasGo = true
case strings.HasPrefix(name, "process_"):
hasProcess = true
}
}
if !hasGo {
t.Fatalf("expected go_* metric family, got families: %v", familyNames(families))
}
if !hasProcess {
t.Fatalf("expected process_* metric family, got families: %v", familyNames(families))
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@controller/cmd/router/metrics_test.go` around lines 76 - 91, Update the
metrics test around the families iteration and assertions to explicitly reject
any jumpstarter_* metric family, while preserving the existing required go_* and
process_* checks. Add a negative assertion that fails with the discovered family
names if a jumpstarter_* prefix is present.

Return a shutdown callback from the router metrics server and invoke it on
SIGINT/SIGTERM, and log unexpected Serve errors. Timeouts and independent
metric-family assertions were already covered by the CodeRabbit follow-up.

Co-authored-by: Cursor <cursoragent@cursor.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
controller/cmd/router/metrics_test.go (1)

111-122: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Test the empty disabled address.

Line 112 tests "0" only. startMetricsServer also disables metrics when the address is "". Convert this to a table-driven test for both "" and "0".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@controller/cmd/router/metrics_test.go` around lines 111 - 122, Update
TestMetricsServerDisabledWhenAddrZero to a table-driven test covering both ""
and "0" as disabled addresses. Run startMetricsServer for each case and preserve
the existing assertions that the returned address is empty and shutdown is nil.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@controller/cmd/router/metrics_test.go`:
- Around line 111-122: Update TestMetricsServerDisabledWhenAddrZero to a
table-driven test covering both "" and "0" as disabled addresses. Run
startMetricsServer for each case and preserve the existing assertions that the
returned address is empty and shutdown is nil.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 030615c6-249e-48c2-9e4a-28f874360256

📥 Commits

Reviewing files that changed from the base of the PR and between 8ab7f05 and 193d8e1.

📒 Files selected for processing (3)
  • controller/cmd/router/main.go
  • controller/cmd/router/metrics.go
  • controller/cmd/router/metrics_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • controller/cmd/router/metrics.go

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants