Skip to content
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

Collapsible INFO logging widget #170

Merged
merged 3 commits into from
Nov 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/edge_containers_cli/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,7 @@ def start(
):
"""Start a service"""
try:
backend.commands.start(service_name, commit)
backend.commands.start(service_name, commit=commit)
except GitError as e:
msg = f"{str(e)} - Commit failed. Try 'ec start <service> --no-commit to set values without updating git"
raise GitError(msg) from e
Expand All @@ -255,7 +255,7 @@ def stop(
):
"""Stop a service"""
try:
backend.commands.stop(service_name, commit)
backend.commands.stop(service_name, commit=commit)
except GitError as e:
msg = f"{str(e)} - Commit failed. Try ec stop <service> --no-commit to set values without updating git"
raise GitError(msg) from e
Expand Down
25 changes: 21 additions & 4 deletions src/edge_containers_cli/cmds/argo_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import webbrowser
from datetime import datetime
from pathlib import Path
from time import sleep

import polars
from ruamel.yaml import YAML
Expand All @@ -15,6 +16,7 @@
from edge_containers_cli.definitions import ECContext
from edge_containers_cli.git import set_values
from edge_containers_cli.globals import TIME_FORMAT
from edge_containers_cli.logging import log
from edge_containers_cli.shell import ShellError, shell


Expand All @@ -26,8 +28,23 @@ def extract_ns_app(target: str) -> tuple[str, str]:
def patch_value(target: str, key: str, value: str | bool | int):
cmd_temp_ = f"argocd app set {target} -p {key}={value}"
shell.run_command(cmd_temp_, skip_on_dryrun=True)
cmd_sync = f"argocd app sync {target} --apply-out-of-sync-only"
shell.run_command(cmd_sync, skip_on_dryrun=True)
max_attempts = 3
attempt = 1
sleep_time = 2
while attempt <= max_attempts:
try:
# Sync may conflict with autosync "another operation is already in progress"
cmd_sync = f"argocd app sync {target} --apply-out-of-sync-only"
shell.run_command(cmd_sync, skip_on_dryrun=True)
return None
except ShellError:
if attempt == max_attempts:
log.debug(f"Argo patch failed after {max_attempts} attempts")
raise
else:
log.debug(f"Argo patch attempt {attempt} failed. Retrying...")
sleep(sleep_time)
attempt += 1


def push_value(target: str, key: str, value: str | bool | int):
Expand Down Expand Up @@ -84,14 +101,14 @@ def restart(self, service_name):
)
shell.run_command(cmd, skip_on_dryrun=True)

def start(self, service_name, commit):
def start(self, service_name, commit=False):
self._check_service(service_name)
if commit:
push_value(self.target, f"ec_services.{service_name}.enabled", True)
else:
patch_value(self.target, f"ec_services.{service_name}.enabled", True)

def stop(self, service_name, commit):
def stop(self, service_name, commit=False):
self._check_service(service_name)
if commit:
push_value(self.target, f"ec_services.{service_name}.enabled", False)
Expand Down
32 changes: 30 additions & 2 deletions src/edge_containers_cli/cmds/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,34 @@ def __init__(self, ctx: ECContext):
self._repo = ctx.repo
self._log_url = ctx.log_url

def __getattribute__(self, attr_name):
"""Override to wrap each abstract method"""
attr = super().__getattribute__(attr_name)
if callable(attr):
if attr_name[0] == "_":
log_f = log.debug
else:
log_f = log.info

def wrapper(*args, **kwargs):
call_msg = (
f"Calling: {attr_name} {', '.join(str(item) for item in args)}"
)
return_msg = (
f"Returned: {attr_name} {', '.join(str(item) for item in args)}"
)
if kwargs:
call_msg = f"{call_msg} [{', '.join(f'{k}={v}' for k, v in kwargs.items())}]"
return_msg = f"{return_msg} [{', '.join(f'{k}={v}' for k, v in kwargs.items())}]"
log_f(call_msg)
result = attr(*args, **kwargs)
log_f(return_msg)
return result

return wrapper
else:
return attr

@property
def target(self):
if not self._target_valid: # Only validate once
Expand Down Expand Up @@ -103,11 +131,11 @@ def restart(self, service_name: str) -> None:
raise NotImplementedError

@abstractmethod
def start(self, service_name: str, commit: bool) -> None:
def start(self, service_name: str, commit: bool = False) -> None:
raise NotImplementedError

