diff --git a/distributed/dashboard/components/scheduler.py b/distributed/dashboard/components/scheduler.py index b00bcfb94b0..9311f187e6a 100644 --- a/distributed/dashboard/components/scheduler.py +++ b/distributed/dashboard/components/scheduler.py @@ -70,7 +70,7 @@ from distributed.diagnostics.task_stream import color_of as ts_color_of from distributed.diagnostics.task_stream import colors as ts_color_lookup from distributed.metrics import time -from distributed.utils import Logs, format_time, log_errors, parse_timedelta +from distributed.utils import Log, format_time, log_errors, parse_timedelta if dask.config.get("distributed.dashboard.export-tool"): from distributed.dashboard.export_tool import ExportTool @@ -2165,7 +2165,9 @@ def update(self): class SchedulerLogs: def __init__(self, scheduler): - logs = Logs(scheduler.get_logs())._repr_html_() + logs = Log( + "\n".join(line for level, line in scheduler.get_logs()) + )._repr_html_() self.root = Div(text=logs) diff --git a/distributed/deploy/cluster.py b/distributed/deploy/cluster.py index aaa05cf5617..b00886ac963 100644 --- a/distributed/deploy/cluster.py +++ b/distributed/deploy/cluster.py @@ -14,7 +14,8 @@ from ..core import Status from ..objects import SchedulerInfo from ..utils import ( - MultiLogs, + Log, + Logs, format_dashboard_link, log_errors, parse_timedelta, @@ -208,19 +209,21 @@ def _log(self, log): print(log) async def _get_logs(self, cluster=True, scheduler=True, workers=True): - logs = MultiLogs() + logs = Logs() if cluster: - logs["Cluster"] = self._cluster_manager_logs + logs["Cluster"] = Log( + "\n".join(line[1] for line in self._cluster_manager_logs) + ) if scheduler: L = await self.scheduler_comm.get_logs() - logs["Scheduler"] = L + logs["Scheduler"] = Log("\n".join(line for level, line in L)) if workers: d = await self.scheduler_comm.worker_logs(workers=workers) for k, v in d.items(): - logs[k] = v + logs[k] = Log("\n".join(line for level, line in v)) return logs diff --git a/distributed/deploy/tests/test_spec_cluster.py b/distributed/deploy/tests/test_spec_cluster.py index ca96104de3b..dff5d06831b 100644 --- a/distributed/deploy/tests/test_spec_cluster.py +++ b/distributed/deploy/tests/test_spec_cluster.py @@ -287,6 +287,8 @@ async def test_logs(cleanup): await cluster logs = await cluster.get_logs() + assert isinstance(logs, dict) + assert all(isinstance(log, str) for log in logs) assert is_valid_xml("
" + logs._repr_html_() + "
") assert "Scheduler" in logs for worker in cluster.scheduler.workers: diff --git a/distributed/tests/test_utils.py b/distributed/tests/test_utils.py index 16bc7074f7d..fa384cd8451 100644 --- a/distributed/tests/test_utils.py +++ b/distributed/tests/test_utils.py @@ -19,8 +19,9 @@ from distributed.utils import ( LRU, All, + Log, + Logs, LoopRunner, - MultiLogs, TimeoutError, _maybe_complex, ensure_bytes, @@ -548,7 +549,10 @@ def test_format_bytes_compat(): def test_logs(): - d = MultiLogs({"123": [("INFO", "Hello")], "456": [("INFO", "World!")]}) + log = Log("Hello") + assert isinstance(log, str) + d = Logs({"123": log, "456": Log("World!")}) + assert isinstance(d, dict) text = d._repr_html_() assert is_valid_xml("
" + text + "
") assert "Hello" in text diff --git a/distributed/utils.py b/distributed/utils.py index fae79ff4552..33793b2f504 100644 --- a/distributed/utils.py +++ b/distributed/utils.py @@ -1241,8 +1241,8 @@ def parse_ports(port): is_coroutine_function = iscoroutinefunction -class Log(tuple): - """A container for a single log entry""" +class Log(str): + """A container for newline-delimited string of log entries""" level_styles = { "WARNING": "font-weight: bold; color: orange;", @@ -1251,34 +1251,34 @@ class Log(tuple): } def _repr_html_(self): - level, message = self - - style = "font-family: monospace; margin: 0;" - style += self.level_styles.get(level, "") - - return '

{message}

'.format( - style=html.escape(style), - message=html.escape(message), - ) - - -class Logs(list): - """A container for a list of log entries""" + logs_html = [] + for message in self.split("\n"): + style = "font-family: monospace; margin: 0;" + for level in self.level_styles: + if level in message: + style += self.level_styles[level] + break + + logs_html.append( + '

{message}

'.format( + style=html.escape(style), + message=html.escape(message), + ) + ) - def _repr_html_(self): - return "\n".join(Log(entry)._repr_html_() for entry in self) + return "\n".join(logs_html) -class MultiLogs(dict): - """A container for a dict mapping strings to lists of log entries""" +class Logs(dict): + """A container for a dict mapping names to strings of log entries""" def _repr_html_(self): summaries = [ "
\n" "{title}\n" - "{logs}\n" - "
".format(title=title, logs=Logs(entries)._repr_html_()) - for title, entries in sorted(self.items()) + "{log}\n" + "".format(title=title, log=log._repr_html_()) + for title, log in sorted(self.items()) ] return "\n".join(summaries)