diff --git a/README.md b/README.md index a541d71..b5bd155 100644 --- a/README.md +++ b/README.md @@ -92,7 +92,7 @@ options: --panels What panels to display on startup separated by a comma. Supports: dashboard/processlist/graphs/replication/locks/ddl [default: dashboard,processlist] --graph-marker What marker to use for graphs (available options: https://tinyurl.com/dolphie-markers) [default: braille] --pypi-repository What PyPi repository to use when checking for a new version. If not specified, it will use Dolphie's PyPi repository - --hostgroup This is used for creating tabs and connecting to them for hosts you specify in Dolphie's config file under a hostgroup section. As an example, you'll have a section called [cluster1] then below it will be listed each host on a new line in the format key=host where key can be anything you want + --hostgroup This is used for creating tabs and connecting to them for hosts you specify in Dolphie's config file under a hostgroup section. As an example, you'll have a section called [cluster1] then below it will be listed each host on a new line in the format key=host where key can be anything you want. Hosts support optional port in the format host:port --show-trxs-only Start with only showing threads that have an active transaction --additional-columns Start with additional columns in Processlist panel --historical-locks Always run the locks query so it can save historical data to its graph instead of only when the Locks panel is open. This query can be expensive in some environments diff --git a/dolphie/Modules/ArgumentParser.py b/dolphie/Modules/ArgumentParser.py index da128ff..021ca23 100644 --- a/dolphie/Modules/ArgumentParser.py +++ b/dolphie/Modules/ArgumentParser.py @@ -278,7 +278,7 @@ def _add_options(self): "This is used for creating tabs and connecting to them for hosts you specify in" " Dolphie's config file under a hostgroup section. As an example, you'll have a section" " called [cluster1] then below it will be listed each host on a new line in the format" - " key=host where key can be anything you want" + " key=host where key can be anything you want. Hosts support optional port in the format host:port" ), metavar="", ) diff --git a/dolphie/Modules/TabManager.py b/dolphie/Modules/TabManager.py index 6f08a1c..29be010 100644 --- a/dolphie/Modules/TabManager.py +++ b/dolphie/Modules/TabManager.py @@ -49,6 +49,7 @@ class Tab: topbar: TopBar = None main_container: VerticalScroll = None + metric_graph_tabs: TabbedContent = None loading_indicator: LoadingIndicator = None sparkline: Sparkline = None panel_dashboard: Container = None @@ -223,7 +224,7 @@ async def create_tab(self, tab_name: str, use_hostgroup: bool = False) -> Tab: classes="panel_container dashboard", ), Container( - TabbedContent(id=f"tabbed_content_{tab_id}", classes="metrics_tabbed_content"), + TabbedContent(id=f"metric_graph_tabs_{tab_id}", classes="metrics_tabbed_content"), id=f"panel_graphs_{tab_id}", classes="panel_container", ), @@ -309,7 +310,7 @@ async def create_tab(self, tab_name: str, use_hostgroup: bool = False) -> Tab: metric_tab_name = metric_instance.tab_name graph_names = metric_instance.graphs - await self.app.query_one(f"#tabbed_content_{tab_id}", TabbedContent).add_pane( + await self.app.query_one(f"#metric_graph_tabs_{tab_id}", TabbedContent).add_pane( TabPane( tab_formatted_name, Label(id=f"stats_{metric_tab_name}_{tab_id}", classes="stats_data"), @@ -365,6 +366,7 @@ async def create_tab(self, tab_name: str, use_hostgroup: bool = False) -> Tab: # Save references to the widgets in the tab tab.topbar = self.app.query_one(TopBar) tab.main_container = self.app.query_one(f"#main_container_{tab.id}", VerticalScroll) + tab.metric_graph_tabs = self.app.query_one(f"#metric_graph_tabs_{tab.id}", TabbedContent) tab.loading_indicator = self.app.query_one(f"#loading_indicator_{tab.id}", LoadingIndicator) tab.sparkline = self.app.query_one(f"#panel_dashboard_queries_qps_{tab.id}", Sparkline) tab.panel_dashboard = self.app.query_one(f"#panel_dashboard_{tab.id}", Container) diff --git a/dolphie/Panels/replication_panel.py b/dolphie/Panels/replication_panel.py index 5b3380a..8ac4371 100644 --- a/dolphie/Panels/replication_panel.py +++ b/dolphie/Panels/replication_panel.py @@ -241,16 +241,22 @@ def create_replication_table(tab: Tab, dashboard_table=False, replica: Replica = replica_sbm = dolphie.replica_lag speed = 0 + lag = None if replica_sbm is not None: if replica_previous_replica_sbm and replica_sbm < replica_previous_replica_sbm: speed = round((replica_previous_replica_sbm - replica_sbm) / dolphie.polling_latency) - if replica_sbm >= 20: - lag = "[red]%s" % "{:0>8}[/red]".format(str(timedelta(seconds=replica_sbm))) - elif replica_sbm >= 10: - lag = "[yellow]%s[/yellow]" % "{:0>8}".format(str(timedelta(seconds=replica_sbm))) - else: - lag = "[green]%s[/green]" % "{:0>8}".format(str(timedelta(seconds=replica_sbm))) + replica_lag = replica_sbm + if data.get("SQL_Delay"): + replica_lag -= data["SQL_Delay"] + + lag_color = "green" + if replica_lag >= 20: + lag_color = "red" + elif replica_lag >= 10: + lag_color = "yellow" + + lag = f"[{lag_color}]{str(timedelta(seconds=replica_lag)).zfill(8)}[/{lag_color}]" data["Master_Host"] = dolphie.get_hostname(data["Master_Host"]) mysql_gtid_enabled = False @@ -289,31 +295,25 @@ def create_replication_table(tab: Tab, dashboard_table=False, replica: Replica = if not dashboard_table: table.add_row("[label]User", "%s" % data["Master_User"]) - if data["Slave_IO_Running"].lower() == "yes": - io_thread_running = "[green]Yes[/green]" - else: - io_thread_running = "[red]NO[/red]" - - if data["Slave_SQL_Running"].lower() == "yes": - sql_thread_running = "[green]Yes[/green]" - else: - sql_thread_running = "[red]NO[/red]" - + io_thread_running = "[green]Yes[/green]" if data.get("Slave_IO_Running").lower() == "yes" else "[red]NO[/red]" + sql_thread_running = "[green]Yes[/green]" if data.get("Slave_SQL_Running").lower() == "yes" else "[red]NO[/red]" table.add_row( "[label]Thread", - "[label]IO %s [label]SQL %s" % (io_thread_running, sql_thread_running), + f"[label]IO {io_thread_running} [label]SQL {sql_thread_running}", ) - lag_source = "Lag" - if replica_sbm_source: - lag_source = f"Lag ({replica_sbm_source})" + replication_delay = "" + if data["SQL_Delay"]: + delay_formatted = str(timedelta(seconds=data["SQL_Delay"])).zfill(8) + replication_delay = f"([dark_yellow]delayed by {delay_formatted}[/dark_yellow])" - if replica_sbm is None or data["Slave_SQL_Running"].lower() == "no": + lag_source = f"Lag ({replica_sbm_source})" if replica_sbm_source else "Lag" + if lag is None or data["Slave_SQL_Running"].lower() == "no": table.add_row(f"[label]{lag_source}", "") else: table.add_row( "[label]%s" % lag_source, - "%s [label]Speed[/label] %s" % (lag, speed), + "%s [label]Speed[/label] %s %s" % (lag, speed, replication_delay), ) if dashboard_table: diff --git a/dolphie/Widgets/host_setup.py b/dolphie/Widgets/host_setup.py index e819f19..52fc042 100644 --- a/dolphie/Widgets/host_setup.py +++ b/dolphie/Widgets/host_setup.py @@ -70,7 +70,7 @@ class HostSetupModal(ModalScreen): def __init__( self, host: str, - port: str, + port: int, username: str, password: str, available_hosts: list, diff --git a/dolphie/app.py b/dolphie/app.py index 26a6eb9..4651dc4 100755 --- a/dolphie/app.py +++ b/dolphie/app.py @@ -46,7 +46,6 @@ from sqlparse import format as sqlformat from textual import events, on, work from textual.app import App, ComposeResult -from textual.css.query import NoMatches from textual.widgets import Switch, TabbedContent from textual.worker import Worker, WorkerState, get_current_worker @@ -74,6 +73,7 @@ def __init__(self, config: Config): "white": "#e9e9e9", "green": "#54efae", "yellow": "#f6ff8f", + "dark_yellow": "#cad45f", "red": "#fd8383", "purple": "#b565f3", "dark_gray": "#969aad", @@ -98,11 +98,10 @@ async def run_worker_main(self, tab_id: int): # Get our worker thread tab.worker = get_current_worker() tab.worker.name = tab_id + tab.worker_running = True dolphie = tab.dolphie try: - tab.worker_running = True - if not dolphie.main_db_connection.is_connected(): self.tab_manager.rename_tab(tab_id) # this will use dolphie.host instead of mysql_host tab.update_topbar(f"[[white]CONNECTING[/white]] {dolphie.host}:{dolphie.port}") @@ -208,16 +207,15 @@ async def run_worker_main(self, tab_id: int): replication_lag=dolphie.replica_lag, ) - tab.worker_running = False except ManualException as exception: # This will set up the worker state change function below to trigger the # host setup modal with the error - tab.worker_running = False - tab.worker_cancel_error = exception await tab.disconnect() + tab.worker_running = False + async def on_worker_state_changed(self, event: Worker.StateChanged): tab = self.tab_manager.get_tab(event.worker.name) if not tab: @@ -273,7 +271,7 @@ async def on_worker_state_changed(self, event: Worker.StateChanged): # Skip this if the conditions are right if len(self.screen_stack) > 1 or dolphie.pause_refresh: tab.replicas_worker_timer = self.set_timer( - tab.dolphie.refresh_interval, partial(self.run_worker_replicas, tab.id) + dolphie.refresh_interval, partial(self.run_worker_replicas, tab.id) ) return @@ -281,7 +279,9 @@ async def on_worker_state_changed(self, event: Worker.StateChanged): if dolphie.panels.replication.visible and dolphie.replica_manager.available_replicas: replication_panel.create_replica_panel(tab) - tab.replicas_worker_timer = self.set_timer(2, partial(self.run_worker_replicas, tab.id)) + tab.replicas_worker_timer = self.set_timer( + dolphie.refresh_interval, partial(self.run_worker_replicas, tab.id) + ) @work(thread=True, group="replicas") def run_worker_replicas(self, tab_id: int): @@ -315,64 +315,57 @@ def run_worker_replicas(self, tab_id: int): def refresh_screen(self, tab: Tab): dolphie = tab.dolphie - try: - loading_indicator = tab.loading_indicator - if loading_indicator.display: - loading_indicator.display = False - tab.main_container.display = True - - self.layout_graphs(tab) + loading_indicator = tab.loading_indicator + if loading_indicator.display: + loading_indicator.display = False + tab.main_container.display = True - if self.tab.id == tab.id: - tab.update_topbar() + self.layout_graphs(tab) - if dolphie.panels.dashboard.visible: - self.refresh_panel(tab, dolphie.panels.dashboard.name) + if self.tab.id == tab.id: + tab.update_topbar() - # Update the sparkline for queries per second - sparkline = tab.sparkline - sparkline_data = dolphie.metric_manager.metrics.dml.Queries.values - if not sparkline.display: - sparkline_data = [0] - sparkline.display = True + if dolphie.panels.dashboard.visible: + self.refresh_panel(tab, dolphie.panels.dashboard.name) - sparkline.data = sparkline_data - sparkline.refresh() + # Update the sparkline for queries per second + sparkline = tab.sparkline + sparkline_data = dolphie.metric_manager.metrics.dml.Queries.values + if not sparkline.display: + sparkline_data = [0] + sparkline.display = True - if dolphie.panels.processlist.visible: - self.refresh_panel(tab, dolphie.panels.processlist.name) + sparkline.data = sparkline_data + sparkline.refresh() - if dolphie.panels.replication.visible: - self.refresh_panel(tab, dolphie.panels.replication.name) + if dolphie.panels.processlist.visible: + self.refresh_panel(tab, dolphie.panels.processlist.name) - if dolphie.panels.locks.visible: - self.refresh_panel(tab, dolphie.panels.locks.name) + if dolphie.panels.replication.visible: + self.refresh_panel(tab, dolphie.panels.replication.name) - if dolphie.panels.ddl.visible: - self.refresh_panel(tab, dolphie.panels.ddl.name) + if dolphie.panels.locks.visible: + self.refresh_panel(tab, dolphie.panels.locks.name) - if dolphie.panels.graphs.visible: - graph_panel = self.query_one(f"#tabbed_content_{tab.id}", TabbedContent) + if dolphie.panels.ddl.visible: + self.refresh_panel(tab, dolphie.panels.ddl.name) - # Hide/show replication tab based on replication status - if dolphie.replication_status: - graph_panel.show_tab(f"graph_tab_replication_lag_{tab.id}") - else: - graph_panel.hide_tab(f"graph_tab_replication_lag_{tab.id}") + if dolphie.panels.graphs.visible: + # Hide/show replication tab based on replication status + if dolphie.replication_status: + tab.metric_graph_tabs.show_tab(f"graph_tab_replication_lag_{tab.id}") + else: + tab.metric_graph_tabs.hide_tab(f"graph_tab_replication_lag_{tab.id}") - # Refresh the graph(s) for the selected tab - self.update_graphs(graph_panel.get_pane(graph_panel.active).name) + # Refresh the graph(s) for the selected tab + self.update_graphs(tab.metric_graph_tabs.get_pane(tab.metric_graph_tabs.active).name) - # We take a snapshot of the processlist to be used for commands - # since the data can change after a key is pressed - dolphie.processlist_threads_snapshot = dolphie.processlist_threads.copy() + # We take a snapshot of the processlist to be used for commands + # since the data can change after a key is pressed + dolphie.processlist_threads_snapshot = dolphie.processlist_threads.copy() - # This denotes that we've gone through the first loop of the worker thread - dolphie.completed_first_loop = True - except NoMatches: - # This is thrown if a user toggles panels on and off and the display_* states aren't 1:1 - # with worker thread/state change due to asynchronous nature of the worker thread - pass + # This denotes that we've gone through the first loop of the worker thread + dolphie.completed_first_loop = True async def on_key(self, event: events.Key): if len(self.screen_stack) > 1: