from __future__ import annotations from datetime import datetime from sqlalchemy import DateTime, Float, ForeignKey, Index, Integer, SmallInteger, String, Text from sqlalchemy.orm import Mapped, mapped_column from .base import Base from steward.core.ids import time_ordered_id class MetricNorm(Base): """What one metric usually does in one time slot (#5739), learned from its own history by steward.core.norms.rebuild_norms. `kind` says what a slot is: "day" (0–23, the UTC hour of day) or "week" (0–167, Monday 00:00 UTC first). A key has slots of one kind at a time. The whole table is rebuilt each pass, so a row is never edited.""" __tablename__ = "metric_norms" source_module: Mapped[str] = mapped_column(String(64), primary_key=True) resource_name: Mapped[str] = mapped_column(String(255), primary_key=True) metric_name: Mapped[str] = mapped_column(String(128), primary_key=True) slot: Mapped[int] = mapped_column(SmallInteger, primary_key=True) kind: Mapped[str] = mapped_column(String(8), nullable=False) # The level the slot's hours usually held, and how high they usually # peaked: the 90th percentile, across past days, of each hour's p95 and max. usual_hold: Mapped[float] = mapped_column(Float, nullable=False) usual_peak: Mapped[float] = mapped_column(Float, nullable=False) days_seen: Mapped[int] = mapped_column(Integer, nullable=False) updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False) class MetricAnomaly(Base): """A metric that departed from its normal (#5740): one item to review. A departure that recurs while the item is still to review joins it as another occurrence (a MetricAnomalySpan) rather than adding a row, so the list stays one row per resource and metric. `review` is to_review until someone marks it normal (its hours are learned) or real (they are not); an ended item nobody reviewed within anomalies.review_days becomes lapsed, and is not learned either.""" __tablename__ = "metric_anomalies" id: Mapped[str] = mapped_column(String(36), primary_key=True, default=time_ordered_id) source_module: Mapped[str] = mapped_column(String(64), nullable=False) resource_name: Mapped[str] = mapped_column(String(255), nullable=False) metric_name: Mapped[str] = mapped_column(String(128), nullable=False) # The first occurrence's start, the latest's start, and the latest's end: # NULL while an occurrence is still going. started_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False) last_started_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False) ended_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) occurrences: Mapped[int] = mapped_column(Integer, nullable=False, default=1) # The highest 15-minute level any occurrence held, and the normal for the # hour it was reached in. peak_level: Mapped[float] = mapped_column(Float, nullable=False) peak_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False) usual_hold: Mapped[float] = mapped_column(Float, nullable=False) usual_peak: Mapped[float] = mapped_column(Float, nullable=False) review: Mapped[str] = mapped_column(String(12), nullable=False, default="to_review") reviewed_by: Mapped[str | None] = mapped_column(String(64), nullable=True) reviewed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) note: Mapped[str | None] = mapped_column(Text, nullable=True) __table_args__ = ( Index("ix_metric_anomalies_key", "source_module", "resource_name", "metric_name"), Index("ix_metric_anomalies_review_last", "review", "last_started_at"), ) class MetricAnomalySpan(Base): """One occurrence of an anomaly: when the metric was out of its normal. Learning skips the hours a span covers unless its anomaly was marked normal, and a resource's chart shades them.""" __tablename__ = "metric_anomaly_spans" id: Mapped[str] = mapped_column(String(36), primary_key=True, default=time_ordered_id) anomaly_id: Mapped[str] = mapped_column( String(36), ForeignKey("metric_anomalies.id", ondelete="CASCADE"), nullable=False, index=True) started_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False) ended_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) peak_level: Mapped[float] = mapped_column(Float, nullable=False)