feat(telemetry): add portable local telemetry plane

This commit is contained in:
DCCONSTRUCTIONS
2026-07-28 00:54:11 +03:00
parent 9fc002d86f
commit b8a008b50e
16 changed files with 1486 additions and 0 deletions
+164
View File
@@ -0,0 +1,164 @@
from __future__ import annotations
import importlib.util
import json
from datetime import UTC, datetime
from pathlib import Path
from types import ModuleType
import pytest
import yaml
REPOSITORY_ROOT = Path(__file__).resolve().parents[1]
NORMALIZER_PATH = (
REPOSITORY_ROOT
/ "deploy"
/ "telemetry-plane"
/ "normalizer"
/ "normalizer.py"
)
COMPOSE_PATH = REPOSITORY_ROOT / "deploy" / "telemetry-plane" / "compose.yaml"
NORMALIZER_SOURCE = NORMALIZER_PATH.read_text(encoding="utf-8")
def _normalizer() -> ModuleType:
spec = importlib.util.spec_from_file_location(
"missioncore_telemetry_normalizer",
NORMALIZER_PATH,
)
assert spec is not None and spec.loader is not None
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
def test_normalizer_accepts_native_telegraf_host_metric() -> None:
normalizer = _normalizer()
payload = json.dumps(
{
"name": "cpu",
"tags": {
"node_id": "DESKTOP-OPJ8J04",
"host": "DESKTOP-OPJ8J04",
"contour_id": "worker-006",
"agent_id": "worker-006",
},
"fields": {"usage_active": 12.5},
"timestamp": 1_785_179_600,
}
).encode()
row = normalizer._normalize(
"mission-core/v1/contours/worker-006/agents/worker-006/host",
payload,
)
assert row[0] == datetime.fromtimestamp(1_785_179_600, tz=UTC)
assert row[1:5] == ("worker-006", "worker-006", "DESKTOP-OPJ8J04", "host")
assert row[5] == "cpu"
assert row[6] == "{}"
assert row[7] == "telegraf.metric-json/v1"
def test_normalizer_keeps_same_timestamp_host_measurements_distinct() -> None:
normalizer = _normalizer()
rows = [
normalizer._normalize(
"mission-core/v1/contours/worker-006/agents/worker-006/host",
json.dumps(
{
"name": name,
"tags": {
"node_id": "DESKTOP-OPJ8J04",
"contour_id": "worker-006",
"agent_id": "worker-006",
},
"fields": fields,
"timestamp": 1_785_179_600,
}
).encode(),
)
for name, fields in (
("cpu", {"usage_active": 12.5}),
("mem", {"used_percent": 43.2}),
)
]
assert rows[0][0] == rows[1][0]
assert rows[0][5] == "cpu"
assert rows[1][5] == "mem"
assert rows[0][:5] == rows[1][:5]
def test_normalizer_keeps_same_measurement_series_distinct() -> None:
normalizer = _normalizer()
rows = [
normalizer._normalize(
"mission-core/v1/contours/worker-006/agents/worker-006/host",
json.dumps(
{
"name": "net",
"tags": {
"node_id": "DESKTOP-OPJ8J04",
"contour_id": "worker-006",
"agent_id": "worker-006",
"interface": interface,
},
"fields": {"bytes_recv": received},
"timestamp": 1_785_179_600,
}
).encode(),
)
for interface, received in (("Ethernet", 10), ("Wi-Fi", 20))
]
assert rows[0][0:6] == rows[1][0:6]
assert rows[0][6] == '{"interface":"Ethernet"}'
assert rows[1][6] == '{"interface":"Wi-Fi"}'
def test_telegraf_upsert_merges_split_fields_in_one_series() -> None:
assert "contour_telemetry_samples.payload -> 'fields'" in NORMALIZER_SOURCE
assert "EXCLUDED.payload -> 'fields'" in NORMALIZER_SOURCE
def test_normalizer_rejects_unschematized_pipeline_payload() -> None:
normalizer = _normalizer()
with pytest.raises(ValueError, match="schema"):
normalizer._normalize(
"mission-core/v1/contours/worker-006/agents/worker-006/pipeline",
json.dumps(
{
"node_id": "DESKTOP-OPJ8J04",
"observed_at_utc": "2026-07-27T12:00:00Z",
}
).encode(),
)
def test_telemetry_plane_uses_the_ndc_docker_namespace() -> None:
document = yaml.safe_load(COMPOSE_PATH.read_text(encoding="utf-8"))
assert document["name"] == "ndc-mission-core-telemetry"
services = document["services"]
assert {
service["container_name"]
for service in services.values()
} == {
"ndc-mission-core-mqtt-broker",
"ndc-mission-core-telemetry-normalizer",
"ndc-mission-core-telemetry-timescaledb",
}
for service in services.values():
assert service["container_name"].startswith("ndc-")
assert service["labels"]["com.nodedc.product"] == "mission-core"
assert service["labels"]["com.nodedc.stack"] == "ndc-mission-core-telemetry"
assert document["networks"]["default"]["name"] == "ndc-mission-core-telemetry"
assert {
volume["name"]
for volume in document["volumes"].values()
} == {
"ndc-mission-core-mqtt-data",
"ndc-mission-core-telemetry-timescale-data",
}
+67
View File
@@ -0,0 +1,67 @@
from __future__ import annotations
import importlib.util
import stat
from pathlib import Path
from types import ModuleType
import pytest
def _prepare_module() -> ModuleType:
path = (
Path(__file__).resolve().parents[1]
/ "deploy"
/ "telemetry-plane"
/ "prepare.py"
)
specification = importlib.util.spec_from_file_location(
"mission_core_telemetry_prepare",
path,
)
assert specification is not None
assert specification.loader is not None
module = importlib.util.module_from_spec(specification)
specification.loader.exec_module(module)
return module
prepare = _prepare_module()
def test_initialize_environment_generates_private_unique_secrets(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
env_path = tmp_path / ".env"
monkeypatch.setattr(prepare, "ENV_PATH", env_path)
prepare._initialize_environment("192.0.2.15")
values = dict(
line.split("=", 1)
for line in env_path.read_text(encoding="utf-8").splitlines()
)
assert values["MISSIONCORE_MQTT_BIND_ADDRESS"] == "192.0.2.15"
secrets = {
values["MISSIONCORE_DB_PASSWORD"],
values["MISSIONCORE_MQTT_INGEST_PASSWORD"],
values["MISSIONCORE_MQTT_WORKER_006_PASSWORD"],
}
assert len(secrets) == 3
assert all(len(secret) >= 40 for secret in secrets)
assert stat.S_IMODE(env_path.stat().st_mode) == 0o600
def test_initialize_environment_refuses_to_replace_credentials(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
env_path = tmp_path / ".env"
env_path.write_text("existing=true\n", encoding="utf-8")
monkeypatch.setattr(prepare, "ENV_PATH", env_path)
with pytest.raises(RuntimeError, match="refusing to overwrite"):
prepare._initialize_environment("127.0.0.1")
assert env_path.read_text(encoding="utf-8") == "existing=true\n"