Files
ServiceMap-Public/backend/app/models.py
T

863 lines
27 KiB
Python

from __future__ import annotations
from datetime import datetime
from enum import StrEnum
from urllib.parse import urlparse
from pydantic import AliasChoices, BaseModel, Field, model_validator
class ServiceKind(StrEnum):
HTTP = "http"
TCP = "tcp"
DNS = "dns"
TLS = "tls"
class HealthStatus(StrEnum):
UNKNOWN = "unknown"
UNMONITORED = "unmonitored"
HEALTHY = "healthy"
DEGRADED = "degraded"
DOWN = "down"
RECOVERING = "recovering"
MAINTENANCE = "maintenance"
class EvidenceProvider(StrEnum):
"""Systems that can make a health claim about an infrastructure subject."""
PROBE = "active_probe"
DOCKER = "docker"
APPOPS = "appops"
RUNTIME = "runtime"
MANUAL = "manual"
class EvidenceScope(StrEnum):
"""Whether a claim describes one component or an aggregate application."""
COMPONENT = "component"
APPLICATION = "application"
class EvidenceFreshness(StrEnum):
FRESH = "fresh"
STALE = "stale"
EXPIRED = "expired"
class HealthResolutionReason(StrEnum):
"""Machine-readable explanation for a health answer or lack of one."""
SELECTED_COMPONENT_EVIDENCE = "selected_component_evidence"
APPLICATION_SCOPED_ONLY = "application_scoped_only"
NO_CURRENT_COMPONENT_EVIDENCE = "no_current_component_evidence"
STALE_EVIDENCE = "stale_evidence"
EXPIRED_EVIDENCE = "expired_evidence"
SELECTED_APPLICATION_EVIDENCE = "selected_application_evidence"
COMPONENT_FAILURE = "component_failure"
COMPONENT_IMPACT = "component_dependency_impact"
COMPONENT_COVERAGE = "component_coverage"
NO_APPLICATION_EVIDENCE = "no_application_evidence"
class ProviderHealth(StrEnum):
HEALTHY = "healthy"
DEGRADED = "degraded"
UNAVAILABLE = "unavailable"
UNKNOWN = "unknown"
class MonitoringState(StrEnum):
"""How a service is currently observed, independently of its health."""
ACTIVE = "active_probe"
DOCKER_HEALTH = "docker_health"
APPOPS = "appops_observation"
OBSERVED = "observed"
UNMONITORED = "unmonitored"
DISABLED = "disabled"
class ProbeEndpointSource(StrEnum):
MANUAL = "manual"
EXPLICIT_LABEL = "explicit_label"
KNOWN_URL = "known_url"
APPOPS = "appops"
REVERSE_PROXY = "reverse_proxy"
PUBLISHED_PORT = "published_port"
DOCKER_HEALTHCHECK = "docker_healthcheck"
CONTAINER_INTERNAL = "container_internal"
DISCOVERED_ENVIRONMENT = "discovered_environment"
class ProbeReachability(StrEnum):
VERIFIED = "verified"
REACHABLE = "reachable"
UNVERIFIED = "unverified"
INTERNAL_ONLY = "internal_only"
NOT_CONFIGURED = "not_configured"
class Criticality(StrEnum):
LOW = "low"
MEDIUM = "medium"
HIGH = "high"
CRITICAL = "critical"
class IncidentStatus(StrEnum):
OPEN = "open"
ACKNOWLEDGED = "acknowledged"
RESOLVED = "resolved"
class DiscoveryCategory(StrEnum):
APPLICATION = "application"
DATABASE = "database"
CACHE = "cache"
PROXY = "proxy"
MONITORING = "monitoring"
DEVELOPMENT = "development"
STORAGE = "storage"
INFRASTRUCTURE = "infrastructure"
BACKGROUND_WORKER = "background_worker"
SYSTEM = "system"
UNKNOWN = "unknown"
class ComponentRole(StrEnum):
"""The operational role a component plays inside an application."""
FRONTEND = "frontend"
API = "api"
DATABASE = "database"
CACHE = "cache"
PROXY = "proxy"
WORKER = "worker"
STORAGE = "storage"
MONITORING = "monitoring"
AGENT = "agent"
SCHEDULER = "scheduler"
AUXILIARY = "auxiliary"
UNKNOWN = "unknown"
class DiscoveryVisibility(StrEnum):
VISIBLE = "visible"
HIDDEN = "hidden"
IGNORED = "ignored"
class DiscoveryLifecycle(StrEnum):
ACTIVE = "active"
MISSING = "missing"
STALE = "stale"
IGNORED = "ignored"
RETIRED = "retired"
class DiscoveryConfidence(StrEnum):
CONFIRMED = "confirmed"
HIGH = "high"
MEDIUM = "medium"
LOW = "low"
class DiscoveryRunStatus(StrEnum):
IDLE = "idle"
DISCOVERING = "discovering"
RECONCILING = "reconciling"
COMPLETED = "completed"
FAILED = "failed"
class SuggestionStatus(StrEnum):
OPEN = "open"
ACCEPTED = "accepted"
REJECTED = "rejected"
SUPERSEDED = "superseded"
class RelationType(StrEnum):
DEPENDS_ON = "depends_on"
ROUTES_TO = "routes_to"
USES_STORAGE = "uses_storage"
COMMUNICATES_WITH = "communicates_with"
class ServiceCreate(BaseModel):
name: str = Field(min_length=1, max_length=120)
kind: ServiceKind
url: str | None = None
host: str | None = Field(default=None, validation_alias=AliasChoices("host", "hostname"))
port: int | None = Field(default=None, ge=1, le=65535)
depends_on: list[str] = Field(default_factory=list)
description: str | None = Field(default=None, max_length=500)
tags: list[str] = Field(default_factory=list, max_length=20)
group: str | None = Field(default=None, max_length=80)
environment: str | None = Field(default=None, max_length=80)
owner: str | None = Field(default=None, max_length=120)
enabled: bool = True
monitoring_enabled: bool = True
monitoring_state: MonitoringState = MonitoringState.ACTIVE
probe_source: ProbeEndpointSource = ProbeEndpointSource.MANUAL
probe_reachability: ProbeReachability = ProbeReachability.VERIFIED
probe_confidence: DiscoveryConfidence = DiscoveryConfidence.CONFIRMED
runtime_state: str | None = Field(default=None, max_length=40)
runtime_health: str | None = Field(default=None, max_length=40)
application: str | None = Field(default=None, max_length=120)
domain: str | None = Field(default=None, max_length=80)
component_role: ComponentRole = ComponentRole.UNKNOWN
application_locked: bool = False
domain_locked: bool = False
component_role_locked: bool = False
criticality: Criticality = Criticality.MEDIUM
category: DiscoveryCategory = DiscoveryCategory.UNKNOWN
visibility: DiscoveryVisibility = DiscoveryVisibility.VISIBLE
lifecycle: DiscoveryLifecycle = DiscoveryLifecycle.ACTIVE
source_type: str | None = Field(default=None, max_length=40)
source_ref: str | None = Field(default=None, max_length=240)
first_seen_at: datetime | None = None
last_seen_at: datetime | None = None
last_discovery_status: str | None = Field(default=None, max_length=40)
container_image: str | None = Field(default=None, max_length=240)
compose_project: str | None = Field(default=None, max_length=120)
layout_x: float | None = None
layout_y: float | None = None
layout_pinned: bool = False
interval_seconds: int | None = Field(default=None, ge=1, le=86400)
timeout_seconds: float | None = Field(default=None, ge=0.1, le=60)
retries: int | None = Field(default=None, ge=0, le=5)
failure_threshold: int = Field(default=1, ge=1, le=20)
recovery_threshold: int = Field(default=1, ge=1, le=20)
expected_status_min: int | None = Field(default=None, ge=100, le=599)
expected_status_max: int | None = Field(default=None, ge=100, le=599)
availability_target: float | None = Field(default=None, ge=0, le=100)
maintenance_enabled: bool = False
maintenance_reason: str | None = Field(default=None, max_length=240)
@model_validator(mode="after")
def validate_endpoint(self) -> ServiceCreate:
if not self.monitoring_enabled and not self.url and not self.host and self.port is None:
return self
if self.kind == ServiceKind.HTTP and not self.url:
raise ValueError("HTTP services require url")
if self.kind == ServiceKind.HTTP:
parsed = urlparse(self.url or "")
if parsed.scheme not in {"http", "https"} or not parsed.netloc:
raise ValueError("HTTP services require an absolute http(s) url")
if self.host or self.port:
raise ValueError("HTTP services must use url instead of host/port")
if self.kind in {ServiceKind.TCP, ServiceKind.TLS} and (not self.host or self.port is None):
raise ValueError(f"{self.kind.value.upper()} services require host and port")
if self.kind == ServiceKind.DNS and not self.host:
raise ValueError("DNS services require hostname in host")
if self.kind in {ServiceKind.TCP, ServiceKind.TLS, ServiceKind.DNS} and self.url:
raise ValueError(f"{self.kind.value.upper()} services must use host/port instead of url")
if self.kind == ServiceKind.DNS and self.port is not None:
raise ValueError("DNS services use a hostname without a port")
if (
self.expected_status_min is not None
and self.expected_status_max is not None
and self.expected_status_min > self.expected_status_max
):
raise ValueError("expected_status_min cannot exceed expected_status_max")
return self
class ServiceUpdate(ServiceCreate):
pass
class Service(ServiceCreate):
id: str
created_at: datetime
updated_at: datetime
state: HealthStatus = HealthStatus.UNKNOWN
failure_streak: int = 0
recovery_streak: int = 0
flapping: bool = False
flap_count: int = 0
last_state_change_at: datetime | None = None
class ProbeResult(BaseModel):
id: str
service_id: str
status: HealthStatus
latency_ms: float | None
detail: str
checked_at: datetime
trigger: str = "manual"
metadata: dict[str, object] = Field(default_factory=dict)
class HealthEvidence(BaseModel):
"""An immutable-ish, provider-scoped claim used to resolve direct health.
Application-scoped evidence is intentionally retained for provenance, but
cannot be promoted to a component's direct health without an explicit
component-level observation from that provider.
"""
id: str
subject_type: str = Field(default="service", max_length=40)
subject_id: str = Field(max_length=240)
service_id: str | None = None
provider: EvidenceProvider
provider_ref: str | None = Field(default=None, max_length=240)
status: HealthStatus
scope: EvidenceScope = EvidenceScope.COMPONENT
confidence: float = Field(default=1.0, ge=0, le=1)
observed_at: datetime
valid_until: datetime
received_at: datetime
summary: str = Field(default="", max_length=500)
metadata: dict[str, object] = Field(default_factory=dict)
active: bool = True
superseded_by: str | None = None
freshness: EvidenceFreshness = EvidenceFreshness.FRESH
class HealthResolution(BaseModel):
status: HealthStatus = HealthStatus.UNKNOWN
selected_evidence_id: str | None = None
selected_provider: EvidenceProvider | None = None
confidence: float | None = Field(default=None, ge=0, le=1)
reason_code: HealthResolutionReason = HealthResolutionReason.NO_CURRENT_COMPONENT_EVIDENCE
explanation: str = "No current component-scoped health evidence is available."
conflict: bool = False
evidence: list[HealthEvidence] = Field(default_factory=list)
class ApplicationHealthResolution(BaseModel):
"""Aggregate health answer that keeps application and component scopes separate."""
status: HealthStatus = HealthStatus.UNKNOWN
confidence: float | None = Field(default=None, ge=0, le=1)
explanation: str = "No current application or component health evidence is available."
reason_code: HealthResolutionReason = HealthResolutionReason.NO_APPLICATION_EVIDENCE
selected_evidence_id: str | None = None
selected_provider: EvidenceProvider | None = None
evidence_freshness: EvidenceFreshness | None = None
conflict: bool = False
resolved_components: int = 0
unresolved_components: int = 0
component_coverage_percent: float = 0
class ProviderStatus(BaseModel):
provider: EvidenceProvider
status: ProviderHealth = ProviderHealth.UNKNOWN
observed_at: datetime
valid_until: datetime
detail: str = Field(default="", max_length=500)
metadata: dict[str, object] = Field(default_factory=dict)
class ServiceSnapshot(BaseModel):
service: Service
latest_probe: ProbeResult | None = None
availability_24h: AvailabilitySummary | None = None
dependency_status: HealthStatus = HealthStatus.UNKNOWN
effective_status: HealthStatus = HealthStatus.UNKNOWN
direct_status: HealthStatus = HealthStatus.UNKNOWN
flapping: bool = False
flap_count: int = 0
next_probe_at: datetime | None = None
dependents: list[str] = Field(default_factory=list)
sources: list[ServiceSource] = Field(default_factory=list)
evidence: list[RelationEvidence] = Field(default_factory=list)
health_evidence: list[HealthEvidence] = Field(default_factory=list)
health_resolution: HealthResolution = Field(default_factory=HealthResolution)
class AvailabilitySummary(BaseModel):
hours: int = 24
total_checks: int = 0
healthy_checks: int = 0
degraded_checks: int = 0
down_checks: int = 0
uptime_percent: float | None = None
average_latency_ms: float | None = None
last_checked_at: datetime | None = None
target_percent: float | None = None
meets_target: bool | None = None
class BulkProbeResult(BaseModel):
requested: int
completed: int
results: list[ProbeResult]
class IncidentCandidate(BaseModel):
service_id: str
service_name: str
status: HealthStatus
reason: str
affected_service_ids: list[str] = Field(default_factory=list)
affected_service_names: list[str] = Field(default_factory=list)
class IncidentRecord(BaseModel):
id: str
status: IncidentStatus
root_service_id: str
root_service_name: str
started_at: datetime
detected_at: datetime
acknowledged_at: datetime | None = None
resolved_at: datetime | None = None
title: str
root_cause_reason: str
affected_service_ids: list[str] = Field(default_factory=list)
affected_service_names: list[str] = Field(default_factory=list)
severity: Criticality = Criticality.MEDIUM
latest_update_at: datetime
class IncidentEvent(BaseModel):
id: str
incident_id: str
event_type: str
message: str
created_at: datetime
service_id: str | None = None
class EventRecord(BaseModel):
id: str
event_type: str
service_id: str | None = None
incident_id: str | None = None
from_status: HealthStatus | None = None
to_status: HealthStatus | None = None
message: str
metadata: dict[str, object] = Field(default_factory=dict)
created_at: datetime
class IncidentOverview(BaseModel):
generated_at: datetime
active: bool
candidates: list[IncidentCandidate]
degraded_service_ids: list[str]
down_service_ids: list[str]
incidents: list[IncidentRecord] = Field(default_factory=list)
class ServiceSource(BaseModel):
id: str
source_type: str
source_id: str
source_name: str | None = None
first_seen_at: datetime
last_seen_at: datetime
lifecycle: DiscoveryLifecycle = DiscoveryLifecycle.ACTIVE
managed: bool = True
metadata: dict[str, object] = Field(default_factory=dict)
class RelationEvidence(BaseModel):
id: str
source_service_id: str
target_service_id: str | None = None
target_ref: str | None = None
relation_type: RelationType = RelationType.DEPENDS_ON
evidence_type: str
confidence: DiscoveryConfidence
explanation: str
source_ref: str | None = None
locked: bool = False
active: bool = True
first_seen_at: datetime
last_seen_at: datetime
class LogicalRelation(BaseModel):
"""One canonical relationship with all independent supporting evidence."""
id: str
source_service_id: str
target_service_id: str | None = None
target_ref: str | None = None
relation_type: RelationType = RelationType.DEPENDS_ON
confidence: DiscoveryConfidence
active: bool = True
locked: bool = False
first_seen_at: datetime
last_seen_at: datetime
evidence: list[RelationEvidence] = Field(default_factory=list)
class SemanticComponent(BaseModel):
service_id: str
name: str
kind: ServiceKind
application_id: str
domain_id: str
role: ComponentRole = ComponentRole.UNKNOWN
direct_status: HealthStatus
effective_status: HealthStatus
unknown_reason: HealthResolutionReason | None = None
runtime_state: str | None = None
runtime_health: str | None = None
monitoring_state: MonitoringState
latest_latency_ms: float | None = None
dependency_ids: list[str] = Field(default_factory=list)
dependent_ids: list[str] = Field(default_factory=list)
root_cause: bool = False
affected: bool = False
class SemanticApplication(BaseModel):
id: str
name: str
domain_id: str
component_ids: list[str] = Field(default_factory=list)
component_count: int = 0
roles: list[ComponentRole] = Field(default_factory=list)
status: HealthStatus
status_explanation: str
health_resolution: ApplicationHealthResolution = Field(default_factory=ApplicationHealthResolution)
dependency_application_ids: list[str] = Field(default_factory=list)
dependent_application_ids: list[str] = Field(default_factory=list)
monitored_components: int = 0
unmonitored_components: int = 0
resolved_components: int = 0
unresolved_components: int = 0
component_coverage_percent: float = 0
root_cause: bool = False
affected: bool = False
class SemanticDomain(BaseModel):
id: str
name: str
application_ids: list[str] = Field(default_factory=list)
application_count: int = 0
status: HealthStatus
resolved_application_count: int = 0
unresolved_application_count: int = 0
application_coverage_percent: float = 0
incident_count: int = 0
root_cause: bool = False
affected: bool = False
class SemanticTopology(BaseModel):
generated_at: datetime
domains: list[SemanticDomain] = Field(default_factory=list)
applications: list[SemanticApplication] = Field(default_factory=list)
components: list[SemanticComponent] = Field(default_factory=list)
relations: list[LogicalRelation] = Field(default_factory=list)
monitoring_coverage: dict[str, int] = Field(default_factory=dict)
class DiscoveryObservation(BaseModel):
id: str
run_id: str
source_id: str
source_type: str
source_name: str
lifecycle: DiscoveryLifecycle
change_type: str
observed_at: datetime
metadata: dict[str, object] = Field(default_factory=dict)
class TopologySuggestion(BaseModel):
id: str
run_id: str
suggestion_type: str
status: SuggestionStatus = SuggestionStatus.OPEN
source_ref: str | None = None
source_service_id: str | None = None
target_ref: str | None = None
target_service_id: str | None = None
proposed_name: str | None = None
proposed_kind: ServiceKind | None = None
proposed_url: str | None = None
proposed_host: str | None = None
proposed_port: int | None = None
proposed_category: DiscoveryCategory = DiscoveryCategory.UNKNOWN
proposed_group: str | None = None
relation_type: RelationType | None = None
confidence: DiscoveryConfidence = DiscoveryConfidence.LOW
evidence: list[str] = Field(default_factory=list)
created_at: datetime
updated_at: datetime
class DiscoveryDiff(BaseModel):
new: int = 0
changed: int = 0
missing: int = 0
unchanged: int = 0
stale: int = 0
new_suggestions: int = 0
class DiscoveryRun(BaseModel):
id: str
status: DiscoveryRunStatus
started_at: datetime
completed_at: datetime | None = None
source_counts: dict[str, int] = Field(default_factory=dict)
diff: DiscoveryDiff = Field(default_factory=DiscoveryDiff)
error: str | None = None
class TopologySnapshot(BaseModel):
"""Compact, immutable capture of the canonical topology after a run."""
id: str
run_id: str | None = None
created_at: datetime
revision: str
service_count: int = 0
relationship_count: int = 0
summary: dict[str, int] = Field(default_factory=dict)
class TopologyChange(BaseModel):
change_type: str
entity_type: str
entity_id: str
name: str
before: dict[str, object] | None = None
after: dict[str, object] | None = None
class TopologyDiff(BaseModel):
before_snapshot_id: str | None = None
after_snapshot_id: str
added: list[TopologyChange] = Field(default_factory=list)
removed: list[TopologyChange] = Field(default_factory=list)
changed: list[TopologyChange] = Field(default_factory=list)
summary: dict[str, int] = Field(default_factory=dict)
class DiscoveryRunRequest(BaseModel):
docker_host: str | None = None
appops_url: str | None = None
include_stopped: bool = False
class DiscoveryReconcileRequest(BaseModel):
run_id: str | None = None
apply_high_confidence: bool = False
include_stopped: bool = False
class EdgeMutationRequest(BaseModel):
source_service_id: str
target_service_id: str
relation_type: RelationType = RelationType.DEPENDS_ON
lock: bool = True
explanation: str = Field(default="Manual topology relationship", max_length=500)
class EdgeActionRequest(BaseModel):
action: str = Field(pattern="^(confirm|reject|lock|remove)$")
class ConfigService(ServiceCreate):
pass
class ConfigDocument(BaseModel):
# Version 1 remains accepted. Version 2 is emitted when the document uses
# the richer service fields; the parser keeps the old empty v2 rejection
# compatibility used by the v1 contract tests.
version: int = Field(default=2, ge=1, le=2)
services: list[ConfigService] = Field(default_factory=list)
class ConfigImportRequest(BaseModel):
content: str
format: str = Field(pattern="^(yaml|json)$")
mode: str = Field(default="merge", pattern="^(merge|replace)$")
class ConfigImportResult(BaseModel):
created: int
updated: int
total: int
class DemoState(BaseModel):
active: bool
synthetic: bool = False
scenario_name: str | None = None
service_count: int = 0
root_cause_service_id: str | None = None
root_cause_service_name: str | None = None
affected_service_ids: list[str] = Field(default_factory=list)
affected_service_names: list[str] = Field(default_factory=list)
affected_count: int = 0
class DemoMutationResult(DemoState):
created: int = 0
updated: int = 0
total: int = 0
restored: bool = False
class DockerDiscoveryRequest(BaseModel):
docker_host: str = "unix:///var/run/docker.sock"
class DockerPort(BaseModel):
container_port: str
host_ip: str | None = None
host_port: int | None = None
class DockerSuggestion(BaseModel):
name: str
kind: ServiceKind
host: str | None = None
port: int | None = None
url: str | None = None
group: str | None = None
evidence: list[str] = Field(default_factory=list)
confidence: str = "low"
depends_on_names: list[str] = Field(default_factory=list)
source_type: str | None = None
source_ref: str | None = None
stable_source_ref: str | None = None
category: DiscoveryCategory = DiscoveryCategory.UNKNOWN
visibility: DiscoveryVisibility = DiscoveryVisibility.VISIBLE
relation_type: RelationType = RelationType.DEPENDS_ON
class ProbeEndpointCandidate(BaseModel):
"""A reachable-or-reviewable endpoint discovered for a Docker workload."""
source: ProbeEndpointSource
kind: ServiceKind
url: str | None = None
host: str | None = None
port: int | None = Field(default=None, ge=1, le=65535)
reachability: ProbeReachability = ProbeReachability.UNVERIFIED
confidence: DiscoveryConfidence = DiscoveryConfidence.LOW
primary: bool = False
evidence: str = ""
preflight_detail: str | None = None
preflight_latency_ms: float | None = None
class DockerCandidate(BaseModel):
container_id: str
name: str
image: str
state: str
health: str | None = None
networks: list[str]
ports: list[DockerPort]
labels: dict[str, str] = Field(default_factory=dict)
import_state: str = "new"
suggestions: list[DockerSuggestion] = Field(default_factory=list)
dependency_suggestions: list[str] = Field(default_factory=list)
stable_source_ref: str | None = None
compose_project: str | None = None
compose_service: str | None = None
category: DiscoveryCategory = DiscoveryCategory.UNKNOWN
visibility: DiscoveryVisibility = DiscoveryVisibility.VISIBLE
lifecycle: DiscoveryLifecycle = DiscoveryLifecycle.ACTIVE
healthcheck_present: bool = False
environment_evidence: list[str] = Field(default_factory=list)
proxy_route_evidence: list[str] = Field(default_factory=list)
appops_app_id: str | None = None
appops_category: str | None = None
appops_criticality: str | None = None
appops_health: str | None = None
appops_scope: EvidenceScope | None = None
appops_discovery_confidence: float | None = Field(default=None, ge=0, le=1)
appops_policy_profile: str | None = None
appops_app_type: str | None = None
probe_candidates: list[ProbeEndpointCandidate] = Field(default_factory=list)
class DockerDiscoveryResult(BaseModel):
docker_host: str
candidates: list[DockerCandidate]
proxy_routes: int = 0
appops_available: bool = False
appops_detail: str = ""
class DiscoveryStatus(BaseModel):
current: DiscoveryRun | None = None
last_completed: DiscoveryRun | None = None
active: bool = False
class DiscoveryResult(DockerDiscoveryResult):
run: DiscoveryRun
suggestions: list[TopologySuggestion] = Field(default_factory=list)
observations: list[DiscoveryObservation] = Field(default_factory=list)
appops_available: bool = False
appops_mappings: int = 0
proxy_routes: int = 0
class ReconcileResult(BaseModel):
run: DiscoveryRun
created: int = 0
updated: int = 0
unchanged: int = 0
missing: int = 0
retired: int = 0
accepted_relations: int = 0
open_suggestions: int = 0
service_ids: list[str] = Field(default_factory=list)
class ProbeRepairChange(BaseModel):
service_id: str
service_name: str
previous_monitoring_state: MonitoringState
next_monitoring_state: MonitoringState
previous_endpoint: str | None = None
next_endpoint: str | None = None
next_probe_source: ProbeEndpointSource | None = None
next_probe_confidence: DiscoveryConfidence | None = None
reason: str
class ProbeRepairReport(BaseModel):
run_id: str | None = None
dry_run: bool = True
inspected: int = 0
changed: int = 0
disabled_unsafe: int = 0
enabled_published: int = 0
docker_health_observed: int = 0
appops_observed: int = 0
runtime_observed: int = 0
unmonitored: int = 0
resolved_incidents: int = 0
changes: list[ProbeRepairChange] = Field(default_factory=list)
class OverviewSummary(BaseModel):
generated_at: datetime
total_services: int
health_counts: dict[str, int]
active_incidents: int
likely_root_causes: list[str] = Field(default_factory=list)
flapping_services: list[str] = Field(default_factory=list)
recent_events: list[EventRecord] = Field(default_factory=list)
monitoring_coverage: dict[str, int] = Field(default_factory=dict)
class MaintenanceRequest(BaseModel):
enabled: bool
reason: str | None = Field(default=None, max_length=240)