feat: router Prometheus /metrics endpoint (JEP-0013 Phase 2) - #933
feat: router Prometheus /metrics endpoint (JEP-0013 Phase 2)#933RoddieKieley wants to merge 1 commit into
Conversation
- 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.
📝 WalkthroughWalkthroughThe router now supports an optional Prometheus metrics endpoint, with deployment configuration binding it to port 8080. Router build and run commands target the package directory, and tests cover endpoint serving, disabling, and deployment arguments. ChangesRouter metrics
Estimated code review effort: 3 (Moderate) | ~20 minutes Suggested reviewers: Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 golangci-lint (2.12.2)level=error msg="[linters_context] typechecking error: pattern ./...: directory prefix . does not contain main module or its selected dependencies" 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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (1)
controller/Makefile (1)
116-125: 📐 Maintainability & Code Quality | 🔵 TrivialRun 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-fixAs per coding guidelines: package tests/type checks, the complete suite, and
make lint-fixare 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
📒 Files selected for processing (7)
controller/Containerfilecontroller/Makefilecontroller/cmd/router/main.gocontroller/cmd/router/metrics.gocontroller/cmd/router/metrics_test.gocontroller/deploy/operator/internal/controller/jumpstarter/jumpstarter_controller.gocontroller/deploy/operator/internal/controller/jumpstarter/router_metrics_bind_test.go
| body, err := io.ReadAll(resp.Body) | ||
| if err != nil { | ||
| t.Fatalf("read body: %v", err) | ||
| } | ||
| 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_") { | ||
| t.Fatalf("expected Prometheus exposition with go_/process_ metrics, got:\n%s", text) |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== locate file =="
fd -a 'metrics_test.go' . || true
echo "== file context =="
cat -n controller/cmd/router/metrics_test.go | sed -n '1,120p'
echo "== go.mod deps for prometheus parser =="
rg -n 'prometheus/client_golang|prometheus/common|expfmt|ParseMetricText' go.mod go.sum || true
echo "== related metrics handlers =="
rg -n 'prometheus|DefaultGatherers|http.Handler|MetricText|Metrics|promhttp' controller/cmd/router -S || trueRepository: jumpstarter-dev/jumpstarter
Length of output: 4295
🏁 Script executed:
#!/bin/bash
set -euo pipefail
python3 - <<'PY'
samples = [
('contain_go_without_exp', 'go_abc process_def'),
('contain_process_without_exp', 'process_xyz'),
('contain_promhttp_without_exp', 'promhttp_request_duration_seconds'),
('valid_comment_like', '# HELP go_gc_duration_seconds summary\nhelp foo'),
('valid_comment_like_with_keyword', '# HELP go_gc_duration_seconds summary\n# PROCESS foo'),
('single_valid_metric_lines', 'go_info{version="1.0"} 1\nprocess_start_time_seconds 123\n'),
('single_metric_missing_help', 'go_info{version="1.0"} 1\n'),
('single_metric_with_extra_label_quote', 'go_info{version=1.0"} 1\n'),
('single_metric_with_extra_label_quote_backwards', 'go_info{version="1.0 1\n'),
]
# Python does not use Go substrings; emulate exact `strings.Contains(text, "go_")` logic separately.
import re
for name,s in samples:
contains_go = 'go_' in s
contains_process = 'process_' in s
contains_promhttp = 'promhttp_' in s
# crude prometheus comment/text parser based on common fields can reject malformed labels/types
# Use a read-only behavioral probe simulating: substring check passes without parsing.
print(f"{name}: contains={contains_go or contains_process or contains_promhttp} {s!r}")
PYRepository: jumpstarter-dev/jumpstarter
Length of output: 889
Parse the metrics body with a Prometheus exposition parser.
The go_ / process_ / promhttp_ substring check can pass malformed text that is not valid Prometheus exposition, so it doesn’t validate the /metrics response contract. Parse the body and fail on parse errors.
🤖 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 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.
| mux := http.NewServeMux() | ||
| mux.Handle("/metrics", promhttp.Handler()) | ||
|
|
||
| srv := &http.Server{Handler: mux} | ||
| go func() { | ||
| _ = srv.Serve(ln) |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "Files:"
git ls-files | rg '(^|/)metrics\.go$|Makefile$|.*router.*metrics.*' || true
echo
echo "Target file context:"
if [ -f controller/cmd/router/metrics.go ]; then
nl -ba controller/cmd/router/metrics.go | sed -n '1,140p'
fi
echo
echo "References to metrics listener/port:"
rg -n 'metrics|:8080|http\.Serve|http\.NewServeMux|promhttp\.Handler|IdleTimeout|ReadHeaderTimeout|ReadTimeout|WriteTimeout' . --glob '!vendor/**' --glob '!node_modules/**' || trueRepository: jumpstarter-dev/jumpstarter
Length of output: 637
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "Package build/test/lint references:"
if [ -f controller/Makefile ]; then
cat controller/Makefile | sed -n '1,220p'
fi
echo
echo "Target file content:"
if [ -f controller/cmd/router/metrics.go ]; then
awk '{printf "%5d\t%s\n", NR, $0}' controller/cmd/router/metrics.go
fi
echo
echo "Router metrics bind tests:"
if [ -f controller/deploy/operator/internal/controller/jumpstarter/router_metrics_bind_test.go ]; then
awk '{printf "%5d\t%s\n", NR, $0}' controller/deploy/operator/internal/controller/jumpstarter/router_metrics_bind_test.go
fiRepository: jumpstarter-dev/jumpstarter
Length of output: 13925
Bound request timeouts on the metrics server.
controller/cmd/router/metrics.go:43 creates a zero-valued http.Server, so slow headers, reads, writes, and idle connections can remain unbounded. Add ReadHeaderTimeout, ReadTimeout, WriteTimeout, and IdleTimeout, or implement a timeout wrapper before srv.Serve(ln).
🧰 Tools
🪛 ast-grep (0.45.0)
[warning] 42-42: This http.Server is constructed without a ReadTimeout. Without a read timeout, a slow or malicious client can hold connections open indefinitely (e.g. a Slowloris attack), exhausting server resources and causing a denial of service. Set ReadTimeout (and ideally ReadHeaderTimeout, WriteTimeout, and IdleTimeout) on the http.Server to bound how long the server waits while reading a request.
Context: http.Server{Handler: mux}
Note: [CWE-400] Uncontrolled Resource Consumption.
(http-server-missing-read-timeout-go)
🤖 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.go` around lines 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.
Source: Linters/SAST tools
| // 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) |
There was a problem hiding this comment.
📐 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 -300Repository: 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
| 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 | ||
| } |
There was a problem hiding this comment.
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.
| go func() { | ||
| _ = srv.Serve(ln) | ||
| }() |
There was a problem hiding this comment.
go func() {
if err := srv.Serve(ln); err != nil && err != http.ErrServerClosed {
ctrl.Log.WithName("metrics").Error(err, "metrics server stopped unexpectedly")
}
}()| mux := http.NewServeMux() | ||
| mux.Handle("/metrics", promhttp.Handler()) | ||
|
|
||
| srv := &http.Server{Handler: mux} |
There was a problem hiding this comment.
Set at least ReadHeaderTimeout and IdleTimeout:
srv := &http.Server{
Handler: mux,
ReadHeaderTimeout: 10 * time.Second,
IdleTimeout: 60 * time.Second,
}There was a problem hiding this comment.
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.
| } | ||
| 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_") { |
There was a problem hiding this comment.
If go_* metrics were removed but promhttp_* remained, the test would still pass. Assert each required family independently.
GET /metricson the Jumpstarter router (Prometheus/OpenMetrics text) via--metrics-bind-address(or equivalent)./metricsreturns 200 and parseable exposition; no undocumentedjumpstarter_*router series invented in this PR.