diff --git a/ChangeLog b/ChangeLog index 9d3687e..00fe38f 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,9 +1,17 @@ CHANGES ======= +v2.5.5 +------ + +* Update requirements to use aprslib 0.7.0 +* fixed the failure during loading for objectstore +* updated docker build + v2.5.4 ------ +* Updated Changelog * Fixed dev command missing initialization v2.5.3 diff --git a/aprsd/cmds/dev.py b/aprsd/cmds/dev.py index 2568b8d..a6d67dd 100644 --- a/aprsd/cmds/dev.py +++ b/aprsd/cmds/dev.py @@ -8,7 +8,7 @@ import logging import click # local imports here -from aprsd import cli_helper, client, messaging, packets, plugin, stats +from aprsd import cli_helper, client, messaging, packets, plugin, stats, trace from ..aprsd import cli @@ -79,6 +79,10 @@ def test_plugin( if type(message) is tuple: message = " ".join(message) + + if config["aprsd"].get("trace", False): + trace.setup_tracing(["method", "api"]) + client.Client(config) stats.APRSDStats(config) messaging.MsgTrack(config=config) diff --git a/aprsd/config.py b/aprsd/config.py index 9650604..054d5b7 100644 --- a/aprsd/config.py +++ b/aprsd/config.py @@ -79,6 +79,7 @@ DEFAULT_CONFIG_DICT = { "logformat": DEFAULT_LOG_FORMAT, "dateformat": DEFAULT_DATE_FORMAT, "save_location": DEFAULT_CONFIG_DIR, + "rich_logging": False, "trace": False, "enabled_plugins": CORE_MESSAGE_PLUGINS, "units": "imperial", diff --git a/aprsd/log.py b/aprsd/log.py index a6ad1de..e5c3e6a 100644 --- a/aprsd/log.py +++ b/aprsd/log.py @@ -5,6 +5,7 @@ import queue import sys from aprsd import config as aprsd_config +from aprsd.logging import logging as aprsd_logging LOG = logging.getLogger("APRSD") @@ -17,10 +18,24 @@ logging_queue = queue.Queue() def setup_logging(config, loglevel, quiet): log_level = aprsd_config.LOG_LEVELS[loglevel] LOG.setLevel(log_level) - log_format = config["aprsd"].get("logformat", aprsd_config.DEFAULT_LOG_FORMAT) + if config["aprsd"].get("rich_logging", False): + log_format = "%(message)s" + else: + log_format = config["aprsd"].get("logformat", aprsd_config.DEFAULT_LOG_FORMAT) date_format = config["aprsd"].get("dateformat", aprsd_config.DEFAULT_DATE_FORMAT) log_formatter = logging.Formatter(fmt=log_format, datefmt=date_format) log_file = config["aprsd"].get("logfile", None) + + rich_logging = False + if config["aprsd"].get("rich_logging", False): + rh = aprsd_logging.APRSDRichHandler( + show_thread=True, thread_width=15, + rich_tracebacks=True, omit_repeated_times=False, + ) + rh.setFormatter(log_formatter) + LOG.addHandler(rh) + rich_logging = True + if log_file: fh = RotatingFileHandler(log_file, maxBytes=(10248576 * 5), backupCount=4) else: @@ -45,7 +60,7 @@ def setup_logging(config, loglevel, quiet): qh.setFormatter(q_log_formatter) LOG.addHandler(qh) - if not quiet: + if not quiet and not rich_logging: sh = logging.StreamHandler(sys.stdout) sh.setFormatter(log_formatter) LOG.addHandler(sh) diff --git a/aprsd/logging/__init__.py b/aprsd/logging/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/aprsd/logging/logging.py b/aprsd/logging/logging.py new file mode 100644 index 0000000..66137fe --- /dev/null +++ b/aprsd/logging/logging.py @@ -0,0 +1,160 @@ +from datetime import datetime +from logging import LogRecord +from pathlib import Path +from typing import TYPE_CHECKING, Callable, Iterable, List, Optional, Union + +from rich._log_render import LogRender +from rich.logging import RichHandler +from rich.text import Text, TextType +from rich.traceback import Traceback + + +if TYPE_CHECKING: + from rich.console import Console, ConsoleRenderable, RenderableType + from rich.table import Table + +from aprsd import utils + + +FormatTimeCallable = Callable[[datetime], Text] + + +class APRSDRichLogRender(LogRender): + + def __init__( + self, *args, + show_thread: bool = False, + thread_width: Optional[int] = 10, + **kwargs, + ): + super().__init__(*args, **kwargs) + self.show_thread = show_thread + self.thread_width = thread_width + + def __call__( + self, + console: "Console", + renderables: Iterable["ConsoleRenderable"], + log_time: Optional[datetime] = None, + time_format: Optional[Union[str, FormatTimeCallable]] = None, + level: TextType = "", + path: Optional[str] = None, + line_no: Optional[int] = None, + link_path: Optional[str] = None, + thread_name: Optional[str] = None, + ) -> "Table": + from rich.containers import Renderables + from rich.table import Table + + output = Table.grid(padding=(0, 1)) + output.expand = True + if self.show_time: + output.add_column(style="log.time") + if self.show_thread: + rgb = str(utils.rgb_from_name(thread_name)).replace(" ", "") + output.add_column(style=f"rgb{rgb}", width=self.thread_width) + if self.show_level: + output.add_column(style="log.level", width=self.level_width) + output.add_column(ratio=1, style="log.message", overflow="fold") + if self.show_path and path: + output.add_column(style="log.path") + row: List["RenderableType"] = [] + if self.show_time: + log_time = log_time or console.get_datetime() + time_format = time_format or self.time_format + if callable(time_format): + log_time_display = time_format(log_time) + else: + log_time_display = Text(log_time.strftime(time_format)) + if log_time_display == self._last_time and self.omit_repeated_times: + row.append(Text(" " * len(log_time_display))) + else: + row.append(log_time_display) + self._last_time = log_time_display + if self.show_thread: + row.append(thread_name) + if self.show_level: + row.append(level) + + row.append(Renderables(renderables)) + if self.show_path and path: + path_text = Text() + path_text.append( + path, style=f"link file://{link_path}" if link_path else "", + ) + if line_no: + path_text.append(":") + path_text.append( + f"{line_no}", + style=f"link file://{link_path}#{line_no}" if link_path else "", + ) + row.append(path_text) + + output.add_row(*row) + return output + + +class APRSDRichHandler(RichHandler): + """APRSD's extension of rich's RichHandler to show threads. + + show_thread (bool, optional): Show the name of the thread in log entry. Defaults to False. + thread_width (int, optional): The number of characters to show for thread name. Defaults to 10. + """ + + def __init__( + self, *args, + show_thread: bool = True, + thread_width: Optional[int] = 10, + **kwargs, + ): + super().__init__(*args, **kwargs) + self.show_thread = show_thread + self.thread_width = thread_width + kwargs["show_thread"] = show_thread + kwargs["thread_width"] = thread_width + self._log_render = APRSDRichLogRender( + show_time=True, + show_level=True, + show_path=True, + omit_repeated_times=False, + level_width=None, + show_thread=show_thread, + thread_width=thread_width, + ) + + def render( + self, *, record: LogRecord, + traceback: Optional[Traceback], + message_renderable: "ConsoleRenderable", + ) -> "ConsoleRenderable": + """Render log for display. + + Args: + record (LogRecord): logging Record. + traceback (Optional[Traceback]): Traceback instance or None for no Traceback. + message_renderable (ConsoleRenderable): Renderable (typically Text) containing log message contents. + + Returns: + ConsoleRenderable: Renderable to display log. + """ + path = Path(record.pathname).name + level = self.get_level_text(record) + time_format = None if self.formatter is None else self.formatter.datefmt + log_time = datetime.fromtimestamp(record.created) + thread_name = record.threadName + + log_renderable = self._log_render( + self.console, + [message_renderable] if not traceback else [ + message_renderable, + traceback, + ], + log_time=log_time, + time_format=time_format, + level=level, + path=path, + line_no=record.lineno, + link_path=record.pathname if self.enable_link_path else None, + thread_name=thread_name, + ) + return log_renderable diff --git a/aprsd/utils.py b/aprsd/utils.py index d639582..124443c 100644 --- a/aprsd/utils.py +++ b/aprsd/utils.py @@ -60,6 +60,17 @@ def end_substr(original, substr): return idx +def rgb_from_name(name): + """Create an rgb tuple from a string.""" + hash = 0 + for char in name: + hash = ord(char) + ((hash << 5) - hash) + red = hash & 255 + green = (hash >> 8) & 255 + blue = (hash >> 16) & 255 + return red, green, blue + + def human_size(bytes, units=None): """Returns a human readable string representation of bytes""" if not units: diff --git a/requirements.in b/requirements.in index 94cc54b..273d502 100644 --- a/requirements.in +++ b/requirements.in @@ -1,5 +1,5 @@ aioax25>=0.0.10 -aprslib +aprslib>=0.7.0 click click-completion flask @@ -21,3 +21,4 @@ update_checker flask-socketio eventlet tabulate +rich diff --git a/requirements.txt b/requirements.txt index 9bf8a0a..e23f556 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,7 +6,7 @@ # aioax25==0.0.10 # via -r requirements.in -aprslib==0.6.47 +aprslib==0.7.0 # via -r requirements.in backoff==1.11.1 # via opencage @@ -25,6 +25,10 @@ click==8.0.1 # flask click-completion==0.5.2 # via -r requirements.in +colorama==0.4.4 + # via rich +commonmark==0.9.1 + # via rich contexter==0.1.4 # via signalslot cryptography==3.4.7 @@ -75,6 +79,8 @@ py3-validate-email==1.0.1 # via -r requirements.in pycparser==2.20 # via cffi +pygments==2.10.0 + # via rich pyopenssl==20.0.1 # via opencage pyserial==3.5 @@ -92,6 +98,8 @@ requests==2.26.0 # -r requirements.in # opencage # update-checker +rich==10.15.2 + # via -r requirements.in shellingham==1.4.0 # via click-completion signalslot==0.1.2