From 3a54d6d71d60cca3acd8483f23b248990cc82288 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Fri, 19 Jun 2026 23:08:29 -0400 Subject: [PATCH] perf(metrics): index plugin_metrics for host/dashboard reads MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit plugin_metrics had only a PK on id, so every host-detail, full-metrics, and dashboard-widget load sequentially scanned the entire time-series table — which grows by (sources × resources × sample cadence), so the host views got slower over time (operator-reported "blocked/slow" loads). monitor_results was already indexed, so the uptime aggregation wasn't the bottleneck. Add two composite indexes matching the hot query shapes: - (source_module, resource_name, recorded_at) — history range scans, fleet/ widget queries, and the 'host:%' sub-resource prefix. - (source_module, resource_name, metric_name, recorded_at) — the latest-value- per-metric group-by/self-join. Model __table_args__ + core migration 0023. Integration lane validates the migration via `alembic upgrade head`. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_016Jg27rgypiW2efULXJDtMC --- .../versions/0023_plugin_metrics_indexes.py | 39 +++++++++++++++++++ steward/models/metrics.py | 13 ++++++- 2 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 steward/migrations/versions/0023_plugin_metrics_indexes.py diff --git a/steward/migrations/versions/0023_plugin_metrics_indexes.py b/steward/migrations/versions/0023_plugin_metrics_indexes.py new file mode 100644 index 0000000..89630c4 --- /dev/null +++ b/steward/migrations/versions/0023_plugin_metrics_indexes.py @@ -0,0 +1,39 @@ +"""Index plugin_metrics for host/dashboard reads + +plugin_metrics had only a PK on id, so every host-detail / full-metrics / +dashboard-widget query (all filter by source_module + resource_name over a +recorded_at range) sequentially scanned the whole time-series table — slower as +samples accumulate. Add the two composite indexes that match those query shapes. + +The non-concurrent CREATE INDEX takes a brief exclusive lock; it runs once at +startup migration time, acceptable for this table. + +Revision ID: 0023_plugin_metrics_indexes +Revises: 0022_unify_monitors +Create Date: 2026-06-20 +""" +from typing import Sequence, Union +from alembic import op + +revision: str = "0023_plugin_metrics_indexes" +down_revision: Union[str, None] = "0022_unify_monitors" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + op.create_index( + "ix_plugin_metrics_module_resource_recorded", + "plugin_metrics", + ["source_module", "resource_name", "recorded_at"], + ) + op.create_index( + "ix_plugin_metrics_module_resource_metric_recorded", + "plugin_metrics", + ["source_module", "resource_name", "metric_name", "recorded_at"], + ) + + +def downgrade() -> None: + op.drop_index("ix_plugin_metrics_module_resource_metric_recorded", "plugin_metrics") + op.drop_index("ix_plugin_metrics_module_resource_recorded", "plugin_metrics") diff --git a/steward/models/metrics.py b/steward/models/metrics.py index 85a8429..979b68f 100644 --- a/steward/models/metrics.py +++ b/steward/models/metrics.py @@ -1,7 +1,7 @@ from __future__ import annotations import uuid from datetime import datetime, timezone -from sqlalchemy import DateTime, Float, String +from sqlalchemy import DateTime, Float, Index, String from sqlalchemy.orm import Mapped, mapped_column from .base import Base @@ -17,3 +17,14 @@ class PluginMetric(Base): recorded_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), nullable=False, default=lambda: datetime.now(timezone.utc) ) + + # This time-series table grows by (sources × resources × sample cadence); every + # host-detail / full-metrics / dashboard-widget read filters by + # (source_module, resource_name) over a recorded_at range. Without these it's a + # full sequential scan on every load. + __table_args__ = ( + Index("ix_plugin_metrics_module_resource_recorded", + "source_module", "resource_name", "recorded_at"), + Index("ix_plugin_metrics_module_resource_metric_recorded", + "source_module", "resource_name", "metric_name", "recorded_at"), + )