Files
FabledSteward/steward/models/norms.py
T
bvandeusenandClaude Opus 5.5 177ea12c9b
CI / lint (push) Successful in 2s
CI / unit (push) Successful in 1m36s
CI / integration (push) Successful in 1m50s
CI / publish (push) Successful in 1m11s
fix(anomalies): a burst within what a metric reaches in most hours opens nothing (#6727)
#6727 "A metric that bursts now and then opens anomalies at hours whose usual
peak reads 0". usual_peak is the p90 of a slot's hourly max, so a metric that
bursts in under about 1 hour in 10 reads 0 there however often it bursts across
the day. #6723 "An anomaly opens on a burst of minutes inside the metric's usual
peak, so most rows aren't outside the norm" therefore still opened on those.

usual_anywhere is the median, across the hours of the day, of each hour's
highest peak. Height is now judged against the greater of it and the slot's
usual peak. Bursts at any hour lift the median; a job at a time of its own
raises a few hours and leaves it low, so the same level at another time of day
is still an anomaly, as System 182 rules. A held level still opens.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 19:45:16 -04:00

83 lines
4.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)
# How high the metric has gone in most hours of the day, whatever the slot.
usual_anywhere: Mapped[float] = mapped_column(Float, nullable=False, default=0.0, server_default="0")
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)