From 712bec3fea9fc82aae74d1e3b894b10e8a1cbee8 Mon Sep 17 00:00:00 2001 From: nessi Date: Thu, 12 Feb 2026 13:39:57 +0100 Subject: [PATCH] Add support for pg_stat_statements configuration in Targets This commit introduces a `use_pg_stat_statements` flag for targets, allowing users to enable or disable the use of `pg_stat_statements` for query insights. It includes database schema changes, backend logic, and UI updates to manage this setting in both creation and editing workflows. --- .../0003_target_pg_stat_statements_flag.py | 26 ++++ backend/app/api/routes/targets.py | 6 + backend/app/models/models.py | 1 + backend/app/schemas/target.py | 2 + backend/app/services/collector.py | 25 ++-- frontend/src/pages/QueryInsightsPage.jsx | 14 +- frontend/src/pages/TargetsPage.jsx | 141 ++++++++++++++++++ frontend/src/styles.css | 15 ++ 8 files changed, 215 insertions(+), 15 deletions(-) create mode 100644 backend/alembic/versions/0003_target_pg_stat_statements_flag.py diff --git a/backend/alembic/versions/0003_target_pg_stat_statements_flag.py b/backend/alembic/versions/0003_target_pg_stat_statements_flag.py new file mode 100644 index 0000000..845c581 --- /dev/null +++ b/backend/alembic/versions/0003_target_pg_stat_statements_flag.py @@ -0,0 +1,26 @@ +"""add target pg_stat_statements flag + +Revision ID: 0003_target_pg_stat_statements_flag +Revises: 0002_alert_definitions +Create Date: 2026-02-12 +""" + +from alembic import op +import sqlalchemy as sa + + +revision = "0003_target_pg_stat_statements_flag" +down_revision = "0002_alert_definitions" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + op.add_column( + "targets", + sa.Column("use_pg_stat_statements", sa.Boolean(), nullable=False, server_default=sa.text("true")), + ) + + +def downgrade() -> None: + op.drop_column("targets", "use_pg_stat_statements") diff --git a/backend/app/api/routes/targets.py b/backend/app/api/routes/targets.py index cb8c068..25c3a0c 100644 --- a/backend/app/api/routes/targets.py +++ b/backend/app/api/routes/targets.py @@ -64,6 +64,7 @@ async def create_target( username=payload.username, encrypted_password=encrypt_secret(payload.password), sslmode=payload.sslmode, + use_pg_stat_statements=payload.use_pg_stat_statements, tags=payload.tags, ) db.add(target) @@ -188,6 +189,11 @@ async def get_activity(target_id: int, user: User = Depends(get_current_user), d @router.get("/{target_id}/top-queries", response_model=list[QueryStatOut]) async def get_top_queries(target_id: int, user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db)) -> list[QueryStatOut]: _ = user + target = await db.scalar(select(Target).where(Target.id == target_id)) + if not target: + raise HTTPException(status_code=404, detail="Target not found") + if not target.use_pg_stat_statements: + return [] rows = ( await db.scalars( select(QueryStat) diff --git a/backend/app/models/models.py b/backend/app/models/models.py index 3542939..b6e5a89 100644 --- a/backend/app/models/models.py +++ b/backend/app/models/models.py @@ -27,6 +27,7 @@ class Target(Base): username: Mapped[str] = mapped_column(String(120), nullable=False) encrypted_password: Mapped[str] = mapped_column(Text, nullable=False) sslmode: Mapped[str] = mapped_column(String(20), nullable=False, default="prefer") + use_pg_stat_statements: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True) tags: Mapped[dict] = mapped_column(JSON, nullable=False, default=dict) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), nullable=False) diff --git a/backend/app/schemas/target.py b/backend/app/schemas/target.py index 8601b13..e29369f 100644 --- a/backend/app/schemas/target.py +++ b/backend/app/schemas/target.py @@ -9,6 +9,7 @@ class TargetBase(BaseModel): dbname: str username: str sslmode: str = "prefer" + use_pg_stat_statements: bool = True tags: dict = Field(default_factory=dict) @@ -33,6 +34,7 @@ class TargetUpdate(BaseModel): username: str | None = None password: str | None = None sslmode: str | None = None + use_pg_stat_statements: bool | None = None tags: dict | None = None diff --git a/backend/app/services/collector.py b/backend/app/services/collector.py index e65442c..8a8eb86 100644 --- a/backend/app/services/collector.py +++ b/backend/app/services/collector.py @@ -106,18 +106,19 @@ async def collect_target(target: Target) -> None: cache_hit_ratio = stat_db["blks_hit"] / (stat_db["blks_hit"] + stat_db["blks_read"]) query_rows = [] - try: - query_rows = await conn.fetch( - """ - SELECT queryid::text, calls, total_exec_time, mean_exec_time, rows, left(query, 2000) AS query_text - FROM pg_stat_statements - ORDER BY total_exec_time DESC - LIMIT 20 - """ - ) - except Exception: - # Extension may be disabled on monitored instance. - query_rows = [] + if target.use_pg_stat_statements: + try: + query_rows = await conn.fetch( + """ + SELECT queryid::text, calls, total_exec_time, mean_exec_time, rows, left(query, 2000) AS query_text + FROM pg_stat_statements + ORDER BY total_exec_time DESC + LIMIT 20 + """ + ) + except Exception: + # Extension may be disabled on monitored instance. + query_rows = [] async with SessionLocal() as db: await _store_metric(db, target.id, "connections_total", activity["total_connections"], {}) diff --git a/frontend/src/pages/QueryInsightsPage.jsx b/frontend/src/pages/QueryInsightsPage.jsx index 4cc1333..ac2c04b 100644 --- a/frontend/src/pages/QueryInsightsPage.jsx +++ b/frontend/src/pages/QueryInsightsPage.jsx @@ -75,8 +75,10 @@ export function QueryInsightsPage() { (async () => { try { const t = await apiFetch("/targets", {}, tokens, refresh); - setTargets(t); - if (t.length > 0) setTargetId(String(t[0].id)); + const supported = t.filter((item) => item.use_pg_stat_statements !== false); + setTargets(supported); + if (supported.length > 0) setTargetId(String(supported[0].id)); + else setTargetId(""); } catch (e) { setError(String(e.message || e)); } finally { @@ -133,11 +135,17 @@ export function QueryInsightsPage() {

Query Insights

Note: This section requires the pg_stat_statements extension on the monitored target.

+ {targets.length === 0 && !loading && ( +
+ No targets with enabled pg_stat_statements are available. + Enable it in Targets Management for a target to use Query Insights. +
+ )} {error &&
{error}
}
- setTargetId(e.target.value)} disabled={!targets.length}> {targets.map((t) => (