Skip to content
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

Closed
wants to merge 39 commits into from

Conversation

zas
Copy link
Collaborator

@zas zas commented Feb 4, 2018

Summary

  • This is a…
    • Bug fix
    • Feature addition
    • Refactoring
    • Minor / simple change (like a typo)
    • Other
  • Describe this change in 1-2 sentences:

Problem

Application log view is really user-friendly, but underlying log system neither.
To improve it, few features will be added:

  • log "domains", allowing devs to regroup logs in sets
  • log verbosity filtering in log view
  • log domains filtering in log view
  • log highlighting in log view
  • saving current log to file

Related to PICARD-1185

Solution

  • log "domains" are implemented, every log.(error|warn|debug|info)() now accepts an optional set of domains using domains kv parameter
  • log messages are stored in a list of MessageObj, which has domains and level properties
  • log verbosity can be set through a menu in log view
  • log domains can be hidden through a menu in log view
  • user-defined text can be highlighted in log view

capture du 2018-02-04 18-19-43

Action

Review & test, the code is still very experimental, feedback welcome

@zas zas requested a review from samj1912 February 4, 2018 17:47
else:
mparms = {
'error': document['error']['message'],
'filename': file.filename
}
log.error(
"AcoustID: Lookup error for '%(filename)s': %(error)r" %
mparms)
mparms, domains='acoustid')
Copy link
Collaborator

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

Copy link
Collaborator

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

Copy link
Collaborator Author

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)

act.triggered.connect(partial(self._verbosity_changed, level))
self.verbosity_menu.addAction(act)

self.verbosity_menu_button = QtWidgets.QPushButton(_("Verbosity"))
Copy link
Collaborator

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?

Copy link
Collaborator Author

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.

@@ -25,6 +27,8 @@

class LogViewCommon(PicardDialog):

default_verbosity = {log.LOG_INFO, log.LOG_WARNING, log.LOG_ERROR, log.LOG_DEBUG}
Copy link
Collaborator

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.

Copy link
Collaborator Author

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.

@@ -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
Copy link
Collaborator

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?

Copy link
Collaborator Author

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

Copy link
Collaborator

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?

Copy link
Collaborator Author

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:

  1. open Log View
  2. check Debug mode checkbox
  3. do some action
  4. press Save as
  5. uncheck Debug mode checkbox

Instead of:

  1. Open Options
  2. go to panel "Advanced"
  3. set the log level to Debug (if it wasn't already...)
  4. Press OK
  5. open the Log View
  6. do some action
  7. press Save as
  8. Open Options
  9. go to panel "Advanced"
  10. set the log level back to what it was (if you remember)
  11. Press OK

self._display()
self._setup_formats()
self.verbosity = self.default_verbosity
self.hidden_domains = set()
Copy link
Collaborator

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.

Copy link
Collaborator Author

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.

self.hbox.addWidget(self.domains_menu_button)
self.hbox.addWidget(self.show_only_domains_menu_button)

self.highlight_text = QtWidgets.QLineEdit()
Copy link
Collaborator

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.

Copy link
Collaborator Author

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)

self.hbox.addWidget(self.highlight_button)
self.highlight_button.clicked.connect(self._highlight_do)

self.clear_log_button = QtWidgets.QPushButton(_("Clear Log"))
Copy link
Collaborator

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.

Copy link
Collaborator Author

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)
Copy link
Collaborator

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?

Copy link
Collaborator Author

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

@samj1912
Copy link
Collaborator

samj1912 commented Feb 6, 2018

Thought of a good idea to add domains to a function - (This can go in picard.util )

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.

zas added 22 commits February 8, 2018 01:09
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
zas added 15 commits February 8, 2018 01:09
- 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 ?)
- Overhead is reduced compared to previous version
- support mixing domains= log.*() argument and @Domain decorator, sets are merged
- add support for log.log()
@zas
Copy link
Collaborator Author

zas commented Feb 9, 2018

Replaced by simpler #834

@zas zas deleted the log_domains branch March 30, 2019 15:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
2 participants