Each task maps to a dedicated branch. Work items are ordered by priority — hard blockers first, then missing-but-manageable gaps.
Branch: feat/gateway-upstream-lb
Problem:
GatewayProxy::upstream_peer always calls servers.first(). If that server is down every request to that route fails. There is no distribution or failover within a gateway upstream group.
Work items:
- Replace the
HashMap<String, Vec<String>>upstream map inGatewayProxywithHashMap<String, Arc<LoadBalancer<RoundRobin>>>(same asLbProxy) - Build a
LoadBalancerper upstream group inGatewayProxy::new, reusing the health check setup fromlb.rs - Call
lb.select(b"", 256)inupstream_peerinstead ofservers.first() - Start a background health check task for each upstream group
- Update config examples and tests
Branch: feat/request-timeouts
Problem:
RouteConfig.timeout_secs is parsed and stored but never passed to Pingora. Slow upstreams hold connections open indefinitely.
Work items:
- Wire
timeout_secsfromRouteConfiginto Pingora's upstream connection options viaupstream_connect_timeoutandupstream_read_timeoutin theProxyHttptrait - Add a global
server.upstream_timeout_secsfallback inServerConfigfor modes without per-route config - Apply the fallback timeout in
LbProxyas well - Document the timeout fields in
GUIDE.md
Branch: feat/control-api-auth
Problem:
api_key is stored in ControlApiConfig but never checked. Any client that can reach port 8485 can dump the full config — including the JWT secret and API key — via GET /api/v1/config.
Work items:
- Add an axum middleware layer that reads the
Authorization: Bearer <key>header on every request - Return
401 Unauthorizedif the key is missing or does not matchcontrol_api.api_key - Skip auth check when
api_keyisNone(opt-in, useful for local dev) - Redact sensitive fields (
jwt_secret,api_key) from the/api/v1/configresponse - Add integration test for auth enforcement
Branch: fix/strip-prefix-unwrap
Problem:
upstream_request_filter in gateway.rs calls regex::Regex::new(&route.path_pattern).unwrap() on every request where strip_prefix: true. A malformed pattern stored in memory panics the Pingora worker thread.
Work items:
- Pre-compile and cache a second regex in
GatewayProxyspecifically for strip-prefix replacement (same pattern, compiled once at startup alongside the match regex) - Store it as
Vec<(Regex, Option<Regex>, RouteConfig)>—Nonewhenstrip_prefixis false - Remove the runtime
unwrap()entirely - Add a test with a route that uses
strip_prefix: true
Branch: feat/middleware-engine
Problem:
rate_limiting, authentication, and cors are parsed from config and referenced by route but never executed. Routes marked middlewares: [rate_limit, auth] are completely unprotected.
Work items:
- Implement a token-bucket rate limiter using
dashmapfor per-key counters - Extract the key from the header named in
RateLimitConfig.key_header(fall back to client IP) - Return
429 Too Many Requestswhen the bucket is exhausted - Respect
requests_per_minuteandburst_size
- Validate
Authorization: Bearer <token>using thejsonwebtokencrate - Skip validation for paths listed in
AuthConfig.excluded_paths - Return
401 Unauthorizedon missing or invalid token
- Inject
Access-Control-Allow-Origin,Access-Control-Allow-Methods,Access-Control-Allow-Headersresponse headers fromCorsConfig - Handle
OPTIONSpreflight requests with204 No Content
- Implement a middleware chain that runs enabled middlewares in order before
upstream_peer - Hook into
request_filterinProxyHttp
Branch: feat/tls-termination
Problem:
server.tls (cert_path, key_path) is parsed from config but never passed to Pingora's listener. The proxy only listens on plaintext HTTP.
Work items:
- Pass the TLS config to
svc.add_tls(addr, cert_path, key_path)instead ofsvc.add_tcp(addr)whenserver.tlsisSome - Validate that the cert and key files exist at startup and surface a clear
ProxyError - Update
compose.yamlnotes — in production, Traefik handles TLS termination so this is for deployments without Traefik - Add a self-signed cert generation recipe to the justfile for local TLS testing
Branch: feat/prometheus-metrics
Problem:
GET /api/v1/metrics returns a hardcoded stub. There is no instrumentation anywhere in the request path.
Work items:
- Add
prometheusandlazy_static(oronce_cell) as dependencies - Define a global
MetricsRegistryin a newsrc/metrics.rsmodule with the following counters and histograms:locci_requests_total{mode, route, upstream, status}— counterlocci_request_duration_seconds{mode, route, upstream}— histogram (buckets: 1ms, 5ms, 10ms, 25ms, 50ms, 100ms, 250ms, 500ms, 1s, 5s)locci_upstream_health{upstream, server}— gauge (1 = healthy, 0 = unhealthy)locci_active_connections{upstream}— gaugelocci_errors_total{mode, error_type}— counter
- Record start time at
request_filterentry - Increment
locci_requests_totaland observelocci_request_duration_secondsinlogging_filter(called after the response is sent) - Increment
locci_errors_totalonfail_to_proxy
- Update
locci_upstream_healthgauge whenever a health check result changes
- Replace the stub in
handlers.rswith Prometheus text-format output viaprometheus::TextEncoder - Expose on
GET /api/v1/metrics(existing route) and optionally on a dedicatedGET /metricspath
- Add
monitoring/directory with adocker-compose.monitoring.yamlthat spins up Prometheus (scraping:8485/api/v1/metrics) and Grafana - Add a pre-built Grafana dashboard JSON for the metrics above
Branch: fix/health-check-task
Problem:
lb.health_check_frequency is set on the LoadBalancer struct but LoadBalancer::update() is never called in a background loop. Health checks do not run.
Work items:
- Spawn a
pingora_core::services::background::background_servicethat callslb.update().awaiton each upstream group at the configured interval - Register the background service with the Pingora server via
server.add_service - Verify that unhealthy servers are removed from the rotation in a test with a mock upstream that returns errors
- Connect health status changes to
TASK-007metrics (locci_upstream_healthgauge)
Branch: feat/hot-reload
Problem:
Config changes require a process restart. The control API hot-reload endpoints return 501 Not Implemented.
Work items:
- Install a
SIGHUPhandler usingtokio::signal::unix - On signal: reload the config file from disk, validate it, and atomically swap the active config via
Arc<ArcSwap<UnifiedConfig>> - Rebuild affected services (gateway routes, upstream pools) without dropping existing connections — Pingora supports graceful handoff via
Server::run_once - Implement
POST /api/v1/routesandDELETE /api/v1/routes/:nameas in-memory mutations that update the swapped config - Log a clear message on successful reload and on validation failure (keep old config on error)
Branch: feat/connection-pool-config
Problem: Pingora's connection pool settings (pool size, idle timeout, connection timeout) are not exposed in the config.
Work items:
- Add
ConnectionPoolConfigtoServerConfig:server: connection_pool: max_idle: 128 idle_timeout_secs: 60 connect_timeout_secs: 10
- Wire these values into Pingora's
HttpPeeroptions in bothLbProxyandGatewayProxy - Document in
GUIDE.md
Branch: feat/structured-logging
Problem: There is no per-request logging. Debugging a live proxy means guessing which upstream received a request and why.
Work items:
- Implement
logging_filterin bothLbProxyandGatewayProxyto emit a structured log line per request:INFO request path=/users method=GET upstream=users_server server=127.0.0.1:3001 status=200 duration_ms=3 - Include a request ID (generate a UUID in
new_ctxand propagate via theCTXtype) - Forward the request ID downstream as
X-Request-Idheader - Log upstream errors with
WARNlevel including the error type fromProxyError
| Task | Branch | Status |
|---|---|---|
| TASK-001 | feat/gateway-upstream-lb |
Open |
| TASK-002 | feat/request-timeouts |
Open |
| TASK-003 | feat/control-api-auth |
Open |
| TASK-004 | fix/strip-prefix-unwrap |
Open |
| TASK-005 | feat/middleware-engine |
Open |
| TASK-006 | feat/tls-termination |
Open |
| TASK-007 | feat/prometheus-metrics |
Open |
| TASK-008 | fix/health-check-task |
Open |
| TASK-009 | feat/hot-reload |
Open |
| TASK-010 | feat/connection-pool-config |
Open |
| TASK-011 | feat/structured-logging |
Open |