@abstractmethod
def stop(self, service_name: str, commit: bool) -> None:
def stop(self, service_name: str, commit: bool = False) -> None:
raise NotImplementedError

def template(self, svc_instance: Path, args: str) -> None:
Expand Down
14 changes: 10 additions & 4 deletions src/edge_containers_cli/cmds/demo_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,11 +83,14 @@ def ps(self, running_only):

@demo_message
def restart(self, service_name):
self.stop(service_name, False)
self.start(service_name, False)
self._stop(service_name, commit=False)
self._start(service_name, commit=False)

@demo_message
def start(self, service_name, commit):
def start(self, service_name, commit=False):
self._start(service_name, commit=commit)

def _start(self, service_name, commit=False):
self._check_service(service_name)
time.sleep(DELAY)
self._stateDF = self._stateDF.with_columns(
Expand All @@ -98,7 +101,10 @@ def start(self, service_name, commit):
)

@demo_message
def stop(self, service_name, commit):
def stop(self, service_name, commit=False):
self._stop(service_name, commit=commit)

def _stop(self, service_name, commit=False):
self._check_service(service_name)
time.sleep(DELAY)
self._stateDF = self._stateDF.with_columns(
Expand Down
4 changes: 2 additions & 2 deletions src/edge_containers_cli/cmds/k8s_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,14 +89,14 @@ def restart(self, service_name):
f"kubectl delete -n {self.target} {pod_name}", skip_on_dryrun=True
)

def start(self, service_name, commit):
def start(self, service_name, commit=False):
self._check_service(service_name)
shell.run_command(
f"kubectl scale -n {self.target} statefulset {service_name} --replicas=1",
skip_on_dryrun=True,
)

