feat(system): add Worker 006 telemetry and network profile

This commit is contained in:
DCCONSTRUCTIONS
2026-07-27 21:06:51 +03:00
parent 438196cd6d
commit 67e12a47a4
17 changed files with 2723 additions and 8 deletions
+6
View File
@@ -52,6 +52,7 @@ from k1link.web.plugin_runtime import (
)
from k1link.web.polygon_api import build_polygon_router, configured_polygon_runs_root
from k1link.web.session_api import build_session_router
from k1link.web.system_telemetry_api import build_system_telemetry_router
REPOSITORY_ROOT = Path(__file__).resolve().parents[3]
INVALID_REQUEST_DETAIL = "Некорректные параметры запроса."
@@ -618,6 +619,11 @@ app.include_router(
),
)
)
app.include_router(
build_system_telemetry_router(
root_provider=lambda: REPOSITORY_ROOT / ".runtime" / "system",
)
)
frontend_dist = REPOSITORY_ROOT / "apps" / "control-station" / "dist"
+834
View File
@@ -0,0 +1,834 @@
from __future__ import annotations
import base64
import ipaddress
import json
import math
import os
import re
import subprocess
import tempfile
import threading
import time
from collections import deque
from collections.abc import Callable
from datetime import UTC, datetime
from pathlib import Path
from typing import Any, Final
from fastapi import APIRouter, HTTPException, Query
from pydantic import BaseModel, ConfigDict, Field, field_validator
PROFILE_SCHEMA: Final = "missioncore.worker-connection-profile/v1"
TELEMETRY_SCHEMA: Final = "missioncore.worker-telemetry/v1"
PROBE_SCHEMA: Final = "missioncore.worker-probe/v1"
DEFAULT_PROFILE_ID: Final = "worker-006"
DEFAULT_DISPLAY_NAME: Final = "Worker 006"
EXPECTED_NODE_ID: Final = "DESKTOP-OPJ8J04"
SSH_HOST_ALIAS: Final = "mission-gpu"
PROFILE_FILE_NAME: Final = "worker-006.json"
CONTAINER_NAMES: Final = (
"mission-core-triton",
"mission-core-perception-worker",
"sentinel-frigate",
"sentinel-ollama",
)
SAFE_HOSTNAME = re.compile(
r"^(?=.{1,253}\.?$)(?:[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?\.)*"
r"[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?\.?$"
)
PROMETHEUS_SAMPLE = re.compile(
r"^(?P<name>nv_inference_(?:request_success|request_failure|count))"
r"(?:\{[^}]*\})?\s+(?P<value>[0-9.eE+-]+)$"
)
RootProvider = Callable[[], Path]
ProbeRunner = Callable[["WorkerConnectionProfile"], dict[str, Any]]
class WorkerConnectionProfile(BaseModel):
model_config = ConfigDict(extra="forbid", frozen=True)
schema_version: str = PROFILE_SCHEMA
profile_id: str = DEFAULT_PROFILE_ID
display_name: str = DEFAULT_DISPLAY_NAME
expected_node_id: str = EXPECTED_NODE_ID
ssh_host_alias: str = SSH_HOST_ALIAS
address: str = ""
port: int = Field(default=22, ge=1, le=65535)
revision: int = Field(default=0, ge=0)
updated_at_utc: str | None = None
@field_validator("address")
@classmethod
def validate_address(cls, value: str) -> str:
normalized = value.strip()
if not normalized:
return ""
if any(character.isspace() or ord(character) < 32 for character in normalized):
raise ValueError("worker address contains whitespace or control characters")
try:
ipaddress.ip_address(normalized)
except ValueError:
if SAFE_HOSTNAME.fullmatch(normalized) is None:
raise ValueError("worker address is not a valid IP address or hostname") from None
return normalized.rstrip(".")
class WorkerConnectionProfilePut(BaseModel):
model_config = ConfigDict(extra="forbid", frozen=True)
revision: int = Field(ge=0)
address: str = Field(default="", max_length=253)
port: int = Field(default=22, ge=1, le=65535)
@field_validator("address")
@classmethod
def validate_address(cls, value: str) -> str:
return WorkerConnectionProfile.validate_address(value)
def _utc_now() -> str:
return datetime.now(UTC).isoformat().replace("+00:00", "Z")
class WorkerProfileStore:
def __init__(self, root: Path) -> None:
self.root = root
self.path = root / PROFILE_FILE_NAME
self._lock = threading.Lock()
def read(self) -> WorkerConnectionProfile:
with self._lock:
if not self.path.is_file():
return WorkerConnectionProfile()
try:
document = json.loads(self.path.read_text(encoding="utf-8"))
return WorkerConnectionProfile.model_validate(document)
except (OSError, ValueError, TypeError, json.JSONDecodeError):
return WorkerConnectionProfile()
def save(self, request: WorkerConnectionProfilePut) -> WorkerConnectionProfile:
with self._lock:
current = self._read_unlocked()
if current.revision != request.revision:
raise RuntimeError("worker profile revision changed")
profile = current.model_copy(
update={
"address": request.address,
"port": request.port,
"revision": current.revision + 1,
"updated_at_utc": _utc_now(),
}
)
self.root.mkdir(mode=0o700, parents=True, exist_ok=True)
descriptor, temporary_name = tempfile.mkstemp(
prefix=".worker-006.",
suffix=".tmp",
dir=self.root,
)
temporary = Path(temporary_name)
try:
with os.fdopen(descriptor, "w", encoding="utf-8", newline="\n") as stream:
json.dump(
profile.model_dump(mode="json"),
stream,
ensure_ascii=False,
sort_keys=True,
separators=(",", ":"),
)
stream.write("\n")
stream.flush()
os.fsync(stream.fileno())
os.chmod(temporary, 0o600)
os.replace(temporary, self.path)
finally:
temporary.unlink(missing_ok=True)
return profile
def _read_unlocked(self) -> WorkerConnectionProfile:
if not self.path.is_file():
return WorkerConnectionProfile()
try:
return WorkerConnectionProfile.model_validate_json(
self.path.read_text(encoding="utf-8")
)
except (OSError, ValueError):
return WorkerConnectionProfile()
WORKER_PROBE_POWERSHELL: Final = r"""
$ErrorActionPreference = "Stop"
$containers = @(
"mission-core-triton",
"mission-core-perception-worker",
"sentinel-frigate",
"sentinel-ollama"
)
$os = Get-CimInstance Win32_OperatingSystem
$cpu = Get-CimInstance Win32_Processor
$computer = Get-CimInstance Win32_ComputerSystem
$dockerStats = @{}
try {
$statsLines = @(docker stats --no-stream --format "{{json .}}" $containers)
foreach ($line in $statsLines) {
if ($line) {
$row = $line | ConvertFrom-Json
$dockerStats[$row.Name] = $row
}
}
} catch {}
$containerStates = @{}
foreach ($name in $containers) {
try {
$state = (docker inspect --format "{{json .State}}" $name) | ConvertFrom-Json
$image = docker inspect --format "{{.Config.Image}}" $name
$containerStates[$name] = [ordered]@{state=$state; image=$image}
} catch {
$containerStates[$name] = $null
}
}
$gpu = $null
try {
$gpuQuery = "name,utilization.gpu,memory.used,memory.total,power.draw,temperature.gpu"
$gpuLine = nvidia-smi "--query-gpu=$gpuQuery" --format=csv,noheader,nounits |
Select-Object -First 1
$gpuParts = @($gpuLine -split ",\s*")
if ($gpuParts.Count -ge 6) {
$gpu = [ordered]@{
name=$gpuParts[0]
utilization_percent=[double]$gpuParts[1]
memory_used_mib=[double]$gpuParts[2]
memory_total_mib=[double]$gpuParts[3]
power_watts=[double]$gpuParts[4]
temperature_celsius=[double]$gpuParts[5]
}
}
} catch {}
$network = @()
foreach ($adapter in @(Get-NetAdapter | Where-Object Status -eq "Up")) {
try {
$statistics = Get-NetAdapterStatistics -Name $adapter.Name
$addresses = @(
Get-NetIPAddress -InterfaceIndex $adapter.ifIndex -AddressFamily IPv4 `
-ErrorAction SilentlyContinue |
Where-Object { $_.IPAddress -notlike "169.254.*" } |
Select-Object -ExpandProperty IPAddress
)
$network += [ordered]@{
name=$adapter.Name
description=$adapter.InterfaceDescription
status=$adapter.Status
link_speed_bps=[double]$adapter.Speed
mac_address=$adapter.MacAddress
addresses=$addresses
received_bytes=[double]$statistics.ReceivedBytes
sent_bytes=[double]$statistics.SentBytes
}
} catch {}
}
$tritonReady = $false
try {
$tritonResponse = Invoke-WebRequest -UseBasicParsing `
-Uri "http://127.0.0.1:8000/v2/health/ready" -TimeoutSec 2
$tritonReady = $tritonResponse.StatusCode -eq 200
} catch {}
$tritonMetrics = @()
try {
$metricsResponse = Invoke-WebRequest -UseBasicParsing `
-Uri "http://127.0.0.1:8002/metrics" -TimeoutSec 2
$tritonMetrics = @(
$metricsResponse.Content -split "`n" |
Where-Object { $_ -match "^nv_inference_(request_success|request_failure|count)" }
)
} catch {}
$perceptionHealth = $null
try {
$healthJson = docker exec mission-core-perception-worker python3 -c "import urllib.request;print(urllib.request.urlopen('http://127.0.0.1:18020/health',timeout=2).read().decode())"
$perceptionHealth = $healthJson | ConvertFrom-Json
} catch {}
$disks = @(
Get-CimInstance Win32_LogicalDisk -Filter "DriveType=3" |
ForEach-Object {
[ordered]@{
name=$_.DeviceID
size_bytes=[double]$_.Size
free_bytes=[double]$_.FreeSpace
}
}
)
[ordered]@{
schema_version="missioncore.worker-probe-raw/v1"
observed_at_utc=[DateTime]::UtcNow.ToString("o")
node_id=$env:COMPUTERNAME
os=[ordered]@{
caption=$os.Caption
version=$os.Version
uptime_seconds=([DateTime]::UtcNow - $os.LastBootUpTime.ToUniversalTime()).TotalSeconds
}
cpu=[ordered]@{
name=(@($cpu | Select-Object -ExpandProperty Name) -join " + ")
logical_processors=[int]$computer.NumberOfLogicalProcessors
load_percent=[double](($cpu | Measure-Object LoadPercentage -Average).Average)
}
memory=[ordered]@{
total_bytes=[double]$os.TotalVisibleMemorySize * 1024
free_bytes=[double]$os.FreePhysicalMemory * 1024
}
disks=$disks
gpu=$gpu
network=$network
docker_stats=$dockerStats
container_states=$containerStates
triton=[ordered]@{ready=$tritonReady; metrics=$tritonMetrics}
perception=$perceptionHealth
} | ConvertTo-Json -Depth 12 -Compress
""".strip()
def _ssh_arguments(profile: WorkerConnectionProfile) -> list[str]:
arguments = [
"ssh",
"-o",
"BatchMode=yes",
"-o",
"StrictHostKeyChecking=yes",
"-o",
f"HostKeyAlias={profile.ssh_host_alias}",
"-o",
"ConnectTimeout=5",
]
if profile.address:
arguments.extend(["-o", f"HostName={profile.address}"])
arguments.extend(
[
"-p",
str(profile.port),
profile.ssh_host_alias,
"powershell.exe",
"-NoLogo",
"-NoProfile",
"-NonInteractive",
"-Command",
(
"$encoded=[Console]::In.ReadToEnd();"
"Invoke-Expression "
"([Text.Encoding]::Unicode.GetString([Convert]::FromBase64String($encoded)))"
),
]
)
return arguments
def run_worker_probe(profile: WorkerConnectionProfile) -> dict[str, Any]:
started = time.perf_counter()
try:
completed = subprocess.run(
_ssh_arguments(profile),
check=False,
capture_output=True,
input=base64.b64encode(WORKER_PROBE_POWERSHELL.encode("utf-16le")),
timeout=12,
)
except (OSError, subprocess.TimeoutExpired):
return _failed_probe(profile, "worker-unreachable", started)
if completed.returncode != 0:
return _failed_probe(profile, "worker-unreachable", started)
try:
raw = completed.stdout.decode("cp866")
document = json.loads(raw.strip())
except (UnicodeDecodeError, json.JSONDecodeError, TypeError):
return _failed_probe(profile, "invalid-worker-response", started)
if not isinstance(document, dict):
return _failed_probe(profile, "invalid-worker-response", started)
node_id = document.get("node_id")
if node_id != profile.expected_node_id:
return {
"schema_version": PROBE_SCHEMA,
"reachable": True,
"identity_matches": False,
"node_id": node_id if isinstance(node_id, str) else None,
"latency_ms": (time.perf_counter() - started) * 1000,
"observed_at_utc": _utc_now(),
"error_code": "worker-identity-mismatch",
"raw": None,
}
return {
"schema_version": PROBE_SCHEMA,
"reachable": True,
"identity_matches": True,
"node_id": node_id,
"latency_ms": (time.perf_counter() - started) * 1000,
"observed_at_utc": _utc_now(),
"error_code": None,
"raw": document,
}
def _failed_probe(
profile: WorkerConnectionProfile,
error_code: str,
started: float,
) -> dict[str, Any]:
return {
"schema_version": PROBE_SCHEMA,
"reachable": False,
"identity_matches": False,
"node_id": None,
"expected_node_id": profile.expected_node_id,
"latency_ms": (time.perf_counter() - started) * 1000,
"observed_at_utc": _utc_now(),
"error_code": error_code,
"raw": None,
}
def _number(value: object) -> float | None:
if isinstance(value, bool) or not isinstance(value, int | float):
return None
result = float(value)
return result if math.isfinite(result) else None
def _mapping(value: object) -> dict[str, Any]:
return value if isinstance(value, dict) else {}
def _items(value: object) -> list[Any]:
return value if isinstance(value, list) else []
def _percent(used: float | None, total: float | None) -> float | None:
if used is None or total is None or total <= 0:
return None
return max(0.0, min(100.0, used / total * 100))
def _container_document(
name: str,
raw_stats: dict[str, Any],
raw_states: dict[str, Any],
) -> dict[str, Any]:
stats = _mapping(raw_stats.get(name))
descriptor = _mapping(raw_states.get(name))
state = _mapping(descriptor.get("state"))
health = _mapping(state.get("Health"))
roles = {
"mission-core-triton": ("Inference Runtime", "mission-core", False),
"mission-core-perception-worker": ("Perception Pipeline", "mission-core", False),
"sentinel-frigate": ("Sentinel Frigate", "external", True),
"sentinel-ollama": ("Sentinel Ollama", "external", True),
}
role, owner, external = roles[name]
return {
"name": name,
"role": role,
"owner": owner,
"external": external,
"image": descriptor.get("image") if isinstance(descriptor.get("image"), str) else None,
"state": state.get("Status") if isinstance(state.get("Status"), str) else "unavailable",
"health": health.get("Status") if isinstance(health.get("Status"), str) else None,
"cpu_percent": _parse_percent(stats.get("CPUPerc")),
"memory_percent": _parse_percent(stats.get("MemPerc")),
"memory_usage": stats.get("MemUsage") if isinstance(stats.get("MemUsage"), str) else None,
"network_io": stats.get("NetIO") if isinstance(stats.get("NetIO"), str) else None,
"block_io": stats.get("BlockIO") if isinstance(stats.get("BlockIO"), str) else None,
"pids": _parse_integer(stats.get("PIDs")),
}
def _parse_percent(value: object) -> float | None:
if not isinstance(value, str):
return None
try:
number = float(value.rstrip("%"))
except ValueError:
return None
return number if math.isfinite(number) else None
def _parse_integer(value: object) -> int | None:
if not isinstance(value, str):
return None
try:
return int(value)
except ValueError:
return None
def _triton_metrics(lines: object) -> dict[str, float | None]:
totals: dict[str, float] = {
"nv_inference_request_success": 0,
"nv_inference_request_failure": 0,
"nv_inference_count": 0,
}
matched: set[str] = set()
for line in _items(lines):
if not isinstance(line, str):
continue
match = PROMETHEUS_SAMPLE.fullmatch(line.strip())
if match is None:
continue
try:
value = float(match.group("value"))
except ValueError:
continue
if math.isfinite(value):
name = match.group("name")
totals[name] += value
matched.add(name)
return {
"requests_succeeded": (
totals["nv_inference_request_success"]
if "nv_inference_request_success" in matched
else None
),
"requests_failed": (
totals["nv_inference_request_failure"]
if "nv_inference_request_failure" in matched
else None
),
"inferences": (
totals["nv_inference_count"] if "nv_inference_count" in matched else None
),
}
def _pipeline_document(raw: dict[str, Any]) -> dict[str, Any]:
perception = _mapping(raw.get("perception"))
current_stage = perception.get("current_stage")
if not isinstance(current_stage, str):
current_stage = None
busy = perception.get("state") == "busy"
stages = (
("source-ingress", "Приём сенсорного потока"),
("camera-decode", "Декодирование камеры"),
("preprocessing", "Предобработка"),
("detector", "Детектор объектов"),
("semantic-model", "Семантическая модель"),
("sensor-fusion", "Camera ↔ LiDAR fusion"),
("tracking", "Трекинг"),
("temporal-state", "Временное состояние"),
("result-publication", "Публикация результата"),
)
def stage_state(stage_id: str) -> str:
if not perception:
return "unavailable"
if not busy:
return "ready"
if stage_id == current_stage or stage_id in {
"source-ingress",
"camera-decode",
"semantic-model",
}:
return "active"
return "waiting"
return {
"service_state": (
perception.get("state") if isinstance(perception.get("state"), str) else "unavailable"
),
"current_stage": current_stage,
"active_request_id": (
perception.get("active_request_id")
if isinstance(perception.get("active_request_id"), str)
else None
),
"active_frame_index": (
int(perception["active_frame_index"])
if isinstance(perception.get("active_frame_index"), int)
else None
),
"completed_runs": (
int(perception["completed_runs"])
if isinstance(perception.get("completed_runs"), int)
else None
),
"failed_runs": (
int(perception["failed_runs"])
if isinstance(perception.get("failed_runs"), int)
else None
),
"model_load_seconds": _number(perception.get("model_load_seconds")),
"stages": [
{
"id": stage_id,
"label": label,
"state": stage_state(stage_id),
}
for stage_id, label in stages
],
}
class WorkerTelemetryService:
def __init__(
self,
store: WorkerProfileStore,
probe_runner: ProbeRunner = run_worker_probe,
*,
cache_seconds: float = 1.5,
) -> None:
self.store = store
self.probe_runner = probe_runner
self.cache_seconds = cache_seconds
self._lock = threading.Lock()
self._cached_at = 0.0
self._cached: dict[str, Any] | None = None
self._previous_network: tuple[float, float, float] | None = None
self._history: deque[dict[str, Any]] = deque(maxlen=300)
def profile_document(self) -> dict[str, Any]:
return {
"schema_version": PROFILE_SCHEMA,
"profile": self.store.read().model_dump(mode="json"),
"security": {
"transport": "ssh",
"host_key_policy": "strict-pinned",
"credentials_managed_by_ui": False,
"ssh_host_alias": SSH_HOST_ALIAS,
},
}
def test_profile(self, request: WorkerConnectionProfilePut) -> dict[str, Any]:
current = self.store.read()
candidate = current.model_copy(
update={"address": request.address, "port": request.port}
)
return self._public_probe(self.probe_runner(candidate))
def apply_profile(self, request: WorkerConnectionProfilePut) -> dict[str, Any]:
current = self.store.read()
if request.revision != current.revision:
raise RuntimeError("worker profile revision changed")
candidate = current.model_copy(
update={"address": request.address, "port": request.port}
)
probe = self.probe_runner(candidate)
if not probe.get("reachable") or not probe.get("identity_matches"):
raise ConnectionError(str(probe.get("error_code") or "worker-unreachable"))
saved = self.store.save(request)
with self._lock:
self._cached = None
self._cached_at = 0
self._previous_network = None
self._history.clear()
return {
**self.profile_document(),
"verification": self._public_probe(probe),
"profile": saved.model_dump(mode="json"),
}
def snapshot(self, history_limit: int) -> dict[str, Any]:
with self._lock:
now = time.monotonic()
if self._cached is not None and now - self._cached_at < self.cache_seconds:
return {
**self._cached,
"history": list(self._history)[-history_limit:],
}
profile = self.store.read()
probe = self.probe_runner(profile)
document = self._telemetry_document(profile, probe, now)
self._cached = document
self._cached_at = now
return {
**document,
"history": list(self._history)[-history_limit:],
}
def _telemetry_document(
self,
profile: WorkerConnectionProfile,
probe: dict[str, Any],
monotonic_now: float,
) -> dict[str, Any]:
public_probe = self._public_probe(probe)
if not probe.get("reachable") or not probe.get("identity_matches"):
return {
"schema_version": TELEMETRY_SCHEMA,
"profile": profile.model_dump(mode="json"),
"connection": public_probe,
"node": None,
"runtimes": [],
"pipeline": _pipeline_document({}),
"network": {"interfaces": [], "aggregate": None},
}
raw = _mapping(probe.get("raw"))
memory = _mapping(raw.get("memory"))
memory_total = _number(memory.get("total_bytes"))
memory_free = _number(memory.get("free_bytes"))
memory_used = (
memory_total - memory_free
if memory_total is not None and memory_free is not None
else None
)
gpu = _mapping(raw.get("gpu"))
gpu_used = _number(gpu.get("memory_used_mib"))
gpu_total = _number(gpu.get("memory_total_mib"))
interfaces = [
self._network_interface(item)
for item in _items(raw.get("network"))
if isinstance(item, dict)
]
received = sum(
value
for item in interfaces
if (value := _number(item.get("received_bytes"))) is not None
)
sent = sum(
value
for item in interfaces
if (value := _number(item.get("sent_bytes"))) is not None
)
receive_rate: float | None = None
send_rate: float | None = None
if self._previous_network is not None:
previous_at, previous_received, previous_sent = self._previous_network
elapsed = monotonic_now - previous_at
if elapsed > 0 and received >= previous_received and sent >= previous_sent:
receive_rate = (received - previous_received) / elapsed
send_rate = (sent - previous_sent) / elapsed
self._previous_network = (monotonic_now, received, sent)
raw_stats = _mapping(raw.get("docker_stats"))
raw_states = _mapping(raw.get("container_states"))
runtimes = [
_container_document(name, raw_stats, raw_states) for name in CONTAINER_NAMES
]
triton = _mapping(raw.get("triton"))
triton_document = {
"ready": triton.get("ready") is True,
**_triton_metrics(triton.get("metrics")),
}
memory_used_percent = _percent(memory_used, memory_total)
gpu_memory_used_percent = _percent(gpu_used, gpu_total)
node: dict[str, Any] = {
"node_id": raw.get("node_id"),
"observed_at_utc": raw.get("observed_at_utc"),
"os": _mapping(raw.get("os")),
"cpu": _mapping(raw.get("cpu")),
"memory": {
"total_bytes": memory_total,
"used_bytes": memory_used,
"free_bytes": memory_free,
"used_percent": memory_used_percent,
},
"disks": _items(raw.get("disks")),
"gpu": {
**gpu,
"memory_used_percent": gpu_memory_used_percent,
}
if gpu
else None,
"triton": triton_document,
}
network = {
"interfaces": interfaces,
"aggregate": {
"received_bytes": received,
"sent_bytes": sent,
"receive_bytes_per_second": receive_rate,
"send_bytes_per_second": send_rate,
},
}
history_row = {
"observed_at_utc": raw.get("observed_at_utc") or _utc_now(),
"cpu_percent": _number(_mapping(raw.get("cpu")).get("load_percent")),
"memory_percent": memory_used_percent,
"gpu_percent": _number(gpu.get("utilization_percent")),
"gpu_memory_percent": gpu_memory_used_percent,
"network_receive_bytes_per_second": receive_rate,
"network_send_bytes_per_second": send_rate,
}
self._history.append(history_row)
return {
"schema_version": TELEMETRY_SCHEMA,
"profile": profile.model_dump(mode="json"),
"connection": public_probe,
"node": node,
"runtimes": runtimes,
"pipeline": _pipeline_document(raw),
"network": network,
}
@staticmethod
def _network_interface(item: dict[str, Any]) -> dict[str, Any]:
addresses = [
address for address in _items(item.get("addresses")) if isinstance(address, str)
]
return {
"name": item.get("name") if isinstance(item.get("name"), str) else "unknown",
"description": (
item.get("description") if isinstance(item.get("description"), str) else None
),
"status": item.get("status") if isinstance(item.get("status"), str) else None,
"link_speed_bps": _number(item.get("link_speed_bps")),
"mac_address": (
item.get("mac_address") if isinstance(item.get("mac_address"), str) else None
),
"addresses": addresses,
"received_bytes": _number(item.get("received_bytes")),
"sent_bytes": _number(item.get("sent_bytes")),
}
@staticmethod
def _public_probe(probe: dict[str, Any]) -> dict[str, Any]:
return {
"schema_version": PROBE_SCHEMA,
"reachable": probe.get("reachable") is True,
"identity_matches": probe.get("identity_matches") is True,
"node_id": probe.get("node_id") if isinstance(probe.get("node_id"), str) else None,
"latency_ms": _number(probe.get("latency_ms")),
"observed_at_utc": (
probe.get("observed_at_utc")
if isinstance(probe.get("observed_at_utc"), str)
else _utc_now()
),
"error_code": (
probe.get("error_code")
if isinstance(probe.get("error_code"), str)
else None
),
}
def build_system_telemetry_router(
*,
root_provider: RootProvider,
probe_runner: ProbeRunner = run_worker_probe,
) -> APIRouter:
store = WorkerProfileStore(root_provider())
service = WorkerTelemetryService(store, probe_runner)
router = APIRouter(prefix="/api/v1/system", tags=["system"])
@router.get("/worker-profile")
def get_worker_profile() -> dict[str, Any]:
return service.profile_document()
@router.post("/worker-profile/test")
def test_worker_profile(request: WorkerConnectionProfilePut) -> dict[str, Any]:
return service.test_profile(request)
@router.put("/worker-profile")
def put_worker_profile(request: WorkerConnectionProfilePut) -> dict[str, Any]:
try:
return service.apply_profile(request)
except RuntimeError as exc:
raise HTTPException(
status_code=409,
detail="Профиль Worker 006 был изменён в другой сессии.",
) from exc
except ConnectionError as exc:
raise HTTPException(
status_code=409,
detail="Новый адрес не прошёл проверку узла Worker 006.",
) from exc
@router.get("/worker-telemetry")
def get_worker_telemetry(
history: int = Query(default=90, ge=1, le=300),
) -> dict[str, Any]:
return service.snapshot(history)
return router