-
Notifications
You must be signed in to change notification settings - Fork 3
feat: add check-services command to verify service runtime status #335
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
SanjeevLakhwani
wants to merge
13
commits into
releases/v21
Choose a base branch
from
feat/check-services-command
base: releases/v21
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 10 commits
Commits
Show all changes
13 commits
Select commit
Hold shift + click to select a range
af12f8c
feat: add check-services command to verify service runtime status
SanjeevLakhwani ac4feb6
refactor: rename check-services to status and decouple from mode
SanjeevLakhwani 1d0811c
perf: make status checks async for parallel execution
SanjeevLakhwani bdc258d
fix: only check configured services in status command
SanjeevLakhwani c038ded
fix: use consistent compose command for status checks
SanjeevLakhwani 2d54783
feat: add health-aware status display with single docker ps call
SanjeevLakhwani 418a81b
feat: alphabetically sort status output
SanjeevLakhwani 16e0112
fix: remove f-string without placeholders for linting
SanjeevLakhwani 631987f
fix(bentoctl): increase character print limit
SanjeevLakhwani 9619ff5
increase character print limit
SanjeevLakhwani e2a1086
refactor(bentoctl): Replace 19 with a constant var
SanjeevLakhwani 089285c
lint: remove whitespace
SanjeevLakhwani 4f9f9c0
lint: remove whitespace
SanjeevLakhwani File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,4 @@ | ||
| import json | ||
| import os | ||
| import pathlib | ||
| import subprocess | ||
|
|
@@ -20,6 +21,7 @@ | |
| "run_as_shell_for_service", | ||
| "logs_service", | ||
| "compose_config", | ||
| "get_services_status", | ||
| ] | ||
|
|
||
| BENTO_SERVICES_DATA_BY_KIND = { | ||
|
|
@@ -32,8 +34,8 @@ | |
| "auth", | ||
| "authz-db", | ||
| "gateway", | ||
| "garage", | ||
| "katsu-db", | ||
| "minio", | ||
| "redis", | ||
| "reference-db", | ||
| ) | ||
|
|
@@ -299,7 +301,7 @@ def mode_service(compose_service: str) -> None: | |
| else: | ||
| mode += "\t(dev)" | ||
|
|
||
| print(f"{compose_service[:18].rjust(18)} ", end="") | ||
| print(f"{compose_service[:19].rjust(19)} ", end="") | ||
| cprint(mode, colour) | ||
|
|
||
|
|
||
|
|
@@ -407,3 +409,165 @@ def logs_service(compose_service: str, follow: bool) -> None: | |
|
|
||
| def compose_config(services_flag: bool) -> None: | ||
| os.execvp(c.COMPOSE[0], (*c.COMPOSE, "config", *(("--services",) if services_flag else ()))) | ||
|
|
||
|
|
||
| def _parse_health_status(status_text: str) -> Literal["healthy", "starting", "unhealthy", "none"]: | ||
| """ | ||
| Parse health status from Docker status text. | ||
|
|
||
| Examples: | ||
| - "Up 5 hours (healthy)" → "healthy" | ||
| - "Up 2 minutes (health: starting)" → "starting" | ||
| - "Up 10 minutes (unhealthy)" → "unhealthy" | ||
| - "Up 1 hour" → "none" (no healthcheck configured) | ||
| """ | ||
| status_lower = status_text.lower() | ||
|
|
||
| if "(healthy)" in status_lower: | ||
| return "healthy" | ||
| elif "health: starting" in status_lower or "(starting)" in status_lower: | ||
| return "starting" | ||
| elif "(unhealthy)" in status_lower: | ||
| return "unhealthy" | ||
| else: | ||
| return "none" | ||
|
|
||
|
|
||
| def _fetch_all_service_statuses() -> Dict[str, Tuple[bool, str, str]]: | ||
| """ | ||
| Fetch status for all services in one docker compose ps call. | ||
| Returns a dict mapping service name to (is_running, status_text, health_status). | ||
| """ | ||
| compose_cmd = _get_compose_with_files(dev=c.DEV_MODE) | ||
| result = subprocess.run( | ||
| (*compose_cmd, "ps", "--format", "json"), | ||
| capture_output=True, | ||
| text=True, | ||
| ) | ||
|
|
||
| if result.returncode != 0: | ||
| err(f" Failed to fetch service statuses: {result.stderr.strip()}") | ||
| exit(1) | ||
|
|
||
| def _load_entry(raw: str): | ||
| try: | ||
| return json.loads(raw) | ||
| except json.JSONDecodeError: | ||
| return None | ||
|
|
||
| stdout_str = result.stdout.strip() | ||
| entries: list = [] | ||
|
|
||
| if not stdout_str: | ||
| return {} | ||
|
|
||
| try: | ||
| parsed = json.loads(stdout_str) | ||
| except json.JSONDecodeError: | ||
| entries = [_load_entry(line) for line in stdout_str.splitlines() if line.strip()] | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. there is another occurrence of this same type of comprehension (albeit formatted differently) on line 530 - the split by newline + if strip. this could maybe be moved into a little generator file-private function helper, or at least both lines should consistently use splitlines |
||
| entries = [e for e in entries if e is not None] | ||
| else: | ||
| if isinstance(parsed, list): | ||
| entries = [ | ||
| _load_entry(entry) if isinstance(entry, str) else entry | ||
| for entry in parsed | ||
| ] | ||
| entries = [e for e in entries if e is not None] | ||
| elif isinstance(parsed, dict): | ||
| entries = [parsed] | ||
| else: | ||
| err(" Unexpected docker compose status output.") | ||
| exit(1) | ||
|
|
||
| # Build dict mapping service name to status | ||
| status_dict = {} | ||
| for entry in entries: | ||
| service_name = entry.get("Service") or entry.get("Name", "").split("-")[-1] | ||
| if not service_name: | ||
| continue | ||
|
|
||
| is_running = entry.get("State") == "running" | ||
| status_text = entry.get("Status") or entry.get("State") or "" | ||
| health_status = _parse_health_status(status_text) | ||
| status_dict[service_name] = (is_running, status_text, health_status) | ||
|
|
||
| return status_dict | ||
|
|
||
|
|
||
| def _print_service_status(service: str, running: bool, status_text: str, health_status: str) -> bool: | ||
| """Print service status with color coding. Returns whether service is running.""" | ||
| print(f"{service[:19].rjust(19)} ", end="") | ||
|
|
||
| # Determine color and prefix based on health status | ||
| if not running: | ||
| colour = "red" | ||
| prefix = "not running" | ||
| elif health_status == "unhealthy": | ||
| colour = "red" | ||
| prefix = "unhealthy" | ||
| elif health_status == "starting": | ||
| colour = "yellow" | ||
| prefix = "starting" | ||
| else: # healthy or none | ||
| colour = "green" | ||
| prefix = "running" | ||
|
|
||
| suffix = f" ({status_text})" if status_text else "" | ||
| cprint(f"{prefix}{suffix}", colour) | ||
| return running | ||
|
|
||
|
|
||
| def _get_configured_services() -> list[str]: | ||
| """Get the list of services that are actually configured in the current compose setup.""" | ||
| result = subprocess.run( | ||
| (*_get_compose_with_files(dev=c.DEV_MODE), "config", "--services"), | ||
| capture_output=True, | ||
| text=True, | ||
| ) | ||
| if result.returncode != 0: | ||
| err(f" Failed to get configured services: {result.stderr.strip()}") | ||
| exit(1) | ||
| services = [s.strip() for s in result.stdout.strip().split('\n') if s.strip()] | ||
| return services | ||
|
|
||
|
|
||
| def get_services_status(compose_service: str) -> None: | ||
| compose_service = translate_service_aliases(compose_service) | ||
|
|
||
| all_statuses = _fetch_all_service_statuses() | ||
|
|
||
| if compose_service == c.SERVICE_LITERAL_ALL: | ||
| configured_services = _get_configured_services() | ||
|
|
||
| results = [] | ||
| for service in sorted(configured_services): | ||
| if service in all_statuses: | ||
| running, status_text, health_status = all_statuses[service] | ||
| is_running = _print_service_status(service, running, status_text, health_status) | ||
| results.append(is_running) | ||
| else: | ||
| # Service is configured but not found in docker ps output | ||
| print(f"{service[:19].rjust(19)} ", end="") | ||
| cprint("not found (No containers)", "red") | ||
| results.append(False) | ||
|
|
||
| if all(results): | ||
| info("All services appear to be running.") | ||
| return | ||
| err("One or more services are not running.") | ||
| exit(1) | ||
|
|
||
| # Single service check | ||
| check_service_is_compose(compose_service) | ||
|
|
||
| if compose_service in all_statuses: | ||
| running, status_text, health_status = all_statuses[compose_service] | ||
| is_running = _print_service_status(compose_service, running, status_text, health_status) | ||
| if not is_running: | ||
| err(f"{compose_service} is not running.") | ||
| exit(1) | ||
| else: | ||
| print(f"{compose_service[:19].rjust(19)} ", end="") | ||
| cprint("not found (No containers)", "red") | ||
| err(f"{compose_service} is not running.") | ||
| exit(1) | ||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hmm