Files
zeekay 1b367e2b62 chore: rebase the tree onto upstream master
The tree was a debranded snapshot of upstream taken around 2026-02-20, and
upstream has moved about five months since. This replaces it with upstream
master, mechanically re-derived: bin/debrand does the rename, and the
pipeline and squashed migrations are restored from the shipped tree.

Not a merge. The previous import replaced the tree wholesale in a single
commit, so there is no shared content history to merge against. Reproducing
the rename on the new tree is what actually converges.

What survives: .hanzo/workflows, the Dockerfiles and bin/docker* entrypoints,
compose files, LLM.md, and insights/migrations. The squashed initial
migration is load-bearing — the live database records it by name, so
upstream's ~918 individual migrations cannot come back.

Nine products are dropped as unreferenced. Twenty-one more that we do not run
are kept anyway because the core app still imports them; unpicking those is a
refactor and does not belong in a rebase.

This base does not build yet, and the reasons are not in the rename:
upstream now wants Python 3.13.13 against our 3.12.12 and Django 5.2 against
our 4.2, and the dependency manifests still name upstream packages that we
republish under our own names. Those are decisions, not renames.
2026-08-01 15:44:40 -07:00

321 lines
11 KiB
Python

"""Orchestrates a metric-symptom investigation into one structured result.
Composes the existing primitives — characterize the primary metric, check each
companion metric over the same window, classify blast radius from the movers,
pull trace exemplars from the anomaly window — into a single
`InvestigationResult`. That result is the seam between investigate and display:
the agent narrates it, the in-app explorer renders it, and the incident report
serializes it, all from the same shape.
"""
from __future__ import annotations
import datetime as dt
from insights.models import Team
from products.metrics.backend.anomaly import characterize_anomaly, dimension_magnitude
from products.metrics.backend.facade.contracts import (
CompanionMetric,
CompanionVerdict,
InvestigationChartSpec,
InvestigationEvidence,
InvestigationResult,
MetricAnomalyReport,
MetricFilter,
TraceExemplar,
)
from products.metrics.backend.metric_event_samples_query_runner import MetricEventSamplesQueryRunner
# A companion counts as "moved with the symptom" once it changes by more than
# this fraction over the window (matches the eye test of "basically flat"
# throughput vs a real shift).
COMPANION_MOVE_THRESHOLD = 0.25
# One mover dominates (-> localized cause) when its change is at least this
# multiple of the next mover's.
DOMINANT_MOVER_RATIO = 2.0
# histogram_quantile defaults to p95 when the caller names no quantile (matching
# characterize_anomaly); every other aggregation carries none.
_DEFAULT_HISTOGRAM_QUANTILE = 0.95
# Label keys that name the emitting service, used to implicate one for the
# logs/traces pivot.
_SERVICE_KEYS = ("service_name", "service.name")
# Enough exemplars to pivot from without bloating the serialized result the
# agent and incident report carry around.
_MAX_TRACE_EXEMPLARS = 5
# Newest-first fetch size; only traced samples survive, so read more than the
# cap to ride out untraced emissions interleaved in the window.
_EXEMPLAR_SAMPLE_FETCH_LIMIT = 100
def investigate(
*,
team: Team,
metric_name: str,
anomaly_from: dt.datetime,
anomaly_to: dt.datetime,
baseline_from: dt.datetime | None = None,
baseline_to: dt.datetime | None = None,
aggregation: str | None = None,
quantile: float | None = None,
filters: tuple[MetricFilter, ...] = (),
candidate_keys: tuple[str, ...] | None = None,
companions: tuple[CompanionMetric, ...] = (),
) -> InvestigationResult:
"""Investigate a metric symptom and return a structured result.
Characterizes `metric_name` over the anomaly window (vs the preceding window
by default), characterizes each companion over the SAME window to confirm or
rule it out, classifies blast radius from the movers, and builds re-runnable
chart specs. Raises `ValueError` for invalid windows (surfaced as 400 by
callers).
"""
symptom = characterize_anomaly(
team=team,
metric_name=metric_name,
anomaly_from=anomaly_from,
anomaly_to=anomaly_to,
baseline_from=baseline_from,
baseline_to=baseline_to,
aggregation=aggregation,
quantile=quantile,
filters=filters,
candidate_keys=candidate_keys,
)
companion_verdicts = tuple(
_assess_companion(
team=team,
companion=companion,
anomaly_from=anomaly_from,
anomaly_to=anomaly_to,
baseline_from=baseline_from,
baseline_to=baseline_to,
filters=filters,
)
for companion in companions
)
blast_radius = _classify_blast_radius(symptom)
service_name = _implicated_service(symptom)
evidence = InvestigationEvidence(
service_name=service_name,
trace_exemplars=_trace_exemplars(
team=team, metric_name=metric_name, anomaly_from=anomaly_from, anomaly_to=anomaly_to
),
log_filter=_log_filter(service_name, symptom) if service_name else None,
)
chart_specs = _build_chart_specs(symptom, companion_verdicts, filters, quantile)
confidence = _confidence(symptom, companion_verdicts, blast_radius)
narrative = _narrate(symptom, companion_verdicts, blast_radius)
return InvestigationResult(
metric_name=metric_name,
symptom=symptom,
blast_radius=blast_radius,
companions=companion_verdicts,
chart_specs=chart_specs,
evidence=evidence,
confidence=confidence,
narrative=narrative,
)
def _assess_companion(
*,
team: Team,
companion: CompanionMetric,
anomaly_from: dt.datetime,
anomaly_to: dt.datetime,
baseline_from: dt.datetime | None,
baseline_to: dt.datetime | None,
filters: tuple[MetricFilter, ...],
) -> CompanionVerdict:
report = characterize_anomaly(
team=team,
metric_name=companion.metric_name,
anomaly_from=anomaly_from,
anomaly_to=anomaly_to,
baseline_from=baseline_from,
baseline_to=baseline_to,
aggregation=companion.aggregation,
quantile=companion.quantile,
filters=filters,
)
moved = report.direction != "flat" and abs(report.change_ratio - 1.0) >= COMPANION_MOVE_THRESHOLD
return CompanionVerdict(
metric_name=companion.metric_name,
role=companion.role,
aggregation=report.aggregation,
direction=report.direction,
change_ratio=report.change_ratio,
moved_with_symptom=moved,
quantile=companion.quantile,
)
def _classify_blast_radius(symptom: MetricAnomalyReport) -> str:
"""One mover that dwarfs the rest is a localized culprit; several moving
together is a shared cause; no movers leaves it unknown. Movers arrive
ranked by magnitude (relative change blended with scale), so the dominance
test must use that same measure — comparing raw change_ratio here can rank
a high-magnitude mover below a tiny but explosive one and flip the verdict."""
movers = symptom.top_movers
if not movers:
return "unknown"
if len(movers) == 1:
return "localized"
top, second = dimension_magnitude(movers[0]), dimension_magnitude(movers[1])
if second == 0.0 or top >= second * DOMINANT_MOVER_RATIO:
return "localized"
return "shared"
def _trace_exemplars(
*,
team: Team,
metric_name: str,
anomaly_from: dt.datetime,
anomaly_to: dt.datetime,
) -> tuple[TraceExemplar, ...]:
"""The newest traced emissions of the symptom metric inside the anomaly
window, capped — concrete traces to pivot into at the moment things broke.
Emissions without trace context are skipped; no samples means no pivot."""
rows = MetricEventSamplesQueryRunner(
team=team,
metric_name=metric_name,
date_from=anomaly_from,
date_to=anomaly_to,
limit=_EXEMPLAR_SAMPLE_FETCH_LIMIT,
).run()
exemplars: list[TraceExemplar] = []
for row in rows:
if not row["trace_id"]:
continue
exemplars.append(
TraceExemplar(
trace_id=row["trace_id"],
span_id=row["span_id"],
timestamp=row["timestamp"],
value=row["value"],
)
)
if len(exemplars) == _MAX_TRACE_EXEMPLARS:
break
return tuple(exemplars)
def _implicated_service(symptom: MetricAnomalyReport) -> str | None:
for mover in symptom.top_movers:
if mover.key in _SERVICE_KEYS:
return mover.label
return None
def _log_filter(service_name: str, symptom: MetricAnomalyReport) -> dict[str, str]:
"""A ready-to-run query-logs filter bracketing onset (or the window start)
through the anomaly end, errors first."""
return {
"service_name": service_name,
"severity": "error",
"date_from": symptom.onset_time or symptom.anomaly_from,
"date_to": symptom.anomaly_to,
}
def _chart_quantile(aggregation: str, quantile: float | None) -> float | None:
"""The quantile a chart re-run needs to reproduce the aggregation: only
histogram_quantile uses one, defaulting to p95 when unspecified."""
if aggregation == "histogram_quantile":
return quantile if quantile is not None else _DEFAULT_HISTOGRAM_QUANTILE
return None
def _build_chart_specs(
symptom: MetricAnomalyReport,
companions: tuple[CompanionVerdict, ...],
filters: tuple[MetricFilter, ...],
symptom_quantile: float | None,
) -> tuple[InvestigationChartSpec, ...]:
"""The hero chart (the symptom) plus one per companion, all on the symptom's
window so the grids line up when rendered side by side. Each spec carries the
quantile its aggregation needs so a re-run reproduces the same line."""
specs = [
InvestigationChartSpec(
title=f"{symptom.metric_name} ({symptom.aggregation})",
metric_name=symptom.metric_name,
aggregation=symptom.aggregation,
anomaly_from=symptom.anomaly_from,
anomaly_to=symptom.anomaly_to,
filters=filters,
quantile=_chart_quantile(symptom.aggregation, symptom_quantile),
)
]
specs.extend(
InvestigationChartSpec(
title=f"{companion.role}: {companion.metric_name} ({companion.aggregation})",
metric_name=companion.metric_name,
aggregation=companion.aggregation,
anomaly_from=symptom.anomaly_from,
anomaly_to=symptom.anomaly_to,
filters=filters,
quantile=_chart_quantile(companion.aggregation, companion.quantile),
)
for companion in companions
)
return tuple(specs)
def _confidence(
symptom: MetricAnomalyReport,
companions: tuple[CompanionVerdict, ...],
blast_radius: str,
) -> str:
if symptom.direction == "flat":
return "low"
explained = any(companion.moved_with_symptom for companion in companions)
if blast_radius == "localized" or explained:
return "high"
return "medium"
def _narrate(
symptom: MetricAnomalyReport,
companions: tuple[CompanionVerdict, ...],
blast_radius: str,
) -> str:
parts: list[str] = []
if symptom.direction == "flat":
parts.append(f"{symptom.metric_name} held flat over the window — no clear anomaly.")
else:
onset = f" starting around {symptom.onset_time}" if symptom.onset_time else ""
parts.append(
f"{symptom.metric_name} {_direction_phrase(symptom)}{onset} "
f"(peak {symptom.anomaly_peak:g} vs baseline {symptom.baseline_mean:g})."
)
if blast_radius == "localized" and symptom.top_movers:
mover = symptom.top_movers[0]
parts.append(f"Localized to {mover.key}={mover.label}.")
elif blast_radius == "shared":
parts.append("Several labels moved together — a shared cause.")
for companion in companions:
if companion.moved_with_symptom:
moved = "moved with it"
elif companion.direction == "flat":
moved = "stayed flat"
else:
moved = "did not move significantly"
parts.append(f"{companion.role.capitalize()} ({companion.metric_name}) {moved}.")
return " ".join(parts)
def _direction_phrase(symptom: MetricAnomalyReport) -> str:
if symptom.direction == "down":
return f"fell to {symptom.change_ratio:.2f}x baseline"
return f"rose {symptom.change_ratio:.1f}x"