Make service health-check self-diagnosing
build / build (push) Failing after 9s
CI / compile (push) Successful in 9s
CI / unit (push) Successful in 19s
CI / integration (push) Successful in 14s

When a service group's cog stays disabled the log now says WHY:
_service_health returns the actual connection error (ConnectionError
= refused/down, ConnectTimeout = firewall/slow, gaierror = DNS/wrong
host) instead of a bare 'unreachable', and on_ready logs the resolved
FILE/LIBRARIAN/RADIO addresses so an env-var that never reached the
process (address falls back to the 192.168.1.15:5000 default) is
obvious at a glance.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit was merged in pull request #9.
This commit is contained in:
2026-08-01 13:49:23 +02:00
committed by gitea
parent defc482a22
commit 442b8a2a60
+22 -8
View File
@@ -120,13 +120,15 @@ SERVICE_EXTENSION_GROUPS = {
SERVICE_RECHECK_SECONDS = 300 SERVICE_RECHECK_SECONDS = 300
def _service_alive(url: str) -> bool: def _service_health(url: str):
"""True when the service answers HTTP at all (any status code counts).""" """Return None when the service answers HTTP at all (any status counts),
otherwise the connection error explaining WHY it's unreachable (refused vs
timeout vs DNS - the difference points straight at the cause)."""
try: try:
requests.get(url, timeout=3) requests.get(url, timeout=3)
return True return None
except requests.exceptions.RequestException: except requests.exceptions.RequestException as exc:
return False return exc
async def _load_extension_safe(name: str) -> bool: async def _load_extension_safe(name: str) -> bool:
@@ -152,12 +154,14 @@ async def _load_service_groups() -> bool:
missing = [e for e in group["extensions"] if e not in client.extensions] missing = [e for e in group["extensions"] if e not in client.extensions]
if not missing: if not missing:
continue continue
alive = await asyncio.to_thread(_service_alive, group["health_url"]) err = await asyncio.to_thread(_service_health, group["health_url"])
if not alive: if err is not None:
logger.warning( logger.warning(
"Service '%s' unreachable (%s) - cogs stay disabled: %s", "Service '%s' unreachable (%s) [%s: %s] - cogs stay disabled: %s",
service, service,
group["health_url"], group["health_url"],
type(err).__name__,
err,
", ".join(missing), ", ".join(missing),
) )
continue continue
@@ -204,6 +208,16 @@ async def on_ready():
for extension in CORE_EXTENSIONS: for extension in CORE_EXTENSIONS:
await _load_extension_safe(extension) await _load_extension_safe(extension)
# Log the ACTUALLY-resolved service addresses. When one shows the built-in
# default (192.168.1.15:5000) it means the matching CONJURER_* env var never
# reached the process - the single most common cause of "service unreachable"
# confusion. Printing them makes env-vs-default obvious at a glance.
logger.info(
"Resolved service addresses -> musician(file): %s | librarian: %s | radio: %s",
FILE_SERVICE_ADDRESS,
LIBRARIAN_SERVICE_ADDRESS,
RADIO_SERVICE_ADDRESS,
)
await _load_service_groups() await _load_service_groups()
logger.info("Sensors: online") logger.info("Sensors: online")