Skip to content

Commit

Permalink
Add collapsable INFO logging widget
Browse files Browse the repository at this point in the history
  • Loading branch information
marcelldls committed Oct 14, 2024
1 parent d12db4f commit 3f72336
Show file tree
Hide file tree
Showing 7 changed files with 137 additions and 75 deletions.
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
26 changes: 21 additions & 5 deletions src/edge_containers_cli/cmds/argo_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +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 @@ -16,6 +16,7 @@
from edge_containers_cli.git import set_values
from edge_containers_cli.globals import TIME_FORMAT
from edge_containers_cli.shell import ShellError, shell
from edge_containers_cli.logging import log


def extract_ns_app(target: str) -> tuple[str, str]:
Expand All @@ -26,8 +27,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 as e:
if attempt == max_attempts:
log.debug(f"Argo patch failed after {max_attempts} attempts")
raise

Check warning on line 42 in src/edge_containers_cli/cmds/argo_commands.py

View check run for this annotation

Codecov / codecov/patch

src/edge_containers_cli/cmds/argo_commands.py#L39-L42

Added lines #L39 - L42 were not covered by tests
else:
log.debug(f"Argo patch attempt {attempt} failed. Retrying...")
sleep(sleep_time)
attempt += 1

Check warning on line 46 in src/edge_containers_cli/cmds/argo_commands.py

View check run for this annotation

Codecov / codecov/patch

src/edge_containers_cli/cmds/argo_commands.py#L44-L46

Added lines #L44 - L46 were not covered by tests


def push_value(target: str, key: str, value: str | bool | int):
Expand Down Expand Up @@ -84,14 +100,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
27 changes: 25 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,29 @@ 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 +126,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
19 changes: 11 additions & 8 deletions src/edge_containers_cli/cmds/demo_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from edge_containers_cli.cmds.commands import CommandError, Commands, ServicesDataFrame
from edge_containers_cli.definitions import ECContext
from edge_containers_cli.globals import TIME_FORMAT
from edge_containers_cli.logging import log

DELAY = 0.0

Expand All @@ -37,19 +38,15 @@ def process_t(time_string) -> str:
def demo_wrapper():
"""Using closure to display once"""
called = False

def decorator(function):
"""Called selectively to avoid breaking autocompletion"""

def wrapper(*args, **kwargs):
nonlocal called
if not called:
called = True
print("***RUNNING IN DEMO MODE***")
return function(*args, **kwargs)

return wrapper

return decorator


Expand Down Expand Up @@ -83,11 +80,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 +98,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
113 changes: 64 additions & 49 deletions src/edge_containers_cli/cmds/monitor.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,33 +10,35 @@
from rich.style import Style
from rich.syntax import Syntax
from rich.text import Text
import logging

# from textual import on
from textual import work
from textual.app import App, ComposeResult
from textual import work, on
from textual.app import App, ComposeResult, RenderResult
from textual.binding import Binding
from textual.color import Color
from textual.containers import Grid
from textual.containers import Grid, Vertical, ScrollableContainer
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, DataTable, Footer, Header, Label, RichLog, Collapsible, Static, Label
from textual.widgets.data_table import RowKey

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


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 +52,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 +346,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,7 +384,8 @@ class MonitorApp(App):
Binding("r", "restart_ioc", "Restart IOC"),
Binding("l", "ioc_logs", "IOC Logs"),
Binding("o", "sort", "Sort"),
Binding("d", "toggle_dark", "Toggle dark mode"),
Binding("m", "monitor_logs", "Monitor logs", show=False),
#Binding("d", "toggle_dark", "Toggle dark mode"),
]

def __init__(
Expand All @@ -402,8 +402,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)
with Static(id="collapsible_container"):
yield Collapsible(
MonitorLogs(),
title="Monitor Logs (m)",
collapsed=True
)
yield Footer()

def on_mount(self) -> None:
Expand Down Expand Up @@ -446,9 +454,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 +465,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 +478,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 +507,13 @@ 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

0 comments on commit 3f72336

Please sign in to comment.