diff --git a/ariadne/services/hermes_autotriage_evidence.py b/ariadne/services/hermes_autotriage_evidence.py index 674ace4..3ac8859 100644 --- a/ariadne/services/hermes_autotriage_evidence.py +++ b/ariadne/services/hermes_autotriage_evidence.py @@ -20,6 +20,8 @@ _MAX_FAILED_TESTS = 10 _MAX_ERROR_DETAILS_CHARS = 2000 _FAILED_TEST_STATUSES = {"FAILED", "REGRESSION"} _TEST_REPORT_TREE = "suites[cases[className,name,status,errorDetails]]" +# Jenkins agent pods are where CI failures actually happen, so this is the +# namespace that matters for every job and is always included. _LOG_EXTRA_NAMESPACES = ("jenkins",) @@ -62,7 +64,7 @@ def collect_evidence(incident_id: str, job: str, last_build: dict) -> dict[str, "incident_id": incident_id, "generated_at": datetime.now(timezone.utc).isoformat(), "jenkins": jenkins, - "log_evidence": collect_log_evidence(_log_config(), incident_id, window_start, window_end), + "log_evidence": collect_log_evidence(_log_config(job), incident_id, window_start, window_end), } @@ -218,13 +220,24 @@ def _client_kwargs() -> dict[str, Any]: return kwargs -def _log_config() -> dict[str, Any]: - """Build the config passed to the frozen OpenSearch log collector.""" +def _log_config(job: str = "") -> dict[str, Any]: + """Build the config passed to the frozen OpenSearch log collector. + Inputs: the job under triage, used to add that service's own namespace. + Outputs: the collector cfg. A build failure sometimes correlates with the + service itself being unhealthy, and its runtime logs are the only place + that shows; without a mapping only the demo and Jenkins namespaces are + searched, which says nothing about the service. + """ + + extra = list(_LOG_EXTRA_NAMESPACES) + own = (getattr(settings, "hermes_job_namespaces", None) or {}).get(job) + if own and own not in extra: + extra.append(own) return { "opensearch_url": settings.opensearch_url, "namespace": settings.hermes_demo_namespace, - "extra_namespaces": list(_LOG_EXTRA_NAMESPACES), + "extra_namespaces": extra, } diff --git a/ariadne/settings.py b/ariadne/settings.py index ce6d0ce..9c163e9 100644 --- a/ariadne/settings.py +++ b/ariadne/settings.py @@ -191,6 +191,7 @@ class Settings: hermes_min_confidence: float hermes_hung_build_minutes: float hermes_max_branches: int + hermes_job_namespaces: dict hermes_max_actions_per_incident: int hermes_api_url: str hermes_api_key: str diff --git a/ariadne/settings_sections.py b/ariadne/settings_sections.py index 25264ba..146cd3c 100644 --- a/ariadne/settings_sections.py +++ b/ariadne/settings_sections.py @@ -291,6 +291,7 @@ def _hermes_autotriage_config() -> dict[str, Any]: "hermes_min_confidence": _env_float("ARIADNE_HERMES_MIN_CONFIDENCE", 0.85), "hermes_hung_build_minutes": _env_float("ARIADNE_HERMES_HUNG_BUILD_MINUTES", 45.0), "hermes_max_branches": _env_int("ARIADNE_HERMES_MAX_BRANCHES", 5), + "hermes_job_namespaces": _pair_map(_env("ARIADNE_HERMES_JOB_NAMESPACES", "")), "hermes_max_actions_per_incident": _env_int("ARIADNE_HERMES_MAX_ACTIONS_PER_INCIDENT", 1), "hermes_api_url": _env( "ARIADNE_HERMES_API_URL", diff --git a/tests/hermes_autotriage_harness.py b/tests/hermes_autotriage_harness.py index 7ffbc5d..fb2f418 100644 --- a/tests/hermes_autotriage_harness.py +++ b/tests/hermes_autotriage_harness.py @@ -60,6 +60,7 @@ def _settings(**overrides) -> SimpleNamespace: # type: ignore[no-untyped-def] "hermes_min_confidence": 0.85, "hermes_hung_build_minutes": 45.0, "hermes_max_branches": 5, + "hermes_job_namespaces": {}, "hermes_max_actions_per_incident": 1, "hermes_api_url": "http://hermes:8642", "hermes_api_key": "key", diff --git a/tests/test_hermes_autotriage_evidence.py b/tests/test_hermes_autotriage_evidence.py index f5f15c6..1547565 100644 --- a/tests/test_hermes_autotriage_evidence.py +++ b/tests/test_hermes_autotriage_evidence.py @@ -340,3 +340,35 @@ def test_signature_absent() -> None: def test_signature_tolerates_malformed_bundle() -> None: assert module.evidence_has_signature({}, INCIDENT_ID) is False + + +def _settings_with(namespaces): # type: ignore[no-untyped-def] + cfg = _settings() + cfg.hermes_job_namespaces = namespaces + return cfg + + +def test_log_namespaces_always_include_jenkins(monkeypatch) -> None: + """CI failures happen in Jenkins agent pods, whatever the service.""" + + monkeypatch.setattr(module, "settings", _settings_with({})) + assert module._log_config("lesavka")["extra_namespaces"] == ["jenkins"] + + +def test_a_mapped_job_also_searches_its_own_namespace(monkeypatch) -> None: + """A build failure can correlate with the service itself being unhealthy.""" + + monkeypatch.setattr(module, "settings", _settings_with({"bstein-dev-home": "bstein-dev-home"})) + spaces = module._log_config("bstein-dev-home")["extra_namespaces"] + assert "bstein-dev-home" in spaces + assert "jenkins" in spaces + + +def test_an_unmapped_job_adds_nothing(monkeypatch) -> None: + monkeypatch.setattr(module, "settings", _settings_with({"other": "other-ns"})) + assert module._log_config("lesavka")["extra_namespaces"] == ["jenkins"] + + +def test_a_namespace_is_never_duplicated(monkeypatch) -> None: + monkeypatch.setattr(module, "settings", _settings_with({"j": "jenkins"})) + assert module._log_config("j")["extra_namespaces"].count("jenkins") == 1