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,862 @@
|
||||
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)
|
||||
Reference in new issue
Block a user