def stop(self, service_name, commit):
def stop(self, service_name, commit=False):
self._check_service(service_name)
shell.run_command(
f"kubectl scale -n {self.target} statefulset {service_name} --replicas=0 ",
Expand Down
117 changes: 69 additions & 48 deletions src/edge_containers_cli/cmds/monitor.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""TUI monitor for containerised IOCs."""

import logging
from collections.abc import Callable
from functools import total_ordering
from threading import Thread
Expand All @@ -10,33 +11,42 @@
from rich.style import Style
from rich.syntax import Syntax
from rich.text import Text

# from textual import on
from textual import work
from textual import on, work
from textual.app import App, ComposeResult
from textual.binding import Binding
from textual.color import Color
from textual.containers import Grid
from textual.containers import Grid, ScrollableContainer, Vertical
from textual.reactive import reactive
from textual.screen import ModalScreen
from textual.widget import Widget
from textual.widgets import Button, DataTable, Footer, Header, Label, RichLog
from textual.widgets import (
Button,
Collapsible,
DataTable,
Footer,
Header,
Label,
RichLog,
Static,
)
from textual.widgets.data_table import RowKey

from edge_containers_cli.cmds.commands import Commands
from edge_containers_cli.definitions import ECLogLevels
from edge_containers_cli.logging import log


class OptionScreen(ModalScreen[bool], inherit_bindings=False):
class ConfirmScreen(ModalScreen[bool], inherit_bindings=False):
BINDINGS = [
Binding("y,enter", "option_yes", "Yes"),
Binding("n,c,escape", "option_cancel", "Cancel"),
]

def __init__(self, service_name: str) -> None:
def __init__(self, service_name: str, type_action: str) -> None:
super().__init__()

self.service_name = service_name
self.type_action = "stop"
self.type_action = type_action

def compose(self) -> ComposeResult:
yield Grid(
Expand All @@ -50,46 +60,15 @@ def compose(self) -> ComposeResult:
)
yield Footer()

def on_button_pressed(self, event: Button.Pressed) -> None:
if event.button.id == "yes":
self.action_option_yes()
else:
self.action_option_cancel()

@on(Button.Pressed, "#yes")
def action_option_yes(self) -> None:
self.dismiss(True)

@on(Button.Pressed, "#cancel")
def action_option_cancel(self) -> None:
self.dismiss(False)


class StartScreen(OptionScreen):
"""Screen with dialog to start service."""

def __init__(self, service_name: str) -> None:
super().__init__(service_name)

self.type_action = "start"


class StopScreen(OptionScreen):
"""Screen with dialog to stop service."""

def __init__(self, service_name: str) -> None:
super().__init__(service_name)

self.type_action = "stop"


class RestartScreen(OptionScreen):
"""Screen with dialog to restart service."""

def __init__(self, service_name: str) -> None:
super().__init__(service_name)

self.type_action = "restart"


class LogsScreen(ModalScreen, inherit_bindings=False):
"""Screen to display IOC logs."""

Expand Down Expand Up @@ -375,6 +354,34 @@ async def populate_table(self) -> None:
table.sort(self.sort_column_id, reverse=False)


class MonitorLogHandler(logging.Handler):
def __init__(self, rich_log: RichLog):
super().__init__()
self.rich_log = rich_log

def emit(self, record: logging.LogRecord):
log_entry = self.format(record)
self.rich_log.write(Text(log_entry))


class MonitorLogs(Static):
"""Widget to display the monitor logs."""

def __init__(self) -> None:
super().__init__()

def compose(self) -> ComposeResult:
yield RichLog(max_lines=25)

def on_mount(self) -> None:
rich_log = self.query_one(RichLog)
handler = MonitorLogHandler(rich_log)
handler.setFormatter(logging.Formatter("%(asctime)s - %(message)s"))
log.addHandler(handler)
log.removeHandler(log.handlers[0]) # Cut noise from main handler
log.setLevel(ECLogLevels.INFO.value)


class MonitorApp(App):
CSS_PATH = "monitor.tcss"

Expand All @@ -385,6 +392,7 @@ class MonitorApp(App):
Binding("r", "restart_ioc", "Restart IOC"),
Binding("l", "ioc_logs", "IOC Logs"),
Binding("o", "sort", "Sort"),
Binding("m", "monitor_logs", "Monitor logs", show=False),
# Binding("d", "toggle_dark", "Toggle dark mode"),
]

Expand All @@ -402,8 +410,16 @@ def __init__(
def compose(self) -> ComposeResult:
"""Create child widgets for the app."""
yield Header(show_clock=True)
self.table = IocTable(self.commands, self.running_only)
yield self.table
with Vertical():
with Static(id="ioc_table_container"):
self.table = IocTable(self.commands, self.running_only)
yield ScrollableContainer(self.table)
yield Collapsible(
MonitorLogs(),
title="Monitor Logs (m)",
collapsed=True,
id="collapsible_container",
)
yield Footer()

def on_mount(self) -> None:
Expand Down Expand Up @@ -446,9 +462,9 @@ def action_start_ioc(self) -> None:
def check_start(start: bool | None) -> None:
"""Called when StartScreen is dismissed."""
if start:
self.commands.start(service_name, False)
self.commands.start(service_name, commit=False)

self.push_screen(StartScreen(service_name), check_start)
self.push_screen(ConfirmScreen(service_name, "start"), check_start)

def action_stop_ioc(self) -> None:
"""Stop the IOC that is currently highlighted."""
Expand All @@ -457,9 +473,9 @@ def action_stop_ioc(self) -> None:
def check_stop(stop: bool | None) -> None:
"""Called when StopScreen is dismissed."""
if stop:
self.commands.stop(service_name, False)
self.commands.stop(service_name, commit=False)

self.push_screen(StopScreen(service_name), check_stop)
self.push_screen(ConfirmScreen(service_name, "stop"), check_stop)

def action_restart_ioc(self) -> None:
"""Restart the IOC that is currently highlighted."""
Expand All @@ -470,7 +486,7 @@ def check_restart(restart: bool | None) -> None:
if restart:
self.commands.restart(service_name)

self.push_screen(RestartScreen(service_name), check_restart)
self.push_screen(ConfirmScreen(service_name, "restart"), check_restart)

def action_ioc_logs(self) -> None:
"""Display the logs of the IOC that is currently highlighted."""
Expand Down Expand Up @@ -499,6 +515,11 @@ def action_sort(self, col_name: str = "") -> None:
new_col = cols[0 if col_index + 1 > 3 else col_index + 1]
self.update_sort_key(new_col)

def action_monitor_logs(self) -> None:
"""Get a new hello and update the content area."""
collapsed_state = self.query_one(Collapsible).collapsed
self.query_one(Collapsible).collapsed = not collapsed_state

def update_sort_key(self, col_name: str) -> None:
"""Method called to update the table sort key attribute."""
table = self.query_one(IocTable)
Expand Down
Loading