Files
stack/tests/test_observability.py
kert aadf037b5e
Some checks failed
CI / lint (push) Has been cancelled
CI / test (push) Has been cancelled
CI / notebooks-smoke (push) Has been cancelled
Infra CI / notebooks (push) Has been cancelled
Infra CI / zotero (push) Has been cancelled
Infra CI / docs (push) Has been cancelled
Infra CI / api (push) Has been cancelled
Infra CI / llm (push) Has been cancelled
Infra CI / mc (push) Has been cancelled
CI / lint (pull_request) Successful in 33s
CI / notebooks-smoke (pull_request) Successful in 1m32s
Infra CI / notebooks (pull_request) Successful in 1m12s
Infra CI / zotero (pull_request) Successful in 14s
Infra CI / docs (pull_request) Successful in 13s
Infra CI / api (pull_request) Successful in 12s
Infra CI / llm (pull_request) Successful in 17s
Infra CI / mc (pull_request) Successful in 13s
CI / test (pull_request) Successful in 17m57s
docs: drop the cadvisor task and mentions from the observability plan and spec
2026-09-08 21:23:33 -04:00

288 lines
9.3 KiB
Python

"""Compose-level invariants for the observability port.
refs docs/superpowers/specs/2026-05-01-observability-port-design.md
"""
from __future__ import annotations
import json
from pathlib import Path
import yaml
ROOT = Path(__file__).resolve().parents[1]
COMPOSE = ROOT / "compose.yml"
def _load() -> dict:
return yaml.safe_load(COMPOSE.read_text())
def _services() -> dict:
return _load().get("services", {})
def test_compose_has_services():
assert _services(), "compose.yml has no services"
class TestTempo:
def test_tempo_service_present(self):
svcs = _services()
assert "tempo" in svcs
assert svcs["tempo"]["networks"] == ["observability"]
def test_tempo_volume_declared(self):
vols = _load().get("volumes", {})
assert "tempo_data" in vols
def test_tempo_config_mounted(self):
svc = _services()["tempo"]
mounts = [v for v in svc["volumes"] if "/etc/tempo" in v]
assert any("./infra/tempo/tempo.yml" in v for v in mounts)
class TestNvidiaExporter:
def test_nvidia_exporter_present(self):
svcs = _services()
assert "nvidia-exporter" in svcs
def test_nvidia_runtime(self):
svc = _services()["nvidia-exporter"]
assert svc.get("runtime") == "nvidia"
def test_nvidia_gpu_reservation(self):
svc = _services()["nvidia-exporter"]
devices = svc["deploy"]["resources"]["reservations"]["devices"]
assert any(d.get("driver") == "nvidia" for d in devices)
class TestLokiRetention:
def test_loki_compactor_configured(self):
cfg = yaml.safe_load((ROOT / "infra/loki/loki-config.yml").read_text())
assert cfg["compactor"]["retention_enabled"] is True
assert cfg["limits_config"]["retention_period"] == "168h"
class TestPromtailRewire:
def test_promtail_uses_docker_sock(self):
svc = _services()["promtail"]
vols = [v for v in svc["volumes"] if isinstance(v, str)]
# New: docker.sock; old filesystem mount must be gone.
assert any("docker.sock" in v for v in vols)
assert not any("/var/lib/docker/containers" in v for v in vols)
def test_promtail_has_docker_gid(self):
svc = _services()["promtail"]
assert "group_add" in svc
def test_promtail_config_is_docker_sd(self):
cfg = yaml.safe_load((ROOT / "infra/loki/promtail-config.yml").read_text())
jobs = cfg["scrape_configs"]
docker_jobs = [j for j in jobs if j.get("job_name") == "docker"]
assert docker_jobs, "expected job_name=docker"
assert "docker_sd_configs" in docker_jobs[0]
class TestPerServiceStages:
"""Each named container must have a matching pipeline stage that emits at
least one service-specific label."""
EXPECTED = {
"traefik": "tf_event",
"git": "gitea_event",
"oauth2-proxy": "oauth_event",
"postgres": "pg_event",
"trino": "trino_event",
"rustfs": "s3_op",
"api": "api_event",
"mail-poller": "mail_event",
"act-runner": "ci_event",
"cloudflared": "cf_event",
}
def test_each_service_has_a_match_stage(self):
cfg = yaml.safe_load((ROOT / "infra/loki/promtail-config.yml").read_text())
stages = cfg["scrape_configs"][0]["pipeline_stages"]
text = yaml.safe_dump(stages)
for container, label in self.EXPECTED.items():
assert f'container="{container}"' in text, f"no match stage for {container}"
assert label in text, f"no {label} extraction for {container}"
def test_quarkus_pair_has_combined_stage(self):
# nessie + polaris share one stage via regex selector.
cfg = yaml.safe_load((ROOT / "infra/loki/promtail-config.yml").read_text())
text = yaml.safe_dump(cfg["scrape_configs"][0]["pipeline_stages"])
assert 'container=~"nessie|polaris"' in text
assert "quarkus_route" in text
class TestOtelTempo:
def test_otel_exports_traces_to_tempo(self):
cfg = yaml.safe_load((ROOT / "infra/otel/otel-collector.yml").read_text())
exporters = cfg["exporters"]
assert "otlp_http/tempo" in exporters
assert exporters["otlp_http/tempo"]["endpoint"] == "http://tempo:4318"
assert "otlp_grpc/jaeger" not in exporters
traces = cfg["service"]["pipelines"]["traces"]
assert "otlp_http/tempo" in traces["exporters"]
assert "memory_limiter" in traces["processors"]
class TestPrometheusTargets:
def test_targets_swap_jaeger_for_tempo_and_add_exporters(self):
path = ROOT / "infra/prometheus/targets/services.yml"
targets = yaml.safe_load(path.read_text())
jobs = {entry["labels"]["job"] for entry in targets if "labels" in entry}
assert "tempo" in jobs
assert "nvidia-gpu" in jobs
assert "jaeger" not in jobs
class TestGrafanaDatasources:
def test_datasources_have_tempo_loki_default_correlation(self):
path = ROOT / "infra/grafana/provisioning/datasources/datasources.yml"
cfg = yaml.safe_load(path.read_text())
ds = {d["name"]: d for d in cfg["datasources"]}
assert "Tempo" in ds and "Jaeger" not in ds
assert ds["Loki"].get("isDefault") is True
tempo = ds["Tempo"]
assert tempo["jsonData"]["tracesToLogsV2"]["datasourceUid"] == "loki"
assert tempo["jsonData"]["serviceMap"]["datasourceUid"] == "prometheus"
PROMTAIL_LABELED = {
"coredns",
"traefik",
"rustfs",
"postgres",
"git",
"act-runner",
"notebooks",
"zotero",
"webdav",
"nessie",
"trino",
"polaris",
"dashboard",
"docs",
"api",
"mail-poller",
"auth-handler",
"oauth2-proxy",
"cloudflared",
"loki",
"promtail",
"tempo",
"otel-collector",
"prometheus",
"grafana",
"nvidia-exporter",
}
PROMTAIL_NOT_LABELED = {"mc", "wire"}
class TestPromtailLabels:
def test_eligible_services_have_promtail_label(self):
svcs = _services()
missing = []
for name in PROMTAIL_LABELED:
if name not in svcs:
missing.append(f" {name}: not in compose")
continue
labels = svcs[name].get("labels", []) or []
if "promtail=true" not in labels:
missing.append(f" {name}: missing promtail=true label")
assert not missing, "\n".join(missing)
def test_excluded_services_do_not_have_label(self):
svcs = _services()
for name in PROMTAIL_NOT_LABELED:
if name not in svcs:
continue
labels = svcs[name].get("labels", []) or []
assert "promtail=true" not in labels, f"{name} should NOT be labeled"
def test_traefik_access_logs_enabled(self):
traefik = _services()["traefik"]
cmd = traefik.get("command", [])
if isinstance(cmd, str):
cmd = cmd.split()
flat = " ".join(str(x) for x in cmd)
assert "--accesslog" in flat or any(
"TRAEFIK_ACCESSLOG" in str(e) for e in traefik.get("environment", [])
)
class TestGrafanaOAuth:
def test_grafana_loads_state_env_file(self):
env_files = _services()["grafana"].get("env_file", [])
# accepts either a list of dicts or a list of strings
joined = yaml.safe_dump(env_files)
assert ".state/gitea/grafana.env" in joined
def test_grafana_oauth_env_vars(self):
envs = _services()["grafana"].get("environment", [])
text = "\n".join(envs)
assert "GF_AUTH_GENERIC_OAUTH_ENABLED=true" in text
assert "GF_AUTH_GENERIC_OAUTH_NAME=Gitea" in text
assert (
"GF_AUTH_GENERIC_OAUTH_ROLE_ATTRIBUTE_PATH=is_admin && 'GrafanaAdmin'"
in text
)
def test_bootstrap_sso_provisions_grafana(self):
src = (ROOT / "dev/scripts/bootstrap_sso.py").read_text()
assert "GRAFANA_ENV" in src
assert 'name="grafana"' in src
assert "/login/generic_oauth" in src
class TestSubdomainSwap:
def test_stack_toml_subdomains(self):
import tomllib
cfg = tomllib.loads((ROOT / "stack.toml").read_text())
subs = cfg["platform"]["subdomains"]
assert "tempo" in subs
assert "jaeger" not in subs
def test_bootstrap_sso_subdomains(self):
src = (ROOT / "dev/scripts/bootstrap_sso.py").read_text()
assert '"tempo"' in src
assert '"jaeger"' not in src
def test_traefik_route_swap(self):
src = (ROOT / "infra/traefik/dynamic/services.yml").read_text()
assert '"tempo"' in src
assert '"jaeger"' not in src
class TestJaegerRemoved:
def test_jaeger_service_gone(self):
assert "jaeger" not in _services()
EXPECTED_DASHBOARDS = {
"homelab-overview",
"gateway-auth",
"data-lake",
"data-pipelines",
"ci",
"gpu-notebooks",
}
class TestDashboards:
def test_all_dashboards_exist_and_parse(self):
dash_dir = ROOT / "infra/grafana/dashboards"
files = {p.stem for p in dash_dir.glob("*.json")}
missing = EXPECTED_DASHBOARDS - files
assert not missing, f"missing dashboards: {missing}"
for name in EXPECTED_DASHBOARDS:
with (dash_dir / f"{name}.json").open() as f:
data = json.load(f)
assert "panels" in data, f"{name}: no panels[]"
assert data["panels"], f"{name}: empty panels[]"