Improve gateway metrics and reliability - #1
Conversation
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (26)
📝 WalkthroughWalkthroughThe change adds gateway observability metrics, Prometheus alerts and recording rules, Grafana panels, isolated benchmark routes, configurable load tests, CI quality gates, and tests for caching, configuration, rate limiting, resilience, and response observation. ChangesGateway observability and validation
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant Client
participant Gateway
participant Redis
participant Upstream
participant Prometheus
participant Grafana
Client->>Gateway: Send benchmark or API request
Gateway->>Redis: Check rate limit or cache
Redis-->>Gateway: Return dependency result
Gateway->>Upstream: Forward request
Upstream-->>Gateway: Return response or error
Gateway->>Prometheus: Expose request and dependency metrics
Prometheus->>Grafana: Supply dashboard queries and alert rules
Grafana-->>Client: Display route and SLO panels
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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.
Pull request overview
This PR strengthens the gateway’s operational reliability and observability by expanding Prometheus metrics (including dependency signals), adding CI quality gates, and refining benchmark/load-test workflows to better isolate gateway overhead and failure behavior.
Changes:
- Expanded Prometheus metrics (RED + dependency/circuit signals), added Prometheus recording rules/alerts, and updated Grafana dashboard.
- Improved reliability primitives (circuit breaker half-open behavior; rate-limiter member uniqueness across replicas) and added tests/benchmarks.
- Added CI workflow + Makefile targets for vet, race detector, and internal coverage threshold; updated k6 load tests and benchmark route config.
Reviewed changes
Copilot reviewed 26 out of 26 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| README.md | Documents expanded metrics, alerts, benchmarking methodology, and CI quality gates. |
| Makefile | Adds race/coverage/benchmark targets and load-test orchestration. |
| loadtests/upstream_failure.js | Adds circuit rejection and dropped-iteration quality gates. |
| loadtests/scale.js | Parameterizes peak rate and tightens thresholds for scale testing. |
| loadtests/rate_limit.js | Adds stronger rate-limit correctness gates and stable test keying. |
| loadtests/cache.js | Adds cache hit-rate gate and uses dedicated cache benchmark route. |
| loadtests/baseline.js | Parameterizes baseline load and targets uncached benchmark route. |
| internal/resilience/circuitbreaker.go | Restricts half-open to a single trial request. |
| internal/resilience/circuitbreaker_test.go | Adds coverage for defaults and failed half-open trial behavior. |
| internal/ratelimit/ratelimit.go | Adds per-instance entropy to ZSET members to avoid cross-replica collisions. |
| internal/ratelimit/ratelimit_test.go | Adds tests for identity key modes, conversions, and member uniqueness. |
| internal/proxy/gateway.go | Records richer request/dependency metrics; adds circuit transition + response size/status observation. |
| internal/proxy/gateway_test.go | Adds tests for new metrics, circuit behavior, retries, and a microbenchmark. |
| internal/observability/metrics.go | Introduces new metric families (in-flight, response size, upstream, Redis, circuit transitions/rejections). |
| internal/observability/metrics_test.go | Validates new metric outputs and status-class logic. |
| internal/config/config_test.go | Adds validation tests for missing/invalid required fields. |
| internal/cache/cache_test.go | Adds tests for cache key variance and header copy semantics. |
| docker-compose.yml | Mounts Prometheus rules directory for recording/alerting rules. |
| deploy/docker/prometheus/rules/gatekeeper.yml | Adds recording rules and alerts for SLO-style monitoring. |
| deploy/docker/prometheus/prometheus.yml | Enables rule file loading for Prometheus. |
| deploy/docker/nginx.conf | Configures upstream keep-alive pool to reduce connection churn under load. |
| deploy/docker/grafana/provisioning/plugins/.gitkeep | Ensures provisioning directory exists in repo. |
| deploy/docker/grafana/provisioning/alerting/.gitkeep | Ensures provisioning directory exists in repo. |
| deploy/docker/grafana/dashboards/gatekeeper.json | Updates dashboard for RED/dependency views and new metrics. |
| deploy/docker/gateway.yaml | Adds dedicated benchmark routes for uncached/cache/rate-limit isolation. |
| .github/workflows/ci.yml | Adds CI pipeline with vet, race detector, and internal coverage floor + artifact upload. |
Suppressed comments (1)
internal/proxy/gateway.go:396
RecordCircuitTransition()already callsSetCircuitState(), so callingSetCircuitState()again here is redundant and can be removed to keep circuit-state updates in one place.
before := breaker.State()
breaker.OnSuccess()
after := breaker.State()
g.metrics.RecordCircuitTransition(routeName, before, after)
g.metrics.SetCircuitState(routeName, after)
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| if !breaker.Allow() { | ||
| status = http.StatusServiceUnavailable | ||
| g.metrics.SetCircuitState(route.Name, breaker.State()) | ||
| g.metrics.RecordCircuitRejected(route.Name) | ||
| copyHeaders(w.Header(), rateHeaders) | ||
| w.Header().Set("Content-Type", "application/json") | ||
| w.WriteHeader(status) | ||
| _, _ = w.Write([]byte(`{"error":"upstream circuit open"}`)) | ||
| return | ||
| } |
| circuitRejected: prometheus.NewCounterVec(prometheus.CounterOpts{ | ||
| Name: "gatekeeper_circuit_rejected_total", | ||
| Help: "Requests rejected because a route circuit breaker is open.", | ||
| }, []string{"route"}), |
| type responseObserver struct { | ||
| http.ResponseWriter | ||
| status int | ||
| bytes int | ||
| wroteHeader bool | ||
| } | ||
|
|
||
| func (w *responseObserver) WriteHeader(status int) { | ||
| if w.wroteHeader { | ||
| return | ||
| } | ||
| w.wroteHeader = true | ||
| w.status = status | ||
| w.ResponseWriter.WriteHeader(status) | ||
| } | ||
|
|
||
| func (w *responseObserver) Write(body []byte) (int, error) { | ||
| if !w.wroteHeader { | ||
| w.WriteHeader(http.StatusOK) | ||
| } | ||
| n, err := w.ResponseWriter.Write(body) | ||
| w.bytes += n | ||
| return n, err | ||
| } |
| | `gatekeeper_retries_total` | Retry attempts by route and reason | | ||
| | `gatekeeper_circuit_state` | One-hot closed, open, and half-open state | | ||
| | `gatekeeper_circuit_transitions_total` | Circuit state changes | | ||
| | `gatekeeper_circuit_rejected_total` | Requests rejected by open circuits | |
| before := breaker.State() | ||
| breaker.OnFailure() | ||
| g.metrics.SetCircuitState(routeName, breaker.State()) | ||
| after := breaker.State() | ||
| g.metrics.RecordCircuitTransition(routeName, before, after) | ||
| g.metrics.SetCircuitState(routeName, after) |
Summary by CodeRabbit
New Features
Bug Fixes
Quality