Files
FabledSteward/plugins/prometheus/migrations/versions/prometheus_001_targets.py
T
bvandeusenandClaude Opus 5.5 3cb2a95054
CI / lint (push) Successful in 2s
CI / integration (push) Failing after 1m12s
CI / unit (push) Failing after 1m27s
CI / publish (push) Skipped
feat(prometheus): scrape any /metrics endpoint into Steward's metrics, with pages (#5668, #5669)
A bundled prometheus plugin, on by default like the other protocol plugins.
Targets are rows edited on its page: address, bearer token (encrypted),
interval, keep/leave-out name patterns and a series cap. Steward does the
filtering; the endpoint serves everything (Archives' ruling).

- A 5 s tick scrapes due targets at once, each within a 30 s deadline, and
  writes the outcome to its row and its series through record_metrics, so
  alert rules can watch any of them.
- Gauges are stored as they are; counters (and a histogram's or summary's
  _sum/_count) as <name>_per_sec rates from the second scrape. Buckets and
  quantiles are skipped. A labelled series is stored under
  "<target>:k=v,…". Each target also records is_up, scrape_ms, series_kept
  and series_capped.
- Pages: targets not answering first, with why; a target's series grouped
  by metric with their newest values; a chart per series; the editor with
  Test, which scrapes once and says what would be stored.
- The counter-to-rate keeper moves to steward/core/counter_rates.py; SNMP's
  RateKeeper now scales its rate on top of it.
- metric_value reads Prometheus's _seconds as a span and _per_sec as a rate.
- The catalog guard treats an empty list as a dynamic source (snmp,
  prometheus), as the rule form does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 07:04:01 -04:00

47 lines
2.0 KiB
Python

"""Prometheus scrape targets are rows edited on the Prometheus page
Each row is a /metrics endpoint: where it is, its bearer token (encrypted),
how often to scrape it, which series to keep, and the newest scrape's
outcome (milestone 506).
Revision ID: prometheus_001_targets
Revises: (none — a branch off core via depends_on)
Create Date: 2026-10-12
"""
from typing import Sequence, Union
import sqlalchemy as sa
from alembic import op
revision: str = "prometheus_001_targets"
down_revision: Union[str, None] = None
branch_labels: Union[str, Sequence[str], None] = "prometheus"
depends_on: Union[str, Sequence[str], None] = ("0004_app_settings",)
def upgrade() -> None:
op.create_table(
"prometheus_targets",
sa.Column("id", sa.String(36), primary_key=True),
sa.Column("name", sa.String(100), nullable=False, unique=True),
sa.Column("url", sa.String(500), nullable=False),
sa.Column("token", sa.Text, nullable=False, server_default=""),
sa.Column("verify_tls", sa.Boolean, nullable=False, server_default=sa.true()),
sa.Column("interval_seconds", sa.Integer, nullable=False, server_default="60"),
sa.Column("keep", sa.JSON, nullable=False, server_default="[]"),
sa.Column("drop", sa.JSON, nullable=False, server_default="[]"),
sa.Column("series_cap", sa.Integer, nullable=False, server_default="1000"),
sa.Column("enabled", sa.Boolean, nullable=False, server_default=sa.true()),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("last_scraped_at", sa.DateTime(timezone=True), nullable=True),
sa.Column("last_ok_at", sa.DateTime(timezone=True), nullable=True),
sa.Column("last_error", sa.Text, nullable=True),
sa.Column("last_kept", sa.Integer, nullable=False, server_default="0"),
sa.Column("last_dropped", sa.Integer, nullable=False, server_default="0"),
sa.Column("last_capped", sa.Integer, nullable=False, server_default="0"),
)
def downgrade() -> None:
op.drop_table("prometheus_targets")