[tor-commits] [nyx/master] Revise log panel's filter handling
atagar at torproject.org
atagar at torproject.org
Tue May 5 05:42:06 UTC 2015
commit c8341ae3fee5330e3fd013aaa054ef3a59736a92
Author: Damian Johnson <atagar at torproject.org>
Date: Fri May 1 09:14:31 2015 -0700
Revise log panel's filter handling
Adding a small LogFilters class which allows us to drop quite a bit.
---
nyx/log_panel.py | 110 ++++++++-------------------------------------------
nyx/menu/actions.py | 6 ++-
nyx/util/log.py | 49 +++++++++++++++++++++++
3 files changed, 69 insertions(+), 96 deletions(-)
diff --git a/nyx/log_panel.py b/nyx/log_panel.py
index 4a7e6de..2840c86 100644
--- a/nyx/log_panel.py
+++ b/nyx/log_panel.py
@@ -4,7 +4,6 @@ for. This provides prepopulation from the log file and supports filtering by
regular expressions.
"""
-import re
import os
import time
import curses
@@ -19,7 +18,7 @@ import nyx.arguments
import nyx.popups
from nyx.util import join, panel, tor_controller, ui_tools
-from nyx.util.log import TOR_RUNLEVELS, LogFileOutput, LogGroup, LogEntry, read_tor_log, condense_runlevels, days_since, log_file_path
+from nyx.util.log import TOR_RUNLEVELS, LogFileOutput, LogGroup, LogEntry, LogFilters, read_tor_log, condense_runlevels, days_since, log_file_path
ENTRY_INDENT = 2 # spaces an entry's message is indented after the first line
@@ -56,10 +55,6 @@ CONFIG = conf.config_dict('nyx', {
CONTENT_HEIGHT_REDRAW_THRESHOLD = 3
-# maximum number of regex filters we'll remember
-
-MAX_REGEX_FILTERS = 5
-
# Log buffer so we start collecting stem/nyx events when imported. This is used
# to make our LogPanel when curses initializes.
@@ -79,21 +74,7 @@ class LogPanel(panel.Panel, threading.Thread):
threading.Thread.__init__(self)
self.setDaemon(True)
- # regex filters the user has defined
-
- self.filter_options = []
-
- for filter in CONFIG['features.log.regex']:
- # checks if we can't have more filters
-
- if len(self.filter_options) >= MAX_REGEX_FILTERS:
- break
-
- try:
- re.compile(filter)
- self.filter_options.append(filter)
- except re.error as exc:
- log.notice('Invalid regular expression pattern (%s): %s' % (exc, filter))
+ self._filter = LogFilters(initial_filters = CONFIG['features.log.regex'])
self.logged_events = [] # needs to be set before we receive any events
@@ -102,7 +83,6 @@ class LogPanel(panel.Panel, threading.Thread):
self.logged_events = self.set_event_listening(logged_events)
- self.regex_filter = None # filter for presented log events (no filtering if None)
self.last_content_height = 0 # height of the rendered content when last drawn
self._log_file = LogFileOutput(CONFIG['features.log_file'])
self.scroll = 0
@@ -113,11 +93,6 @@ class LogPanel(panel.Panel, threading.Thread):
self._last_update = -1 # time the content was last revised
self._halt = False # terminates thread if true
self._cond = threading.Condition() # used for pausing/resuming the thread
-
- # restricts concurrent write access to attributes used to draw the display
- # and pausing:
- # msg_log, logged_events, regex_filter, scroll
-
self.vals_lock = threading.RLock()
# cached parameters (invalidated if arguments for them change)
@@ -187,49 +162,7 @@ class LogPanel(panel.Panel, threading.Thread):
Provides our currently selected regex filter.
"""
- return self.filter_options[0] if self.regex_filter else None
-
- def set_filter(self, log_filter):
- """
- Filters log entries according to the given regular expression.
-
- Arguments:
- log_filter - regular expression used to determine which messages are
- shown, None if no filter should be applied
- """
-
- if log_filter == self.regex_filter:
- return
-
- with self.vals_lock:
- self.regex_filter = log_filter
- self.redraw(True)
-
- def make_filter_selection(self, selected_option):
- """
- Makes the given filter selection, applying it to the log and reorganizing
- our filter selection.
-
- Arguments:
- selected_option - regex filter we've already added, None if no filter
- should be applied
- """
-
- if selected_option:
- try:
- self.set_filter(re.compile(selected_option))
-
- # move selection to top
-
- self.filter_options.remove(selected_option)
- self.filter_options.insert(0, selected_option)
- except re.error as exc:
- # shouldn't happen since we've already checked validity
-
- log.warn("Invalid regular expression ('%s': %s) - removing from listing" % (selected_option, exc))
- self.filter_options.remove(selected_option)
- else:
- self.set_filter(None)
+ return self._filter
def show_filter_prompt(self):
"""
@@ -239,15 +172,7 @@ class LogPanel(panel.Panel, threading.Thread):
regex_input = nyx.popups.input_prompt('Regular expression: ')
if regex_input:
- try:
- self.set_filter(re.compile(regex_input))
-
- if regex_input in self.filter_options:
- self.filter_options.remove(regex_input)
-
- self.filter_options.insert(0, regex_input)
- except re.error as exc:
- nyx.popups.show_msg('Unable to compile expression: %s' % exc, 2)
+ self._filter.select(regex_input)
def show_event_selection_prompt(self):
"""
@@ -333,7 +258,7 @@ class LogPanel(panel.Panel, threading.Thread):
with self.vals_lock:
try:
for entry in reversed(self._msg_log):
- is_visible = not self.regex_filter or self.regex_filter.search(entry.display_message)
+ is_visible = self._filter.match(entry.display_message)
if is_visible:
snapshot_file.write(entry.display_message + '\n')
@@ -363,27 +288,24 @@ class LogPanel(panel.Panel, threading.Thread):
# Provides menu to pick regular expression filters or adding new ones:
# for syntax see: http://docs.python.org/library/re.html#regular-expression-syntax
- options = ['None'] + self.filter_options + ['New...']
- old_selection = 0 if not self.regex_filter else 1
+ options = ['None'] + self._filter.latest_selections() + ['New...']
+ old_selection = 0 if not self._filter.selection() else 1
# does all activity under a curses lock to prevent redraws when adding
# new filters
- with CURSES_LOCK:
+ with panel.CURSES_LOCK:
selection = nyx.popups.show_menu('Log Filter:', options, old_selection)
# applies new setting
if selection == 0:
- self.set_filter(None)
+ self._filter.select(None)
elif selection == len(options) - 1:
# selected 'New...' option - prompt user to input regular expression
self.show_filter_prompt()
elif selection != -1:
- self.make_filter_selection(self.filter_options[selection - 1])
-
- if len(self.filter_options) > MAX_REGEX_FILTERS:
- del self.filter_options[MAX_REGEX_FILTERS:]
+ self._filter.select(self._filter.latest_selections()[selection - 1])
elif key.match('e'):
self.show_event_selection_prompt()
elif key.match('a'):
@@ -399,7 +321,7 @@ class LogPanel(panel.Panel, threading.Thread):
('down arrow', 'scroll log down a line', None),
('a', 'save snapshot of the log', None),
('e', 'change logged events', None),
- ('f', 'log regex filter', 'enabled' if self.regex_filter else 'disabled'),
+ ('f', 'log regex filter', 'enabled' if self._filter.selection() else 'disabled'),
('u', 'duplicate log entries', 'visible' if CONFIG['features.log.showDuplicateEntries'] else 'hidden'),
('c', 'clear event log', None),
]
@@ -419,10 +341,10 @@ class LogPanel(panel.Panel, threading.Thread):
# draws the top label
if self.is_title_visible():
- comp = condense_runlevels(*self.logged_events)
+ comp = list(condense_runlevels(*self.logged_events))
- if self.regex_filter:
- comp.append('filter: %s' % self.regex_filter)
+ if self._filter.selection():
+ comp.append('filter: %s' % self._filter.selection())
comp_str = join(comp, ', ', width - 10)
title = 'Events (%s):' % comp_str if comp_str else 'Events:'
@@ -470,7 +392,7 @@ class LogPanel(panel.Panel, threading.Thread):
while deduplicated_log:
entry, duplicate_count = deduplicated_log.pop(0)
- if self.regex_filter and not self.regex_filter.search(entry.display_message):
+ if not self._filter.match(entry.display_message):
continue # filter doesn't match log message - skip
# checks if we should be showing a divider with the date
@@ -701,6 +623,6 @@ class LogPanel(panel.Panel, threading.Thread):
# notifies the display that it has new content
- if not self.regex_filter or self.regex_filter.search(event.display_message):
+ if self._filter.match(event.display_message):
with self._cond:
self._cond.notifyAll()
diff --git a/nyx/menu/actions.py b/nyx/menu/actions.py
index 03b7b77..25b7c14 100644
--- a/nyx/menu/actions.py
+++ b/nyx/menu/actions.py
@@ -213,12 +213,14 @@ def make_log_menu(log_panel):
# filter submenu
+ log_filter = log_panel.get_filter()
+
filter_menu = nyx.menu.item.Submenu('Filter')
- filter_group = nyx.menu.item.SelectionGroup(log_panel.make_filter_selection, log_panel.get_filter())
+ filter_group = nyx.menu.item.SelectionGroup(log_filter.select, log_filter.selection())
filter_menu.add(nyx.menu.item.SelectionMenuItem('None', filter_group, None))
- for option in log_panel.filter_options:
+ for option in log_filter.latest_selections():
filter_menu.add(nyx.menu.item.SelectionMenuItem(option, filter_group, option))
filter_menu.add(nyx.menu.item.MenuItem('New...', log_panel.show_filter_prompt))
diff --git a/nyx/util/log.py b/nyx/util/log.py
index e188cb4..64abc4f 100644
--- a/nyx/util/log.py
+++ b/nyx/util/log.py
@@ -3,8 +3,10 @@ Logging utilities, primiarily short aliases for logging a message at various
runlevels.
"""
+import collections
import datetime
import os
+import re
import time
import threading
@@ -307,6 +309,53 @@ class LogFileOutput(object):
self._file = None
+class LogFilters(object):
+ """
+ Regular expression filtering for log output. This is thread safe and tracks
+ the latest selections.
+ """
+
+ def __init__(self, initial_filters = None, max_filters = 5):
+ self._max_filters = max_filters
+ self._selected = None
+ self._past_filters = collections.OrderedDict()
+ self._lock = threading.RLock()
+
+ if initial_filters:
+ for regex in initial_filters:
+ self.select(regex)
+
+ self.select(None)
+
+ def select(self, regex):
+ with self._lock:
+ if regex is None:
+ self._selected = None
+ return
+
+ if regex in self._past_filters:
+ del self._past_filters[regex]
+
+ try:
+ self._past_filters[regex] = re.compile(regex)
+ self._selected = regex
+
+ if len(self._past_filters) > self._max_filters:
+ self._past_filters.popitem(False)
+ except re.error as exc:
+ notice('Invalid regular expression pattern (%s): %s' % (exc, regex))
+
+ def selection(self):
+ return self._selected
+
+ def latest_selections(self):
+ return list(reversed(self._past_filters.keys()))
+
+ def match(self, message):
+ regex_filter = self._past_filters.get(self._selected)
+ return not regex_filter or bool(regex_filter.search(message))
+
+
def trace(msg, **attr):
_log(stem.util.log.TRACE, msg, **attr)
More information about the tor-commits
mailing list