135 lines
4.6 KiB
Python
135 lines
4.6 KiB
Python
"""Health-evidence selection policy.
|
|
|
|
The policy is deliberately small and pure. Storage retains every provider
|
|
claim; this module decides which *current component-scoped* claim can answer
|
|
the direct-health question. It keeps aggregate AppOps health useful in the
|
|
inspector without ever treating it as a component probe.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from datetime import UTC, datetime, timedelta
|
|
|
|
from .models import (
|
|
EvidenceFreshness,
|
|
EvidenceProvider,
|
|
EvidenceScope,
|
|
HealthEvidence,
|
|
HealthResolution,
|
|
HealthResolutionReason,
|
|
HealthStatus,
|
|
)
|
|
|
|
_PROVIDER_PRIORITY = {
|
|
EvidenceProvider.MANUAL: 100,
|
|
EvidenceProvider.PROBE: 90,
|
|
EvidenceProvider.DOCKER: 80,
|
|
EvidenceProvider.APPOPS: 40,
|
|
EvidenceProvider.RUNTIME: 20,
|
|
}
|
|
|
|
|
|
def appops_health_status(value: str | None) -> HealthStatus | None:
|
|
"""Keep AppOps aggregate health scoped to the application it describes."""
|
|
|
|
return {
|
|
"unhealthy": HealthStatus.DOWN,
|
|
"down": HealthStatus.DOWN,
|
|
"warning": HealthStatus.DEGRADED,
|
|
"degraded": HealthStatus.DEGRADED,
|
|
"healthy": HealthStatus.HEALTHY,
|
|
}.get((value or "").casefold())
|
|
|
|
|
|
def utc_now() -> datetime:
|
|
return datetime.now(UTC)
|
|
|
|
|
|
def freshness_for(evidence: HealthEvidence, *, now: datetime | None = None) -> EvidenceFreshness:
|
|
"""Return freshness without mutating the persisted observation.
|
|
|
|
The bounded stale window is solely explanatory: stale and expired claims
|
|
are both excluded from health selection. Showing the distinction helps an
|
|
operator understand whether a provider is merely late or genuinely old.
|
|
"""
|
|
|
|
now = now or utc_now()
|
|
if evidence.valid_until >= now:
|
|
return EvidenceFreshness.FRESH
|
|
ttl = max(evidence.valid_until - evidence.observed_at, timedelta(seconds=1))
|
|
return (
|
|
EvidenceFreshness.STALE if evidence.valid_until + ttl >= now else EvidenceFreshness.EXPIRED
|
|
)
|
|
|
|
|
|
def with_freshness(evidence: HealthEvidence, *, now: datetime | None = None) -> HealthEvidence:
|
|
return evidence.model_copy(update={"freshness": freshness_for(evidence, now=now)})
|
|
|
|
|
|
def resolve_health(
|
|
evidence: list[HealthEvidence], *, now: datetime | None = None
|
|
) -> HealthResolution:
|
|
"""Resolve current direct health from independently persisted evidence."""
|
|
|
|
now = now or utc_now()
|
|
annotated = [with_freshness(item, now=now) for item in evidence]
|
|
eligible = [
|
|
item
|
|
for item in annotated
|
|
if item.active
|
|
and item.scope == EvidenceScope.COMPONENT
|
|
and item.freshness == EvidenceFreshness.FRESH
|
|
]
|
|
if not eligible:
|
|
component_claims = [
|
|
item for item in annotated if item.scope == EvidenceScope.COMPONENT and item.active
|
|
]
|
|
app_claims = [
|
|
item for item in annotated if item.scope == EvidenceScope.APPLICATION and item.active
|
|
]
|
|
if component_claims:
|
|
reason_code = (
|
|
HealthResolutionReason.STALE_EVIDENCE
|
|
if any(item.freshness == EvidenceFreshness.STALE for item in component_claims)
|
|
else HealthResolutionReason.EXPIRED_EVIDENCE
|
|
)
|
|
elif app_claims:
|
|
reason_code = HealthResolutionReason.APPLICATION_SCOPED_ONLY
|
|
else:
|
|
reason_code = HealthResolutionReason.NO_CURRENT_COMPONENT_EVIDENCE
|
|
explanation = (
|
|
"Only application-scoped evidence is current; it is shown for provenance and is not "
|
|
"used as component health."
|
|
if app_claims
|
|
else "No current component-scoped health evidence is available."
|
|
)
|
|
return HealthResolution(reason_code=reason_code, explanation=explanation, evidence=annotated)
|
|
|
|
ordered = sorted(
|
|
eligible,
|
|
key=lambda item: (
|
|
_PROVIDER_PRIORITY[item.provider],
|
|
item.confidence,
|
|
item.observed_at,
|
|
item.received_at,
|
|
item.id,
|
|
),
|
|
reverse=True,
|
|
)
|
|
selected = ordered[0]
|
|
conflict = len({item.status for item in eligible}) > 1
|
|
scope_note = " Conflicting current component signals are retained below." if conflict else ""
|
|
return HealthResolution(
|
|
status=selected.status,
|
|
selected_evidence_id=selected.id,
|
|
selected_provider=selected.provider,
|
|
confidence=selected.confidence,
|
|
reason_code=HealthResolutionReason.SELECTED_COMPONENT_EVIDENCE,
|
|
explanation=(
|
|
f"{selected.provider.value} selected from current component-scoped evidence "
|
|
f"(confidence {selected.confidence:.2f}).{scope_note}"
|
|
),
|
|
conflict=conflict,
|
|
evidence=annotated,
|
|
)
|