Code indexing in gitaly is broken and leads to code not being visible to the user. We work on the issue with highest priority.

Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • bec/bec_widgets
1 result
Show changes
Commits on Source (2)
......@@ -8,14 +8,15 @@ import re
from collections import deque
from functools import partial, reduce
from re import Pattern
from typing import TYPE_CHECKING, Literal
from typing import Literal
from bec_lib.client import BECClient
from bec_lib.connector import ConnectorBase
from bec_lib.endpoints import MessageEndpoints
from bec_lib.logger import LogLevel, bec_logger
from bec_lib.messages import LogMessage, StatusMessage
from qtpy.QtCore import QDateTime, Qt, Signal # type: ignore
from PySide6.QtCore import QObject
from qtpy.QtCore import QDateTime, Qt, Signal, SignalInstance # type: ignore
from qtpy.QtGui import QFont
from qtpy.QtWidgets import (
QApplication,
......@@ -51,9 +52,6 @@ from bec_widgets.widgets.utility.logpanel._util import (
simple_color_format,
)
if TYPE_CHECKING:
from PySide6.QtCore import SignalInstance
logger = bec_logger.logger
MODULE_PATH = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
......@@ -68,20 +66,22 @@ DEFAULT_LOG_COLORS = {
}
class BecLogsQueue:
class BecLogsQueue(QObject):
"""Manages getting logs from BEC Redis and formatting them for display"""
new_message = Signal()
def __init__(
self,
parent: QObject | None,
conn: ConnectorBase,
new_message_signal: SignalInstance,
maxlen: int = 1000,
line_formatter: LineFormatter = noop_format,
) -> None:
super().__init__(parent=parent)
self._timestamp_start: QDateTime | None = None
self._timestamp_end: QDateTime | None = None
self._conn = conn
self._new_message_signal: SignalInstance | None = new_message_signal
self._max_length = maxlen
self._data: deque[LogMessage] = deque([], self._max_length)
self._display_queue: deque[str] = deque([], self._max_length)
......@@ -91,9 +91,9 @@ class BecLogsQueue:
self._set_formatter_and_update_filter(line_formatter)
self._conn.register([MessageEndpoints.log()], None, self._process_incoming_log_msg)
def disconnect(self):
def unsub_from_redis(self):
"""Stop listening to the Redis log stream"""
self._conn.unregister([MessageEndpoints.log()], None, self._process_incoming_log_msg)
self._new_message_signal.disconnect()
def _process_incoming_log_msg(self, msg: dict):
try:
......@@ -101,10 +101,9 @@ class BecLogsQueue:
self._data.append(_msg)
if self.filter is None or self.filter(_msg):
self._display_queue.append(self._line_formatter(_msg))
if self._new_message_signal:
self._new_message_signal.emit()
except Exception:
pass
self.new_message.emit()
except Exception as e:
logger.warning(f"Error in LogPanel incoming message callback: {e}")
def _set_formatter_and_update_filter(self, line_formatter: LineFormatter = noop_format):
self._line_formatter: LineFormatter = line_formatter
......@@ -144,6 +143,7 @@ class BecLogsQueue:
@property
def filter(self) -> LineFilter:
"""A function which filters a log message based on all applied criteria"""
thresh = LogLevel[self._log_level].value if self._log_level is not None else 0
return self._combine_filters(
partial(level_filter, thresh=thresh),
......@@ -153,6 +153,7 @@ class BecLogsQueue:
)
def update_level_filter(self, level: str):
"""Change the log-level of the level filter"""
if level not in [l.name for l in LogLevel]:
logger.error(f"Logging level {level} unrecognized for filter!")
return
......@@ -160,34 +161,42 @@ class BecLogsQueue:
self._set_formatter_and_update_filter(self._line_formatter)
def update_search_filter(self, search_query: Pattern | str | None = None):
"""Change the string or regex to filter against"""
self._search_query = search_query
self._set_formatter_and_update_filter(self._line_formatter)
def update_time_filter(self, start: QDateTime | None, end: QDateTime | None):
"""Change the start and/or end times to filter against"""
self._timestamp_start = start
self._timestamp_end = end
self._set_formatter_and_update_filter(self._line_formatter)
def update_service_filter(self, services: set[str]):
"""Change the selected services to display"""
self._selected_services = services
self._set_formatter_and_update_filter(self._line_formatter)
def update_line_formatter(self, line_formatter: LineFormatter):
"""Update the formatter"""
self._set_formatter_and_update_filter(line_formatter)
def display_all(self) -> str:
"""Return formatted output for all log messages"""
return "\n".join(self._queue_formatter(self._data.copy()))
def format_new(self):
"""Return formatted output for the display queue"""
res = "\n".join(self._display_queue)
self._display_queue = deque([], self._max_length)
return res
def clear_logs(self):
"""Clear the cache and display queue"""
self._data = deque([])
self._display_queue = deque([])
def fetch_history(self):
"""Fetch all available messages from Redis"""
self._data = deque(
item["data"]
for item in self._conn.xread(
......@@ -196,14 +205,16 @@ class BecLogsQueue:
)
def unique_service_names_from_history(self) -> set[str]:
"""Go through the log history to determine active service names"""
return set(msg.log_msg["service_name"] for msg in self._data)
class LogPanelToolbar(QWidget):
services_selected: pyqtBoundSignal = Signal(set)
services_selected: SignalInstance = Signal(set)
def __init__(self, parent: QWidget | None = None) -> None:
"""A toolbar for the logpanel, mainly used for managing the states of filters"""
super().__init__(parent)
# in unix time
......@@ -337,6 +348,7 @@ class LogPanelToolbar(QWidget):
def service_list_update(
self, services_info: dict[str, StatusMessage], services_from_history: set[str], *_, **__
):
"""Change the list of services which can be selected"""
self._unique_service_names = set([s.split("/")[0] for s in services_info.keys()])
self._unique_service_names |= services_from_history
if self._services_selected is None:
......@@ -396,10 +408,11 @@ class LogPanel(TextBox):
self._update_colors()
self._service_status = service_status or BECServiceStatusMixin(self, client=self.client) # type: ignore
self._log_manager = BecLogsQueue(
parent,
self.client.connector, # type: ignore
new_message_signal=self._new_messages,
line_formatter=partial(simple_color_format, colors=self._colors),
)
self._log_manager.new_message.connect(self._new_messages)
self.toolbar = LogPanelToolbar(parent=parent)
self.toolbar_area = QScrollArea()
......@@ -513,7 +526,9 @@ class LogPanel(TextBox):
def cleanup(self):
self._service_status.cleanup()
self._log_manager.disconnect()
self._log_manager.unsub_from_redis()
self._log_manager.new_message.disconnect(self._new_messages)
self._new_messages.disconnect(self._on_append)
super().cleanup()
......
# pylint: disable=missing-function-docstring, missing-module-docstring, unused-import
# pylint: disable=no-member
# pylint: disable=missing-function-docstring
# pylint: disable=redefined-outer-name
# pylint: disable=protected-access
from collections import deque
from unittest.mock import MagicMock
......@@ -62,6 +65,7 @@ def log_panel(qtbot, mocked_client: MagicMock):
qtbot.addWidget(widget)
qtbot.waitExposed(widget)
yield widget
widget.cleanup()
def test_log_panel_init(log_panel: LogPanel):
......@@ -89,6 +93,7 @@ def test_logpanel_output(qtbot, log_panel: LogPanel):
assert log_panel.plain_text == TEST_COMBINED_PLAINTEXT
def display_queue_empty():
print(log_panel._log_manager._display_queue)
return len(log_panel._log_manager._display_queue) == 0
next_text = "datetime | error | test log message"
......@@ -102,7 +107,7 @@ def test_logpanel_output(qtbot, log_panel: LogPanel):
}
)
qtbot.waitUntil(display_queue_empty)
qtbot.waitUntil(display_queue_empty, timeout=5000)
assert log_panel.plain_text == TEST_COMBINED_PLAINTEXT + next_text + "\n"
......