Ctrl K
ComunidadGuías…

backend/tests/test_http_request_observability.py

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
"""Every HTTP request must leave a trace — a metric with a status, and a log line.

Found 2026-09-03 while stress-testing RC: Sublarr recorded no HTTP requests at
all. `record_http_request` existed, fully defined with a status label, but
nothing called it, and the gunicorn line carried no `--access-logfile`. A
clean 4xx answer therefore left no trace anywhere — a whole probe campaign was
invisible from the inside, and so would any user's report of a bad response.

This wires the existing recorder to the request cycle. Two things it must get
right beyond "a number goes up":

- The `endpoint` label is the ROUTE TEMPLATE, not the concrete path. Labelling
  by `request.path` would mint a new time series per wanted-item id and blow up
  cardinality; `/api/v1/wanted/<id>` must collapse to one series.
- The status is the REAL response status, including 4xx and 5xx — the whole
  point is to see the answers that used to vanish.
"""

import pytest


def _counter_value(metric, labels):
    for sample_family in metric.collect():
        for sample in sample_family.samples:
            if sample.name.endswith("_total") and all(
                sample.labels.get(k) == v for k, v in labels.items()
            ):
                return sample.value
    return 0.0


@pytest.fixture
def _metrics():
    import metrics

    if not metrics.METRICS_AVAILABLE:
        pytest.skip("prometheus_client not installed")
    return metrics


class TestRequestsAreCounted:
    def test_a_request_increments_the_counter_with_its_status(self, client, _metrics):
        labels = {"method": "GET", "endpoint": "/api/v1/health", "status": "200"}
        before = _counter_value(_metrics.HTTP_REQUEST_TOTAL, labels)

        assert client.get("/api/v1/health").status_code == 200

        after = _counter_value(_metrics.HTTP_REQUEST_TOTAL, labels)
        assert after == before + 1

    def test_a_4xx_is_recorded_not_dropped(self, client, _metrics):
        # The exact class of answer the outage hid: a rejected request that
        # never raised, so nothing in the pipeline saw it. It must be counted
        # with its real status. An unknown API path is caught by the SPA
        # fallback rule (/<path:path>) and returns 404 — so it has a route
        # template and lands in exactly one series rather than one-per-path.
        client.get("/api/v1/definitely-no-such-route")

        got_404 = _counter_value(
            _metrics.HTTP_REQUEST_TOTAL,
            {"method": "GET", "endpoint": "/<path:path>", "status": "404"},
        )
        assert got_404 >= 1, "a 404 was not recorded under its route template"

    def test_the_endpoint_label_is_the_route_template_not_the_path(self, client, _metrics):
        # Two different concrete paths on the same rule must share one series,
        # or the label set grows without bound. /api/v1/health takes no params,
        # so use a parametrised route if one is reachable; otherwise assert the
        # negative: the raw path never appears as a label.
        client.get("/api/v1/wanted?page=1")
        client.get("/api/v1/wanted?page=2")

        raw_path_series = _counter_value(
            _metrics.HTTP_REQUEST_TOTAL,
            {"method": "GET", "endpoint": "/api/v1/wanted?page=1", "status": "200"},
        )
        assert raw_path_series == 0, "the query string leaked into the label"

    def test_in_progress_returns_to_zero_after_a_request(self, client, _metrics):
        client.get("/api/v1/health")

        gauge = _counter_value(_metrics.HTTP_REQUESTS_IN_PROGRESS, {}) or _gauge_value(
            _metrics.HTTP_REQUESTS_IN_PROGRESS
        )
        assert gauge == 0, "a request left the in-progress gauge above zero"


def _gauge_value(metric):
    for family in metric.collect():
        for sample in family.samples:
            return sample.value
    return 0.0