0478ee7568
Tracks end-to-end response time and request counts (by stream mode and result) via prometheus_client. Exposes GET /metrics behind a bearer token (METRICS_TOKEN env var) so Prometheus can scrape it without exposing raw data publicly. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
110 lines
3.8 KiB
Python
110 lines
3.8 KiB
Python
"""Tests for the /metrics endpoint (token gate) and the metrics helpers."""
|
|
|
|
import os
|
|
import time
|
|
from unittest.mock import patch
|
|
|
|
import pytest
|
|
from quart import Quart
|
|
|
|
import app as app_module
|
|
from metrics import CHAT_REQUESTS_TOTAL, render_metrics, time_response
|
|
|
|
|
|
def _make_app() -> Quart:
|
|
"""Build a minimal Quart app exposing only the production /metrics handler.
|
|
|
|
Reuses the real handler from ``app.py`` but registers it on a fresh app
|
|
without the full app's ``@app.while_serving`` lifespan, so no Tortoise DB
|
|
connection is attempted and the route can be tested hermetically.
|
|
"""
|
|
minimal = Quart(__name__)
|
|
minimal.add_url_rule(
|
|
"/metrics",
|
|
view_func=app_module.app.view_functions["metrics_endpoint"],
|
|
methods=["GET"],
|
|
)
|
|
return minimal
|
|
|
|
|
|
def _counter_value(stream: str, result: str) -> float:
|
|
return CHAT_REQUESTS_TOTAL.labels(stream=stream, result=result)._value.get()
|
|
|
|
|
|
class TestMetricsEndpoint:
|
|
async def test_403_when_token_unset(self):
|
|
app = _make_app()
|
|
with patch.dict(os.environ, {}, clear=True):
|
|
async with app.test_client() as client:
|
|
resp = await client.get("/metrics")
|
|
assert resp.status_code == 403
|
|
|
|
async def test_403_when_token_empty(self):
|
|
app = _make_app()
|
|
with patch.dict(os.environ, {"METRICS_TOKEN": ""}):
|
|
async with app.test_client() as client:
|
|
resp = await client.get("/metrics")
|
|
assert resp.status_code == 403
|
|
|
|
async def test_403_wrong_token(self):
|
|
app = _make_app()
|
|
with patch.dict(os.environ, {"METRICS_TOKEN": "expected-secret"}):
|
|
async with app.test_client() as client:
|
|
resp = await client.get(
|
|
"/metrics",
|
|
headers={"Authorization": "Bearer wrong-secret"},
|
|
)
|
|
assert resp.status_code == 403
|
|
|
|
async def test_403_missing_auth_header(self):
|
|
app = _make_app()
|
|
with patch.dict(os.environ, {"METRICS_TOKEN": "expected-secret"}):
|
|
async with app.test_client() as client:
|
|
resp = await client.get("/metrics")
|
|
assert resp.status_code == 403
|
|
|
|
async def test_200_correct_token_and_metric_present(self):
|
|
app = _make_app()
|
|
with patch.dict(os.environ, {"METRICS_TOKEN": "expected-secret"}):
|
|
with time_response(stream=False):
|
|
time.sleep(0.001)
|
|
async with app.test_client() as client:
|
|
resp = await client.get(
|
|
"/metrics",
|
|
headers={"Authorization": "Bearer expected-secret"},
|
|
)
|
|
assert resp.status_code == 200
|
|
assert "text/plain" in resp.content_type
|
|
body = await resp.get_data(as_text=True)
|
|
assert "raggr_chat_response_seconds" in body
|
|
|
|
|
|
class TestTimeResponse:
|
|
def test_counts_success(self):
|
|
before = _counter_value("false", "success")
|
|
with time_response(stream=False):
|
|
pass
|
|
after = _counter_value("false", "success")
|
|
assert after == before + 1
|
|
|
|
def test_counts_error_on_exception(self):
|
|
before = _counter_value("false", "error")
|
|
with pytest.raises(RuntimeError):
|
|
with time_response(stream=False):
|
|
raise RuntimeError("boom")
|
|
after = _counter_value("false", "error")
|
|
assert after == before + 1
|
|
|
|
def test_observes_histogram_value(self):
|
|
with time_response(stream=False):
|
|
time.sleep(0.001)
|
|
out = render_metrics().decode()
|
|
assert "raggr_chat_response_seconds" in out
|
|
|
|
def test_stream_labels_distinct(self):
|
|
before = _counter_value("true", "success")
|
|
with time_response(stream=True):
|
|
pass
|
|
after = _counter_value("true", "success")
|
|
assert after == before + 1
|