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
[WIP] Improve log view so one can filter out messages based on verbosity or domains #830
Conversation
picard/acoustid/__init__.py
Outdated
else: | ||
mparms = { | ||
'error': document['error']['message'], | ||
'filename': file.filename | ||
} | ||
log.error( | ||
"AcoustID: Lookup error for '%(filename)s': %(error)r" % | ||
mparms) | ||
mparms, domains='acoustid') |
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.
Instead of manually specifying the domains, we can use the built in logging module from python stdlib and take advantage of its attributes https://docs.python.org/3/library/logging.html#logrecord-attributes
This will save us all the overhead and the repetitions
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'd say we can simply drop the domains part and just keep the UI additions and maybe we can continue this in another PR
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.
Instead of manually specifying the domains, we can use the built in logging module from python stdlib and take advantage of its attributes https://docs.python.org/3/library/logging.html#logrecord-attributes
This will save us all the overhead and the repetitions
I don't see how LogRecord provides this feature, as is.
Using module is not what i want, since, in the same module, i want to be able to specify the domain of the log line. Filtering on module can be good, but that's not my goal.
In fact, to implement this you need to subclass logging.Logger
, something like that:
def _onestr2set(domains):
if not domains:
return set()
if isinstance(domains, str):
# this is a shortcut: allow one to pass a domains as sole
# argument, but use it as first argument of set
return set((domains,))
else:
return set(domains)
class CustomLogger(logging.Logger):
def _log(self, level, msg, args, exc_info=None, extra=None):
if extra is None:
extra = { 'domains': set() }
super(CustomLogger, self)._log(level, msg, args, exc_info, extra)
logging.setLoggerClass(CustomLogger)
def debug(message, *args, domains=None):
domains = _onestr2set(domains)
root.debug(message, *args, extra={'domains': domains})
def info(message, *args, domains=None):
domains = _onestr2set(domains)
root.info(message, *args, extra={'domains': domains})
def warning(message, *args, domains=None):
domains = _onestr2set(domains)
root.warning(message, *args, extra={'domains': domains})
def error(message, *args, domains=None):
domains = _onestr2set(domains)
root.error(message, *args, extra={'domains': domains})
root = logging.getLogger('picard')
root.setLevel(logging.DEBUG)
handler = logging.StreamHandler()
bf = logging.Formatter('{asctime} {levelname:8s} {message} {domains}', style='{')
handler.setFormatter(bf)
root.addHandler(handler)
picard/ui/logview.py
Outdated
act.triggered.connect(partial(self._verbosity_changed, level)) | ||
self.verbosity_menu.addAction(act) | ||
|
||
self.verbosity_menu_button = QtWidgets.QPushButton(_("Verbosity")) |
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.
Any reason for having the gui layouts part here instead of a ui file?
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.
Nope, but since menus are generated, you'll not have much more than few widgets in ui file anyway.
picard/ui/logview.py
Outdated
@@ -25,6 +27,8 @@ | |||
|
|||
class LogViewCommon(PicardDialog): | |||
|
|||
default_verbosity = {log.LOG_INFO, log.LOG_WARNING, log.LOG_ERROR, log.LOG_DEBUG} |
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 am not sure how much sense this makes. Generally debug includes all the levels above it. So if debug is checked all errors, info and warning logs should come too.
This is default behaviour in almost all loggers.
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.
That's not the behaviour of the logger here, but the behaviour of the log viewer: each log message is set to a certain level (and that's the default behaviour of almost all loggers), whether you want to log or display a certain level is up to you.
For example, i may want to write only debug messages to one debug file, and the rest in a common file, or each level in a separate file, that's not defined by the logger, but by the log handler.
The idea here is to reduce the view to what is interesting, if i know the thing i'm looking for is a debug message, i want to hide the other levels. And this is possible here.
picard/ui/logview.py
Outdated
@@ -37,7 +41,11 @@ def __init__(self, title, logger, w=740, h=340, parent=None): | |||
self.browser.setDocument(self.doc) | |||
self.vbox = QtWidgets.QVBoxLayout(self) | |||
self.vbox.addWidget(self.browser) | |||
self._display() | |||
self._setup_formats() | |||
self.verbosity = self.default_verbosity |
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.
Dont really get the use of the debug checkbox and this drop down. We can keep only this and remove the checkbox?
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.
Dont really get the use of the debug checkbox and this drop down. We can keep only this and remove the checkbox?
That's 2 different things, once again, you mix up the logger and the log viewer:
- debug mode controls if we log debug messages (if disabled, they aren't written anywhere at all)
- verbosity debug unchecked will hide the debug messages from the view, but they are still outputted to console for example
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.
Hmm, can we separate the logger config from the log viewer config?
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.
Hmm, can we separate the logger config from the log viewer config?
What do you mean ?
Debug mode is just a trick to allow user to enable the debug mode from within the app (before it was only possible using the -d command line option).
Logging level setting could be moved elsewhere in Options, but then it would be a bit cumbersome to toggle debug mode on/off.
Think about the end user:
- open Log View
- check Debug mode checkbox
- do some action
- press Save as
- uncheck Debug mode checkbox
Instead of:
- Open Options
- go to panel "Advanced"
- set the log level to Debug (if it wasn't already...)
- Press OK
- open the Log View
- do some action
- press Save as
- Open Options
- go to panel "Advanced"
- set the log level back to what it was (if you remember)
- Press OK
picard/ui/logview.py
Outdated
self._display() | ||
self._setup_formats() | ||
self.verbosity = self.default_verbosity | ||
self.hidden_domains = set() |
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.
Again, don't get why both domains and show only domains are needed? Does selecting domains discard messages altogether and show only
create a temp filter? If so, we should keep the distinction between filters and records separate.
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.
Because domains is a tuple (even if a trick allows to pass just a string, when the tuple should have only one element), example:
- a message A with domains
('ws', 'ratecontrol')
- a message B with domains
('ws')
- a message C with domains
('ratecontrol')
- a message D with domains
('ws', 'ratecontrol', 'subratecontrol')
Show only (ws)
shows A,B,D (only)
Show only (ws, ratecontrol)
shows A, B, C,D
Show only (ws, ratecontrol)
+ hide ('subratecrontrol')
shows A, B, C but not D
Show only totally unset + hide ('ws') shows C only
That said, i didn't write tests to ensure the actual behaviour is the one i expect ;)
Do you get why both are needed ? Include list + exclude list, both working together to provide more than you could do with just one.
picard/ui/logview.py
Outdated
self.hbox.addWidget(self.domains_menu_button) | ||
self.hbox.addWidget(self.show_only_domains_menu_button) | ||
|
||
self.highlight_text = QtWidgets.QLineEdit() |
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.
Pressing enter after inputting text shifts the UI to verbosity selection instead of applying the HL.
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, i know, i'll clean up those UI quirks later when we are sure which features we want and how (experimental PR)
picard/ui/logview.py
Outdated
self.hbox.addWidget(self.highlight_button) | ||
self.highlight_button.clicked.connect(self._highlight_do) | ||
|
||
self.clear_log_button = QtWidgets.QPushButton(_("Clear Log")) |
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.
Can we add some tooltips for all these options? It is unclear to a user whether these are destructive or just temp.
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, of course, this option actually clears the circular buffer. UI tuning will come at the end.
if enabled: | ||
main_logger.setLevel(logging.DEBUG) | ||
else: | ||
main_logger.setLevel(logging.INFO) |
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.
Shouldn't the default level be WARNING?
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.
Historically, log was showing anything but debug level
Thought of a good idea to add domains to a function - (This can go in from picard import log
from functools import partial, wraps
def domain(*domains):
def wrapper(f):
@wraps(f)
def caller(*args, **kwargs):
log.debug = partial(log.debug, domains=domains)
log.info = partial(log.info, domains=domains)
log.warning = partial(log.warning, domains=domains)
log.error = partial(log.error, domains=domains)
return f(*args, **kwargs)
return caller
return wrapper It can be used as follows @domain('random', 'more')
def random_func(*args):
log.debug('logs as the above domains') Makes the above more readable too as we know what domain a function is associated with. And ofc if we supply the domains explicitly, it overrides the decorator. |
Convert Logger to QObject, and emit a signal when the domain list is modified Disable updates when the menu is shown No need to open/close log view anymore
Name, prefix and color for each log level are set in one place
It implies few changes, most notably a re-implementation of a circular buffer (deque-based) as a logging handler. The way history log is displayed in the History View was adapted too.
- get rid of LogMessage and old Logger class - adapt log view code according to those changes - code tidy up and simplification
- funcname and pathname are now correct - drop ratelimit wrapper - accept domains kv argument on all log.*(): log.debug('string', domains={'a','b','c'}) - provide a useful "name" since we are using only one logger, and since module is useless for submodules (=__init__ for all)
logger.(debug|error|...)() are just doing that
- much simpler and reliable - fix few button enabled/disabled quirks - fix highlighting on log updates - highlight exactly the string entered, but support wildcards (* and ?)
Replaced by simpler #834 |
Summary
Problem
Application log view is really user-friendly, but underlying log system neither.
To improve it, few features will be added:
Related to PICARD-1185
Solution
log.(error|warn|debug|info)()
now accepts an optional set of domains usingdomains
kv parameterMessageObj
, which hasdomains
andlevel
propertiesAction
Review & test, the code is still very experimental, feedback welcome