from __future__ import annotations import uuid from datetime import UTC, datetime, timedelta import pytest from sqlalchemy import create_engine, func, select from sqlalchemy.orm import Session from modelforge_api.domain.observability import ( AlertAction, AlertState, MaintenanceWindowCreate, MetricDefinition, MetricRegistry, SLOPolicyCreate, SLOState, TelemetryType, metrics, ) from modelforge_api.persistence.models import ( Accelerator, AcceleratorTelemetryLatest, AlertHistoryEvent, AlertRuleRevision, Base, CapacityAggregate, CapacitySnapshot, ComputeNode, GatewayRequest, OperationalAlert, OperationalIncident, ServiceLevelIndicator, SLOPolicyRevision, StorageRoot, StorageVolumeState, ) from modelforge_api.services.observability import ObservabilityService from modelforge_api.settings import Settings @pytest.fixture def session() -> Session: engine = create_engine("sqlite+pysqlite:///:memory:") Base.metadata.create_all(engine) with Session(engine) as value: yield value def service(session: Session) -> ObservabilityService: return ObservabilityService( session, Settings(observability_monitor_enabled=False), actor="m14-test", ) def production_node(session: Session, *, liveness: str = "offline") -> ComputeNode: node = ComputeNode( key=f"m14-node-{uuid.uuid4()}", hostname="gpu_node.example.test", display_name="GPU Node", identity_source="node_agent", status="active", inventory={}, enabled=True, production_eligible=True, lab_eligible=True, benchmark_eligible=True, liveness_state=liveness, last_heartbeat_at=datetime.now(UTC), total_ram_bytes=64 * 1024**3, ) session.add(node) session.flush() return node def test_metric_contract_rejects_unbounded_labels_and_exports_histogram() -> None: registry = MetricRegistry() registry.define( MetricDefinition( name="modelforge_test_duration_seconds", type=TelemetryType.HISTOGRAM, help="Bounded test duration", labels=("route_class",), ) ) registry.observe("modelforge_test_duration_seconds", {"route_class": "gateway"}, 0.25) output = registry.render() assert "# TYPE modelforge_test_duration_seconds histogram" in output assert 'modelforge_test_duration_seconds_bucket{le="0.25",route_class="gateway"} 1' in output assert 'modelforge_test_duration_seconds_bucket{le="+Inf",route_class="gateway"} 1' in output assert 'modelforge_test_duration_seconds_sum{route_class="gateway"} 0.25' in output with pytest.raises(ValueError, match="bounded and non-sensitive"): registry.define( MetricDefinition( name="modelforge_invalid_total", type=TelemetryType.COUNTER, help="Invalid metric", labels=("request_id",), ) ) counter = MetricRegistry() counter.define( MetricDefinition( name="modelforge_test_requests_total", type=TelemetryType.COUNTER, help="Bounded request outcomes", labels=("status",), ) ) counter.increment("modelforge_test_requests_total", {"status": "ok"}, 2) assert counter.samples("modelforge_test_requests_total") == [({"status": "ok"}, 2)] with pytest.raises(ValueError, match="requires labels"): counter.increment("modelforge_test_requests_total", {"payload": "secret"}) def test_defaults_are_typed_versioned_and_lab_has_no_error_budget(session: Session) -> None: subject = service(session) subject.ensure_defaults() assert session.scalar(select(func.count()).select_from(ServiceLevelIndicator)) == 11 assert session.scalar(select(func.count()).select_from(SLOPolicyRevision)) == 9 # 12 M14 operational rules plus the five M15 recovery rules. assert session.scalar(select(func.count()).select_from(AlertRuleRevision)) == 17 recovery_rules = { item.key: item.alert_type for item in session.scalars( select(AlertRuleRevision).where(AlertRuleRevision.key.like("recovery.%")) ) } assert recovery_rules == { "recovery.backup-stale": "BACKUP_STALE", "recovery.backup-failed": "BACKUP_FAILED", "recovery.backup-verification-failed": "BACKUP_VERIFICATION_FAILED", "recovery.restore-failed": "RESTORE_FAILED", "recovery.readiness-degraded": "RECOVERY_READINESS_DEGRADED", } original = session.scalar( select(SLOPolicyRevision).where(SLOPolicyRevision.key == "gateway.production") ) assert original is not None revised = subject.create_policy( SLOPolicyCreate( key=original.key, sli_definition_id=original.sli_definition_id, objective=0.995, rolling_window_seconds=original.rolling_window_seconds, minimum_sample_count=original.minimum_sample_count, severity=original.severity, environment="PRODUCTION", effective_from=datetime.now(UTC), rationale="A deliberate test revision preserving immutable history.", created_by="test-operator", ) ) session.refresh(original) assert revised.revision == 2 assert revised.active is True assert original.active is False assert original.fingerprint != revised.fingerprint evaluations = subject.evaluate_slos(datetime.now(UTC) + timedelta(seconds=1)) vision = next(item for item in evaluations if item.policy_key == "vision.embedding.lab") production = next(item for item in evaluations if item.policy_key == "gateway.production") assert vision.state is SLOState.INSUFFICIENT_DATA assert vision.allowed_bad is None assert vision.remaining_bad is None assert production.allowed_bad is not None def test_slo_evaluation_tracks_population_latency_and_burn_rates(session: Session) -> None: subject = service(session) subject.ensure_defaults() now = datetime.now(UTC) for index in range(20): completed = index != 19 session.add( GatewayRequest( request_id=uuid.uuid4(), capability_key="rag.embedding", capability_version=1, status="completed" if completed else "failed", priority="interactive", input_sha256=f"{index:064x}", input_count=1, total_latency_ms=1500.0 if index == 18 else (100.0 if completed else None), failure_code=None if completed else "RUNTIME_FAILED", decision_evidence={"source": "m14-test"}, created_at=now - timedelta(seconds=index), finished_at=now - timedelta(seconds=index) if completed else None, ) ) session.commit() results = subject.evaluate_slos(now + timedelta(seconds=1)) success = next( item for item in results if item.policy_key == "rag.embedding.production.success" ) latency = next( item for item in results if item.policy_key == "rag.embedding.production.latency" ) assert success.sample_count == 20 assert success.bad_count == 1 assert success.state is SLOState.BREACHED assert success.long_burn_rate == pytest.approx(5.0) assert latency.sample_count == 19 assert latency.bad_count == 1 assert latency.observed_value == pytest.approx(240.0) assert latency.short_burn_rate == pytest.approx((1 / 19) / 0.05) def test_slo_window_at_risk_stale_and_budget_recovery(session: Session) -> None: subject = service(session) subject.ensure_defaults() now = datetime.now(UTC) node = production_node(session, liveness="online") node.last_heartbeat_at = now - timedelta(seconds=100) for index in range(100): completed = index < 98 session.add( GatewayRequest( request_id=uuid.uuid4(), capability_key="other.capability", capability_version=1, status="completed" if completed else "failed", priority="interactive", input_sha256=f"{index + 1000:064x}", input_count=1, total_latency_ms=100 if completed else None, failure_code=None if completed else "CONTROLLED_FAILURE", decision_evidence={}, created_at=now - timedelta(minutes=index), ) ) session.add( GatewayRequest( request_id=uuid.uuid4(), capability_key="other.capability", capability_version=1, status="failed", priority="interactive", input_sha256="f" * 64, input_count=1, failure_code="OLD_FAILURE", decision_evidence={}, created_at=now - timedelta(days=2), ) ) session.commit() first = subject.evaluate_slos(now) gateway = next(item for item in first if item.policy_key == "gateway.production") freshness = next(item for item in first if item.policy_key == "gpu_node.production.freshness") assert gateway.state is SLOState.AT_RISK assert gateway.sample_count == 100 assert gateway.bad_count == 2 assert gateway.remaining_bad == 0 assert gateway.long_burn_rate == pytest.approx(2.0) assert freshness.state is SLOState.STALE recovered_at = now + timedelta(hours=25) node.last_heartbeat_at = recovered_at for index in range(20): session.add( GatewayRequest( request_id=uuid.uuid4(), capability_key="other.capability", capability_version=1, status="completed", priority="interactive", input_sha256=f"{index + 2000:064x}", input_count=1, total_latency_ms=100, decision_evidence={}, created_at=recovered_at - timedelta(seconds=index), ) ) session.commit() recovered = subject.evaluate_slos(recovered_at) gateway_recovered = next(item for item in recovered if item.policy_key == "gateway.production") assert gateway_recovered.state is SLOState.HEALTHY assert gateway_recovered.bad_count == 0 assert gateway_recovered.remaining_bad == pytest.approx(0.2) def test_alert_pending_firing_acknowledged_resolved_and_correlated(session: Session) -> None: subject = service(session) subject.ensure_defaults() node = production_node(session) session.commit() start = datetime.now(UTC) - timedelta(seconds=30) first = next( item for item in subject.evaluate_alerts(start) if item.alert_type == "NODE_OFFLINE" ) assert first.state is AlertState.PENDING firing = next( item for item in subject.evaluate_alerts(start + timedelta(seconds=11)) if item.id == first.id ) assert firing.state is AlertState.FIRING assert firing.occurrence_count == 2 assert session.scalar(select(func.count()).select_from(OperationalIncident)) == 1 acknowledged = subject.acknowledge( firing.id, AlertAction(actor="on-call", reason="Investigating node connectivity") ) assert acknowledged.state is AlertState.ACKNOWLEDGED node.liveness_state = "online" session.commit() resolved = next( item for item in subject.evaluate_alerts(datetime.now(UTC) + timedelta(seconds=1)) if item.id == first.id ) assert resolved.state is AlertState.RESOLVED history = list( session.scalars( select(AlertHistoryEvent) .where(AlertHistoryEvent.alert_id == first.id) .order_by(AlertHistoryEvent.occurred_at) ) ) assert [item.to_state for item in history] == [ "PENDING", "FIRING", "ACKNOWLEDGED", "RESOLVED", ] incident = session.scalar(select(OperationalIncident)) assert incident is not None and incident.state == "RESOLVED" def test_capability_rules_separate_lab_from_production(session: Session) -> None: subject = service(session) subject.ensure_defaults() production_rule = session.scalar( select(AlertRuleRevision).where(AlertRuleRevision.key == "capability.unavailable") ) lab_rule = session.scalar( select(AlertRuleRevision).where(AlertRuleRevision.key == "capability.lab-unavailable") ) assert production_rule is not None assert production_rule.condition == {"production_only": True} assert lab_rule is not None assert lab_rule.condition == {"lab_only": True} def test_overview_reports_observability_persistence_degradation(session: Session) -> None: subject = service(session) subject.ensure_defaults() metrics.gauge("modelforge_observability_degraded", {}, 1) try: assert subject.overview().status == "OBSERVABILITY_DEGRADED" finally: metrics.gauge("modelforge_observability_degraded", {}, 0) def test_maintenance_window_suppresses_matching_alert(session: Session) -> None: subject = service(session) subject.ensure_defaults() node = production_node(session) session.commit() now = datetime.now(UTC) subject.create_maintenance_window( MaintenanceWindowCreate( name="GPU Node maintenance rehearsal", starts_at=now - timedelta(minutes=1), ends_at=now + timedelta(minutes=10), matcher={"rule_key": "node.offline", "subject_ref": str(node.id)}, reason="M14 deterministic suppression verification", created_by="test-operator", ) ) alert = next(item for item in subject.evaluate_alerts(now) if item.alert_type == "NODE_OFFLINE") assert alert.state is AlertState.SUPPRESSED assert alert.suppressed_until is not None assert alert.suppressed_until.replace(tzinfo=UTC) == now + timedelta(minutes=10) assert session.scalar(select(func.count()).select_from(OperationalIncident)) == 0 def test_alert_cooldown_reuses_fingerprint_and_storage_threshold_fires( session: Session, ) -> None: subject = service(session) subject.ensure_defaults() node = production_node(session) storage = StorageRoot( compute_node_id=node.id, name="models", purpose="model_artifacts", path="D:/models", status="ready", writable=True, capacity_bytes=100 * 1024**3, free_bytes=12 * 1024**3, reserve_bytes=10 * 1024**3, reserve_percent=10, validation_details={}, ) session.add(storage) session.commit() start = datetime.now(UTC) - timedelta(seconds=30) first = subject.evaluate_alerts(start) node_alert = next(item for item in first if item.alert_type == "NODE_OFFLINE") storage_alert = next(item for item in first if item.alert_type == "STORAGE_LOW") assert storage_alert.state is AlertState.FIRING firing = next( item for item in subject.evaluate_alerts(start + timedelta(seconds=11)) if item.id == node_alert.id ) assert firing.state is AlertState.FIRING node.liveness_state = "online" storage.free_bytes = 90 * 1024**3 session.commit() resolved = next( item for item in subject.evaluate_alerts(start + timedelta(seconds=20)) if item.id == node_alert.id ) node.liveness_state = "offline" session.commit() during_cooldown = next( item for item in subject.evaluate_alerts(start + timedelta(seconds=30)) if item.id == node_alert.id ) assert during_cooldown.state is AlertState.RESOLVED recurred = next( item for item in subject.evaluate_alerts(start + timedelta(seconds=321)) if item.id == node_alert.id ) assert recurred.state is AlertState.PENDING assert recurred.fingerprint == resolved.fingerprint def test_incident_groups_evidenced_downstream_only(session: Session) -> None: subject = service(session) subject.ensure_defaults() node = production_node(session) session.commit() now = datetime.now(UTC) - timedelta(seconds=20) subject.evaluate_alerts(now) node_alert = next( item for item in subject.evaluate_alerts(now + timedelta(seconds=11)) if item.alert_type == "NODE_OFFLINE" ) capability_rule = session.scalar( select(AlertRuleRevision).where(AlertRuleRevision.key == "capability.unavailable") ) assert capability_rule is not None downstream = OperationalAlert( rule_id=capability_rule.id, fingerprint="d" * 64, alert_type="CAPABILITY_UNAVAILABLE", severity="CRITICAL", state="FIRING", source="capability_deployment", subject_type="capability", subject_ref="rag.embedding@1", summary="Stable capability cannot serve", details={}, first_seen_at=now, last_seen_at=now, firing_at=now, occurrence_count=1, ) unrelated = OperationalAlert( rule_id=capability_rule.id, fingerprint="e" * 64, alert_type="RUNTIME_CRASH_LOOP", severity="CRITICAL", state="FIRING", source="serving_jobs", subject_type="service", subject_ref="runtime", summary="Runtime failures", details={}, first_seen_at=now, last_seen_at=now, firing_at=now, occurrence_count=1, ) session.add_all((downstream, unrelated)) session.flush() subject._correlate_incident(downstream, {"details": {"node_id": str(node.id)}}, now) subject._correlate_incident(unrelated, {"details": {}}, now) session.commit() root = session.get(OperationalAlert, node_alert.id) assert root is not None assert downstream.incident_id == root.incident_id assert unrelated.incident_id is None assert session.scalar(select(func.count()).select_from(OperationalIncident)) == 1 def test_capacity_trends_forecast_refusal_and_retention(session: Session) -> None: subject = service(session) node = production_node(session, liveness="online") accelerator = Accelerator( compute_node_id=node.id, device_index=0, device_uuid="GPU-M14", name="Test GPU", total_vram_bytes=16 * 1024**3, status="active", capabilities={}, ) session.add(accelerator) session.flush() telemetry = AcceleratorTelemetryLatest( accelerator_id=accelerator.id, used_vram_bytes=4 * 1024**3, free_vram_bytes=12 * 1024**3, availability={}, observed_at=datetime.now(UTC), received_at=datetime.now(UTC), ) storage = StorageVolumeState( compute_node_id=node.id, purpose="model_artifacts", path="D:/models", total_bytes=2 * 1024**4, used_bytes=1024**4, free_bytes=1024**4, availability={}, observed_at=datetime.now(UTC), received_at=datetime.now(UTC), ) session.add_all((telemetry, storage)) session.commit() base = datetime.now(UTC) - timedelta(hours=2) for index in range(12): observed = base + timedelta(minutes=index * 10) telemetry.used_vram_bytes = (4 * 1024**3) + index * 1024**2 telemetry.received_at = observed telemetry.observed_at = observed storage.free_bytes = (1024**4) - index * 1024**3 storage.observed_at = observed storage.received_at = observed session.commit() subject.collect_capacity(observed) trend = subject.capacity_trend(node.id, hours=4) assert trend.status == "AVAILABLE" assert trend.sample_count == 12 assert trend.metrics["gpu_schedulable_p95"] is not None assert trend.metrics["external_gpu_max"] is not None assert trend.forecast["status"] == "AVAILABLE" assert trend.forecast["storage_change_bytes_per_day"] < 0 snapshots = list( session.scalars(select(CapacitySnapshot).order_by(CapacitySnapshot.observed_at)) ) for item in snapshots[1:]: session.delete(item) session.commit() refused = subject.capacity_trend(node.id, hours=4) assert refused.status == "INSUFFICIENT_DATA" assert refused.forecast == {"status": "INSUFFICIENT_DATA"} old = snapshots[0] old.observed_at = datetime.now(UTC) - timedelta(days=8) session.add(old) session.commit() retention = subject.apply_retention(datetime.now(UTC)) assert retention == { "aggregates_created": 1, "raw_deleted": 1, "aggregates_deleted": 0, } assert session.scalar(select(func.count()).select_from(CapacityAggregate)) == 1 assert session.scalar(select(func.count()).select_from(CapacitySnapshot)) == 0