Feat: WT-1718 improve webtrit components monitoring - #5
Open
Avareco wants to merge 3 commits into
Open
Conversation
Adds a /metrics endpoint in the Prometheus text format, served on its own port (METRICS_PORT, default 9568) rather than the application's, so that it is never reachable through the public ingress. Request rate, latency, status and requests in flight come from an ASGI middleware rather than the RouteWithLogging route class: the latter only wraps routes registered on the routers, so /api/health-check — declared straight on the app — would be missing. Labels stay bounded, because the handler is the route template and status codes are grouped into 2xx/3xx/4xx/5xx. A collector reports the shared httpx clients at scrape time rather than on a timer: the limit, the connections held by destination and state, and the requests queued for a free one — queued above zero means the pool is exhausted right now. Two counters record what was given up on: calls killed by REQUEST_DEADLINE, and checkouts that timed out on the pool, the latter being what Finch reports as a telemetry event on the Elixir side. The pool timeout is counted where it is already caught, next to the existing check that keeps it from tripping the disaster-recovery failover. httpcore keeps the queue, the limit and the connection origin private, so each is read defensively and a rename upstream costs the metric rather than the scrape; a test covers that path. Metrics are imported lazily in the connector so it stays importable on its own. The instrumentator is capped at 7.1.* because 8.x pulls a starlette newer than the one FastAPI is pinned to here, and a failure to start the metrics server is logged and swallowed: a port already in use must not turn into an outage. The server runs in a daemon thread, which is correct for the single uvicorn worker the image starts. Adding --workers would give each worker its own registry and a scrape would report just one of them.
…toring # Conflicts: # app/bss/adapters/portaswitch/adapter.py
…ng (WT-1718) Both this branch and WT-1774 added a tests/test_34_*.py, so merging main left two files claiming the same ordinal. The pagination test is already on main, so this one moves to 35. Strings move to double quotes, matching app/metrics.py and the prevailing style of the directory. No references to fix: the filename is not named in run-tests.sh, in the workflow, or in conftest.py.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
WT-1718: expose Prometheus metrics from the adapter
Why
In WT-1717 the instance stopped serving requests while staying healthy as far as the cluster was concerned: the connection pool to PortaBilling ran dry, requests piled up — "902 requests in / 0 out" — with zero restarts, /api/health-check still returning its constant, and not one alert firing. We heard about it from the customer.
The numbers that would have shown this are unreachable from outside the process by construction: the httpx pool is a structure in the application's own memory, and Kubernetes can only see that the container is alive.
This PR closes point 2 of the ticket.
What it adds
/metricson a dedicated port (METRICS_PORT, 9568 by default) rather than the application's, so that it is never reachable through the public ingress.Our own metrics (
app/metrics.py):httpx_pool_connections{verify,host,state}httpx_pool_max_connections{verify}PORTASWITCH_MAX_CONNECTIONS, 100 by default)httpx_pool_queued_requests{verify}httpx_pool_timeouts_total{host}request_deadline_expired_total{method}REQUEST_DEADLINEThe third row is the important one. A busy pool is not a problem — that is what a pool is for. The problem starts when the queue for it stops draining.
Saturation reads as
connections == max_connectionsandqueued_requests > 0at the same time.Both counters are deliberately threshold-free: any increment already means a live user got nothing back.
From prometheus-fastapi-instrumentator
http_requests_inprogress{handler,method}http_requests_total{handler,method,status}http_request_duration_seconds{handler}http_request_size_bytes, http_response_size_bytesThe library leaves
requests_inprogressoff by default; it is enabled explicitly here, because it is precisely the metric that was missing.How it is enabled
METRICS_PORTin the environment; left unset, the 9568 default applies. A failure to start the metrics server is logged and swallowed — a port already in use must not turn into an outage.Worth a reviewer's attention
Middleware rather than RouteWithLogging. The existing route class only wraps routes registered on the routers, and /api/health-check is declared straight on app, so it would have been missing from the metrics.
The pool is read at scrape time, not on a timer, so the values are never stale. connections hands out a copy of the list and len() on a list cannot tear, so reading from the scrape thread is safe. The numbers may be a moment behind the event loop, which does not matter for a gauge.
Every private httpcore attribute is read defensively. _max_connections, _requests, _origin, is_idle() — all through getattr with a fallback. A future version that renames them costs the metric, not the scrape. There is a test for exactly that.
Single worker. The metrics server runs in a daemon thread of this process, which is correct for the one uvicorn worker the image starts. --workers 4 would give each worker its own registry and a scrape would report just one of the four; that would need the multiprocess mode of prometheus_client. The limitation is written into the module docstring.
record_pool_timeout is called by hand from except PoolTimeout. In the Elixir components Finch emits this as a telemetry event on its own; in httpx a pool timeout is an exception, so it can only be counted where it is caught. The import is lazy, inside the function, so the connector stays importable and working without the metrics stack.
Label cardinality is bounded. handler is the route template (/user/voicemails/{message_id}), never the resolved path, and statuses are grouped into 2xx/3xx/4xx/5xx. Neither a user id nor a resource id can reach a label.
prometheus-fastapi-instrumentator is capped at 7.1.* — 8.x requires a starlette newer than the one FastAPI is pinned to here.
The branch carries a merge commit — bringing main in to pick up WT-1774. It contributes no changes of its own.
Verified
The tests are unit tests against a fake pool — no server, no network. What they exercise is the reading of the pool, because that is where all the fragility is: in another library's private attributes.
Not in this PR