Initial curated ServiceMap source from 7a0a61232d05ba9097432abef6f7f4102a64d0b8
This commit is contained in:
commit
105251b9c8
96 files changed
+23587
No files matched your search
@@ -0,0 +1,206 @@
|
||||
"""Recurring, side-effect-bounded infrastructure health observation."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import threading
|
||||
from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
from uuid import uuid4
|
||||
|
||||
from .docker_discovery import appops_inventory, observe_container_health
|
||||
from .docker_provider import docker_provider_mode
|
||||
from .evidence import appops_health_status, utc_now
|
||||
from .models import (
|
||||
EvidenceProvider,
|
||||
EvidenceScope,
|
||||
HealthEvidence,
|
||||
HealthStatus,
|
||||
ProviderHealth,
|
||||
ProviderStatus,
|
||||
)
|
||||
from .store import Store
|
||||
|
||||
|
||||
def observation_ttl(interval_seconds: int) -> timedelta:
|
||||
"""Provider observation TTL with room for one delayed refresh cycle."""
|
||||
|
||||
return timedelta(seconds=max(150, interval_seconds * 3))
|
||||
|
||||
|
||||
def _appops_status(value: str | None) -> HealthStatus | None:
|
||||
return appops_health_status(value)
|
||||
|
||||
|
||||
def _docker_status(state: str, health: str | None) -> HealthStatus | None:
|
||||
if (health or "").casefold() in {"unhealthy", "down"} or state.casefold() in {"exited", "dead", "stopped"}:
|
||||
return HealthStatus.DOWN
|
||||
if (health or "").casefold() == "degraded" or state.casefold() == "restarting":
|
||||
return HealthStatus.DEGRADED
|
||||
if (health or "").casefold() == "healthy":
|
||||
return HealthStatus.HEALTHY
|
||||
return None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ObservationResult:
|
||||
docker_observations: int = 0
|
||||
appops_observations: int = 0
|
||||
skipped: bool = False
|
||||
|
||||
|
||||
class ObservationManager:
|
||||
"""Refresh provider evidence only; discovery and reconciliation stay manual."""
|
||||
|
||||
def __init__(self, store: Store, *, interval_seconds: int) -> None:
|
||||
self.store = store
|
||||
self.interval_seconds = interval_seconds
|
||||
self._lock = threading.Lock()
|
||||
|
||||
@staticmethod
|
||||
def _docker_host() -> str:
|
||||
return os.getenv("SERVICEMAP_DOCKER_HOST", "unix:///var/run/docker.sock")
|
||||
|
||||
@staticmethod
|
||||
def _appops_url() -> str | None:
|
||||
return os.getenv("SERVICEMAP_APPOPS_URL")
|
||||
|
||||
def refresh(self) -> ObservationResult:
|
||||
if self.store.demo_active() or not self._lock.acquire(blocking=False):
|
||||
return ObservationResult(skipped=True)
|
||||
try:
|
||||
now = utc_now()
|
||||
valid_until = now + observation_ttl(self.interval_seconds)
|
||||
services_by_ref = {
|
||||
service.source_ref: service
|
||||
for service in self.store.list_services()
|
||||
if service.source_ref
|
||||
}
|
||||
persisted_app_links = self.store.application_evidence_links(EvidenceProvider.APPOPS)
|
||||
appops = appops_inventory(self._appops_url())
|
||||
if appops.available:
|
||||
self.store.set_provider_status(
|
||||
ProviderStatus(
|
||||
provider=EvidenceProvider.APPOPS,
|
||||
status=ProviderHealth.HEALTHY,
|
||||
observed_at=now,
|
||||
valid_until=valid_until,
|
||||
detail=appops.detail,
|
||||
metadata={"mapping_count": len(appops.by_container)},
|
||||
)
|
||||
)
|
||||
else:
|
||||
self.store.set_provider_status(
|
||||
ProviderStatus(
|
||||
provider=EvidenceProvider.APPOPS,
|
||||
status=ProviderHealth.UNAVAILABLE,
|
||||
observed_at=now,
|
||||
valid_until=valid_until,
|
||||
detail=appops.detail,
|
||||
)
|
||||
)
|
||||
|
||||
linked_apps: dict[str, set[str]] = {
|
||||
app_id: set(service_ids) for app_id, service_ids in persisted_app_links.items()
|
||||
}
|
||||
docker_count = 0
|
||||
try:
|
||||
docker_observations = observe_container_health(self._docker_host())
|
||||
if (
|
||||
docker_provider_mode() == "broker"
|
||||
and not docker_observations
|
||||
and any(ref.startswith("docker:") for ref in services_by_ref)
|
||||
):
|
||||
raise ValueError("Docker broker returned no containers for configured services")
|
||||
self.store.set_provider_status(
|
||||
ProviderStatus(
|
||||
provider=EvidenceProvider.DOCKER,
|
||||
status=ProviderHealth.HEALTHY,
|
||||
observed_at=now,
|
||||
valid_until=valid_until,
|
||||
detail=f"{len(docker_observations)} containers observed",
|
||||
metadata={"container_count": len(docker_observations)},
|
||||
)
|
||||
)
|
||||
for observation in docker_observations:
|
||||
service = services_by_ref.get(observation.stable_source_ref)
|
||||
if service is None:
|
||||
continue
|
||||
docker_count += 1
|
||||
appops_info = appops.by_container.get(observation.name.casefold())
|
||||
if appops_info:
|
||||
linked_apps.setdefault(appops_info.app_id, set()).add(service.id)
|
||||
status = _docker_status(observation.state, observation.health)
|
||||
if status is None:
|
||||
continue
|
||||
self.store.record_health_evidence(
|
||||
HealthEvidence(
|
||||
id=str(uuid4()),
|
||||
subject_type="service",
|
||||
subject_id=service.id,
|
||||
service_id=service.id,
|
||||
provider=EvidenceProvider.DOCKER,
|
||||
provider_ref=observation.stable_source_ref,
|
||||
status=status,
|
||||
scope=EvidenceScope.COMPONENT,
|
||||
confidence=0.95 if observation.healthcheck_present else 0.8,
|
||||
observed_at=now,
|
||||
valid_until=valid_until,
|
||||
received_at=now,
|
||||
summary="Docker healthcheck" if observation.healthcheck_present else "Docker runtime state",
|
||||
metadata={
|
||||
"container_id": observation.container_id,
|
||||
"state": observation.state,
|
||||
"health": observation.health,
|
||||
},
|
||||
)
|
||||
)
|
||||
except (OSError, RuntimeError, ValueError) as exc:
|
||||
self.store.set_provider_status(
|
||||
ProviderStatus(
|
||||
provider=EvidenceProvider.DOCKER,
|
||||
status=ProviderHealth.UNAVAILABLE,
|
||||
observed_at=now,
|
||||
valid_until=valid_until,
|
||||
detail=str(exc)[:500],
|
||||
)
|
||||
)
|
||||
|
||||
appops_count = 0
|
||||
if appops.available:
|
||||
for app_id, service_ids in linked_apps.items():
|
||||
info = next(
|
||||
(item for item in appops.by_container.values() if item.app_id == app_id),
|
||||
None,
|
||||
)
|
||||
status = _appops_status(info.health if info else None)
|
||||
if info is None or status is None or not service_ids:
|
||||
continue
|
||||
appops_count += 1
|
||||
self.store.record_health_evidence(
|
||||
HealthEvidence(
|
||||
id=str(uuid4()),
|
||||
subject_type="application",
|
||||
subject_id=f"appops:{app_id}",
|
||||
provider=EvidenceProvider.APPOPS,
|
||||
provider_ref=app_id,
|
||||
status=status,
|
||||
scope=EvidenceScope.APPLICATION,
|
||||
confidence=info.discovery_confidence if info.discovery_confidence is not None else 0.5,
|
||||
observed_at=now,
|
||||
valid_until=valid_until,
|
||||
received_at=now,
|
||||
summary="AppOps application health; never used as component direct health",
|
||||
metadata={
|
||||
"app_id": app_id,
|
||||
"category": info.category,
|
||||
"policy_profile": info.policy_profile,
|
||||
"app_type": info.app_type,
|
||||
"discovery_confidence": info.discovery_confidence,
|
||||
},
|
||||
),
|
||||
service_ids=sorted(service_ids),
|
||||
)
|
||||
return ObservationResult(docker_observations=docker_count, appops_observations=appops_count)
|
||||
finally:
|
||||
self._lock.release()
|
||||
Reference in new issue
Block a user