111 lines
4.2 KiB
Python
111 lines
4.2 KiB
Python
from __future__ import annotations
|
|
|
|
import os
|
|
from unittest import TestCase
|
|
from unittest.mock import patch
|
|
|
|
from fastapi import APIRouter
|
|
from fastapi.testclient import TestClient
|
|
from sqlalchemy import text
|
|
|
|
from govoplan_core.core.registry import PlatformRegistry
|
|
from govoplan_core.db.query_metrics import collect_query_metrics
|
|
from govoplan_core.db.session import create_database_engine
|
|
from govoplan_core.server.fastapi import create_govoplan_app
|
|
|
|
|
|
class QueryMetricsTests(TestCase):
|
|
def test_non_sqlite_engine_uses_bounded_pool_configuration(self) -> None:
|
|
configured = {
|
|
"GOVOPLAN_DB_POOL_SIZE": "8",
|
|
"GOVOPLAN_DB_MAX_OVERFLOW": "16",
|
|
"GOVOPLAN_DB_POOL_TIMEOUT_SECONDS": "45",
|
|
"GOVOPLAN_DB_POOL_RECYCLE_SECONDS": "900",
|
|
}
|
|
engine = object()
|
|
with (
|
|
patch.dict(os.environ, configured, clear=False),
|
|
patch(
|
|
"govoplan_core.db.session.create_engine",
|
|
return_value=engine,
|
|
) as create_engine,
|
|
patch(
|
|
"govoplan_core.db.session.instrument_engine",
|
|
side_effect=lambda value: value,
|
|
),
|
|
):
|
|
result = create_database_engine(
|
|
"postgresql+psycopg://govoplan@example/govoplan",
|
|
)
|
|
|
|
self.assertIs(engine, result)
|
|
create_engine.assert_called_once_with(
|
|
"postgresql+psycopg://govoplan@example/govoplan",
|
|
pool_pre_ping=True,
|
|
connect_args={},
|
|
pool_size=8,
|
|
max_overflow=16,
|
|
pool_timeout=45,
|
|
pool_recycle=900,
|
|
)
|
|
|
|
def test_non_sqlite_engine_rejects_pool_values_outside_safe_bounds(self) -> None:
|
|
invalid_values = (
|
|
("GOVOPLAN_DB_POOL_SIZE", "0"),
|
|
("GOVOPLAN_DB_MAX_OVERFLOW", "201"),
|
|
("GOVOPLAN_DB_POOL_TIMEOUT_SECONDS", "not-a-number"),
|
|
("GOVOPLAN_DB_POOL_RECYCLE_SECONDS", "86401"),
|
|
)
|
|
for name, value in invalid_values:
|
|
with self.subTest(name=name, value=value):
|
|
with patch.dict(os.environ, {name: value}, clear=False):
|
|
with self.assertRaisesRegex(ValueError, name):
|
|
create_database_engine(
|
|
"postgresql+psycopg://govoplan@example/govoplan",
|
|
)
|
|
|
|
def test_collect_query_metrics_counts_instrumented_engine_queries(self) -> None:
|
|
engine = create_database_engine("sqlite:///:memory:")
|
|
try:
|
|
with collect_query_metrics() as metrics:
|
|
with engine.connect() as connection:
|
|
self.assertEqual(1, connection.execute(text("select 1")).scalar_one())
|
|
|
|
self.assertEqual(1, metrics.query_count)
|
|
self.assertGreaterEqual(metrics.total_ms, 0.0)
|
|
self.assertGreaterEqual(metrics.slowest_ms, 0.0)
|
|
self.assertEqual(0, metrics.error_count)
|
|
finally:
|
|
engine.dispose()
|
|
|
|
def test_slow_request_log_includes_query_metrics(self) -> None:
|
|
engine = create_database_engine("sqlite:///:memory:")
|
|
router = APIRouter()
|
|
|
|
@router.get("/query")
|
|
def query_route() -> dict[str, bool]:
|
|
with engine.connect() as connection:
|
|
connection.execute(text("select 1")).scalar_one()
|
|
return {"ok": True}
|
|
|
|
try:
|
|
with patch.dict(os.environ, {"GOVOPLAN_SLOW_REQUEST_MS": "0.001"}):
|
|
app = create_govoplan_app(
|
|
title="query metrics test",
|
|
version="0",
|
|
registry=PlatformRegistry(),
|
|
api_router=router,
|
|
)
|
|
|
|
with TestClient(app) as client, self.assertLogs("govoplan.request", level="WARNING") as logs:
|
|
response = client.get("/query")
|
|
|
|
self.assertEqual(200, response.status_code, response.text)
|
|
output = "\n".join(logs.output)
|
|
self.assertIn("db_query_count=1", output)
|
|
self.assertIn("db_time_ms=", output)
|
|
self.assertIn("db_slowest_ms=", output)
|
|
self.assertIn("db_error_count=0", output)
|
|
finally:
|
|
engine.dispose()
|