-
-
Notifications
You must be signed in to change notification settings - Fork 412
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add a custom log handler #6900
base: main
Are you sure you want to change the base?
Add a custom log handler #6900
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
from qtpy.QtCore import Qt | ||
from qtpy.QtWidgets import ( | ||
QDialog, | ||
QHBoxLayout, | ||
QLabel, | ||
QTextEdit, | ||
QVBoxLayout, | ||
) | ||
|
||
from napari import _LOG_STREAM | ||
from napari._qt.dialogs.qt_about import QtCopyToClipboardButton | ||
from napari.utils.translations import trans | ||
|
||
|
||
class LogDialog(QDialog): | ||
def __init__( | ||
self, | ||
parent=None, | ||
) -> None: | ||
super().__init__(parent._qt_window) | ||
|
||
self.layout = QVBoxLayout() | ||
|
||
# Description | ||
title_label = QLabel(trans._('napari log')) | ||
title_label.setTextInteractionFlags( | ||
Qt.TextInteractionFlag.TextSelectableByMouse | ||
) | ||
self.layout.addWidget(title_label) | ||
|
||
# Add information | ||
self.infoTextBox = QTextEdit() | ||
self.infoTextBox.setTextInteractionFlags( | ||
Qt.TextInteractionFlag.TextSelectableByMouse | ||
) | ||
self.infoTextBox.setLineWrapMode(QTextEdit.NoWrap) | ||
# Add text copy button | ||
self.infoCopyButton = QtCopyToClipboardButton(self.infoTextBox) | ||
self.info_layout = QHBoxLayout() | ||
self.info_layout.addWidget(self.infoTextBox, 1) | ||
self.info_layout.addWidget( | ||
self.infoCopyButton, 0, Qt.AlignmentFlag.AlignTop | ||
) | ||
self.info_layout.setAlignment(Qt.AlignmentFlag.AlignTop) | ||
self.layout.addLayout(self.info_layout) | ||
|
||
self.infoTextBox.setText(str(_LOG_STREAM)) | ||
self.infoTextBox.setMinimumSize( | ||
int(self.infoTextBox.document().size().width() + 19), | ||
int(min(self.infoTextBox.document().size().height() + 10, 500)), | ||
) | ||
|
||
self.setLayout(self.layout) | ||
|
||
self.setObjectName('LogDialog') | ||
self.setWindowTitle(trans._('napari log')) | ||
self.setWindowModality(Qt.WindowModality.ApplicationModal) | ||
self.exec_() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Exec should not be called in constructor, but in calling place. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
import logging | ||
|
||
# keep track of all the logging info in a stream | ||
_LOG_SEPARATOR = '<NAPARI_LOG_SEPARATOR>' | ||
|
||
|
||
class _LogHandler: | ||
def __init__(self): | ||
self.logs = [] | ||
|
||
def write(self, log_msg): | ||
level_name, time, thread, msg = log_msg.split(_LOG_SEPARATOR) | ||
levels = logging.getLevelNamesMapping() | ||
level_value = levels[level_name] | ||
self.logs.append((level_value, level_name, time, thread, msg)) | ||
|
||
def flush(self): | ||
pass | ||
|
||
def __str__(self): | ||
return ''.join( | ||
[ | ||
f'[{time}] ({thread}) {level_name}: {msg}' | ||
for _, level_name, time, thread, msg in self.logs | ||
] | ||
) | ||
|
||
def logs_at_level(self, level=logging.DEBUG): | ||
if isinstance(level, str): | ||
level = logging.getLevelName(level) | ||
return [ | ||
(level_value, *others) | ||
for level_value, *others in self.logs | ||
if level_value >= level | ||
] | ||
|
||
|
||
def _get_custom_log_stream(): | ||
log_stream = _LogHandler() | ||
handler = logging.StreamHandler(log_stream) | ||
handler.setFormatter( | ||
logging.Formatter( | ||
f'%(levelname)s{_LOG_SEPARATOR}%(asctime)s{_LOG_SEPARATOR}%(threadName)s{_LOG_SEPARATOR}%(message)s' | ||
) | ||
) | ||
logger = logging.getLogger('napari') | ||
logger.setLevel(logging.DEBUG) | ||
logger.addHandler(handler) | ||
Comment on lines
+46
to
+48
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It will be nice to connect to all loggers and be able to filter messages based on logger (so someone could filter logs only from a given plugin logger. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. True... I'm not sure how to do that though; I guess we somehow get the root logger? And btw, is there a way to get these logging parameters (level, thread, etc) "raw" instead of formatting to string and then de-formatting? |
||
return log_stream |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that custom log handler should be initialized in
napari.qt.qt_event_loop
, same as notification manager.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But the logger should work regardless of qt right? Also headless it would be useful.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As I understand, it is log handler, not logger. The current code means, that each time one import napari it starts storing all logs in memory, even without a clean way to show them.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes... but isn't that what we want? Would be good to be able to get these logs programmatically as well. We can also discard old logs if we go above a certain size.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My personal feeling is that we should do this, like with notifications (warnigs), but I may be wrong.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
do what? I'm not sure I follow ^^'