From a92c7a7fd1299b91fcfc5e1aae1674a2ff4744c3 Mon Sep 17 00:00:00 2001 From: arkhan Date: Wed, 9 Jul 2025 16:43:12 -0500 Subject: [PATCH 1/2] feat(podman-logs): improve logs formatting and behavior like docker-compose --- podman_compose.py | 219 ++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 194 insertions(+), 25 deletions(-) diff --git a/podman_compose.py b/podman_compose.py index b4b24d54..17274016 100755 --- a/podman_compose.py +++ b/podman_compose.py @@ -3215,7 +3215,28 @@ async def compose_up(compose: PodmanCompose, args: argparse.Namespace) -> int | max_service_length = 0 for cnt in compose.containers: - curr_length = len(cnt["_service"]) + # Saltar contenedores excluidos + if cnt["_service"] in excluded: + continue + + service_name = cnt["_service"] + container_name = cnt["name"] + + if getattr(args, 'names', False): + # Con -n: mostrar solo servicio_numero (sin prefijo de proyecto) + expected_name = compose.format_name(service_name, str(cnt["num"])) + + if container_name == expected_name: + # Es un nombre generado automáticamente, mostrar solo servicio_numero + display_name = compose.join_name_parts(service_name, str(cnt["num"])) + else: + # Es un container_name personalizado, usarlo tal como está + display_name = container_name + else: + # Sin -n: mostrar nombre completo del contenedor (comportamiento por defecto) + display_name = container_name + + curr_length = len(display_name) max_service_length = curr_length if curr_length > max_service_length else max_service_length tasks: set[asyncio.Task] = set() @@ -3237,11 +3258,33 @@ async def handle_sigint() -> None: loop.add_signal_handler(signal.SIGINT, lambda: asyncio.create_task(handle_sigint())) for i, cnt in enumerate(compose.containers): - # Add colored service prefix to output by piping output through sed + # Add colored service prefix to output like docker-compose color_idx = i % len(compose.console_colors) color = compose.console_colors[color_idx] - space_suffix = " " * (max_service_length - len(cnt["_service"]) + 1) - log_formatter = "{}[{}]{}|\x1b[0m".format(color, cnt["_service"], space_suffix) + + # Determinar el nombre a mostrar + service_name = cnt["_service"] + container_name = cnt["name"] + + if getattr(args, 'names', False): + # Con -n: mostrar solo servicio_numero (sin prefijo de proyecto) + expected_name = compose.format_name(service_name, str(cnt["num"])) + + if container_name == expected_name: + # Es un nombre generado automáticamente, mostrar solo servicio_numero + display_name = compose.join_name_parts(service_name, str(cnt["num"])) + else: + # Es un container_name personalizado, usarlo tal como está + display_name = container_name + else: + # Sin -n: mostrar nombre completo del contenedor (comportamiento por defecto) + display_name = container_name + + # Calcular espacios para alinear el | exactamente + # max_service_length + 1 espacio, menos la longitud del display_name actual + space_suffix = " " * (max_service_length + 1 - len(display_name)) + log_formatter = "{}{}{}|\x1b[0m".format(color, display_name, space_suffix) + if cnt["_service"] in excluded: log.debug("** skipping: %s", cnt["name"]) continue @@ -3596,29 +3639,149 @@ async def compose_logs(compose: PodmanCompose, args: argparse.Namespace) -> None if not args.services and not args.latest: args.services = container_names_by_service.keys() compose.assert_services(args.services) + targets = [] + service_by_container = {} + for service in args.services: - targets.extend(container_names_by_service[service]) - podman_args = [] - if args.follow: - podman_args.append("-f") - if args.latest: - podman_args.append("-l") - if args.names: - podman_args.append("-n") - if args.since: - podman_args.extend(["--since", args.since]) - # the default value is to print all logs which is in podman = 0 and not - # needed to be passed - if args.tail and args.tail != "all": - podman_args.extend(["--tail", args.tail]) - if args.timestamps: - podman_args.append("-t") - if args.until: - podman_args.extend(["--until", args.until]) - for target in targets: - podman_args.append(target) - await compose.podman.run([], "logs", podman_args) + containers = container_names_by_service[service] + targets.extend(containers) + for container in containers: + service_by_container[container] = service + + should_use_colors = ( + (len(args.services) > 1 or args.names) + and not args.latest + and sys.stdout.isatty() + and not getattr(args, "no_color", False) + ) + + if should_use_colors: + # Calcular la longitud máxima para alineación, igual que en compose_up + max_service_length = 0 + for target in targets: + cnt = compose.container_by_name[target] + service_name = cnt["_service"] + container_name = cnt["name"] + + if getattr(args, 'names', False): + # Con -n: mostrar solo servicio_numero (sin prefijo de proyecto) + expected_name = compose.format_name(service_name, str(cnt["num"])) + + if container_name == expected_name: + # Es un nombre generado automáticamente, mostrar solo servicio_numero + display_name = compose.join_name_parts(service_name, str(cnt["num"])) + else: + # Es un container_name personalizado, usarlo tal como está + display_name = container_name + else: + # Sin -n: mostrar nombre completo del contenedor (comportamiento por defecto) + display_name = container_name + + curr_length = len(display_name) + max_service_length = ( + curr_length if curr_length > max_service_length else max_service_length + ) + + tasks = [] + service_colors = {} + + for target in targets: + cnt = compose.container_by_name[target] + service_name = cnt["_service"] + container_name = cnt["name"] + + # Aplicar la misma lógica de display_name que en compose_up + if getattr(args, 'names', False): + # Con -n: mostrar solo servicio_numero (sin prefijo de proyecto) + expected_name = compose.format_name(service_name, str(cnt["num"])) + + if container_name == expected_name: + # Es un nombre generado automáticamente, mostrar solo servicio_numero + display_name = compose.join_name_parts(service_name, str(cnt["num"])) + else: + # Es un container_name personalizado, usarlo tal como está + display_name = container_name + else: + # Sin -n: mostrar nombre completo del contenedor (comportamiento por defecto) + display_name = container_name + + # Asignar color por servicio (no por contenedor individual) + if service_name not in service_colors: + color_idx = len(service_colors) % len(compose.console_colors) + service_colors[service_name] = compose.console_colors[color_idx] + + color = service_colors[service_name] + + # Calcular espacios para alinear el | exactamente, igual que en compose_up + # max_service_length + 1 espacio, menos la longitud del display_name actual + space_suffix = " " * (max_service_length + 1 - len(display_name)) + log_formatter = "{}{}{}|\x1b[0m".format(color, display_name, space_suffix) + + podman_args = [] + if args.follow: + podman_args.append("-f") + if args.names: + podman_args.append("-n") + if args.since: + podman_args.extend(["--since", args.since]) + if args.tail and args.tail != "all": + podman_args.extend(["--tail", args.tail]) + if args.timestamps: + podman_args.append("-t") + if args.until: + podman_args.extend(["--until", args.until]) + podman_args.append(target) + + task = asyncio.create_task( + compose.podman.run([], "logs", podman_args, log_formatter=log_formatter), + name=f"logs-{service_name}-{target}", + ) + tasks.append(task) + + async def handle_sigint() -> None: + log.info("Caught SIGINT or Ctrl+C, stopping log streaming...") + for task in tasks: + if not task.done(): + task.cancel() + + if sys.platform != 'win32': + loop = asyncio.get_event_loop() + loop.add_signal_handler(signal.SIGINT, lambda: asyncio.create_task(handle_sigint())) + + try: + await asyncio.gather(*tasks) + except KeyboardInterrupt: + for task in tasks: + if not task.done(): + task.cancel() + await asyncio.gather(*tasks, return_exceptions=True) + except Exception as e: + log.error("Error in logs command: %s", e) + for task in tasks: + if not task.done(): + task.cancel() + await asyncio.gather(*tasks, return_exceptions=True) + raise + else: + podman_args = [] + if args.follow: + podman_args.append("-f") + if args.latest: + podman_args.append("-l") + if args.names: + podman_args.append("-n") + if args.since: + podman_args.extend(["--since", args.since]) + if args.tail and args.tail != "all": + podman_args.extend(["--tail", args.tail]) + if args.timestamps: + podman_args.append("-t") + if args.until: + podman_args.extend(["--until", args.until]) + for target in targets: + podman_args.append(target) + await compose.podman.run([], "logs", podman_args) @cmd_run(podman_compose, "config", "displays the compose file") @@ -3877,6 +4040,12 @@ def compose_up_parse(parser: argparse.ArgumentParser) -> None: help="Return the exit code of the selected service container. " "Implies --abort-on-container-exit.", ) + parser.add_argument( + "-n", + "--names", + action="store_true", + help="Show short service names instead of full container names in logs", + ) @cmd_parse(podman_compose, "down") From f76bda4a1560576f6dc2e7ad0e4e9af9ce2819c8 Mon Sep 17 00:00:00 2001 From: arkhan Date: Wed, 9 Jul 2025 17:15:43 -0500 Subject: [PATCH 2/2] fix: remove comments and add annotation type --- podman_compose.py | 31 +------------------------------ 1 file changed, 1 insertion(+), 30 deletions(-) diff --git a/podman_compose.py b/podman_compose.py index 17274016..a8acb0f5 100755 --- a/podman_compose.py +++ b/podman_compose.py @@ -3215,7 +3215,6 @@ async def compose_up(compose: PodmanCompose, args: argparse.Namespace) -> int | max_service_length = 0 for cnt in compose.containers: - # Saltar contenedores excluidos if cnt["_service"] in excluded: continue @@ -3223,17 +3222,12 @@ async def compose_up(compose: PodmanCompose, args: argparse.Namespace) -> int | container_name = cnt["name"] if getattr(args, 'names', False): - # Con -n: mostrar solo servicio_numero (sin prefijo de proyecto) expected_name = compose.format_name(service_name, str(cnt["num"])) - if container_name == expected_name: - # Es un nombre generado automáticamente, mostrar solo servicio_numero display_name = compose.join_name_parts(service_name, str(cnt["num"])) else: - # Es un container_name personalizado, usarlo tal como está display_name = container_name else: - # Sin -n: mostrar nombre completo del contenedor (comportamiento por defecto) display_name = container_name curr_length = len(display_name) @@ -3258,30 +3252,21 @@ async def handle_sigint() -> None: loop.add_signal_handler(signal.SIGINT, lambda: asyncio.create_task(handle_sigint())) for i, cnt in enumerate(compose.containers): - # Add colored service prefix to output like docker-compose color_idx = i % len(compose.console_colors) color = compose.console_colors[color_idx] - # Determinar el nombre a mostrar service_name = cnt["_service"] container_name = cnt["name"] if getattr(args, 'names', False): - # Con -n: mostrar solo servicio_numero (sin prefijo de proyecto) expected_name = compose.format_name(service_name, str(cnt["num"])) - if container_name == expected_name: - # Es un nombre generado automáticamente, mostrar solo servicio_numero display_name = compose.join_name_parts(service_name, str(cnt["num"])) else: - # Es un container_name personalizado, usarlo tal como está display_name = container_name else: - # Sin -n: mostrar nombre completo del contenedor (comportamiento por defecto) display_name = container_name - # Calcular espacios para alinear el | exactamente - # max_service_length + 1 espacio, menos la longitud del display_name actual space_suffix = " " * (max_service_length + 1 - len(display_name)) log_formatter = "{}{}{}|\x1b[0m".format(color, display_name, space_suffix) @@ -3657,7 +3642,6 @@ async def compose_logs(compose: PodmanCompose, args: argparse.Namespace) -> None ) if should_use_colors: - # Calcular la longitud máxima para alineación, igual que en compose_up max_service_length = 0 for target in targets: cnt = compose.container_by_name[target] @@ -3665,17 +3649,13 @@ async def compose_logs(compose: PodmanCompose, args: argparse.Namespace) -> None container_name = cnt["name"] if getattr(args, 'names', False): - # Con -n: mostrar solo servicio_numero (sin prefijo de proyecto) expected_name = compose.format_name(service_name, str(cnt["num"])) if container_name == expected_name: - # Es un nombre generado automáticamente, mostrar solo servicio_numero display_name = compose.join_name_parts(service_name, str(cnt["num"])) else: - # Es un container_name personalizado, usarlo tal como está display_name = container_name else: - # Sin -n: mostrar nombre completo del contenedor (comportamiento por defecto) display_name = container_name curr_length = len(display_name) @@ -3684,37 +3664,28 @@ async def compose_logs(compose: PodmanCompose, args: argparse.Namespace) -> None ) tasks = [] - service_colors = {} + service_colors: dict[str, str] = {} for target in targets: cnt = compose.container_by_name[target] service_name = cnt["_service"] container_name = cnt["name"] - # Aplicar la misma lógica de display_name que en compose_up if getattr(args, 'names', False): - # Con -n: mostrar solo servicio_numero (sin prefijo de proyecto) expected_name = compose.format_name(service_name, str(cnt["num"])) - if container_name == expected_name: - # Es un nombre generado automáticamente, mostrar solo servicio_numero display_name = compose.join_name_parts(service_name, str(cnt["num"])) else: - # Es un container_name personalizado, usarlo tal como está display_name = container_name else: - # Sin -n: mostrar nombre completo del contenedor (comportamiento por defecto) display_name = container_name - # Asignar color por servicio (no por contenedor individual) if service_name not in service_colors: color_idx = len(service_colors) % len(compose.console_colors) service_colors[service_name] = compose.console_colors[color_idx] color = service_colors[service_name] - # Calcular espacios para alinear el | exactamente, igual que en compose_up - # max_service_length + 1 espacio, menos la longitud del display_name actual space_suffix = " " * (max_service_length + 1 - len(display_name)) log_formatter = "{}{}{}|\x1b[0m".format(color, display_name, space_suffix)