commit 00739ee4ebf3c74d1451225c026a90ab52eed711
Author: Damian Johnson <atagar(a)torproject.org>
Date: Sat Oct 8 23:23:08 2011 -0700
Adding logging utility
Borrowing the logger from arm, with some refactoring to remove unneeded
functions and make it conform with this project's coding conventions.
---
stem/util/log.py | 147 +++++++++++++++++++++++++++++++++++++++++++++++++++++
stem/util/term.py | 2 +-
2 files changed, 148 insertions(+), 1 deletions(-)
diff --git a/stem/util/log.py b/stem/util/log.py
new file mode 100644
index 0000000..dae2e2a
--- /dev/null
+++ b/stem/util/log.py
@@ -0,0 +1,147 @@
+"""
+Tracks application events, both directing them to attached listeners and
+keeping a record of them. A limited space is provided for old events, keeping
+and trimming them on a per-runlevel basis (ie, too many DEBUG events will only
+result in entries from that runlevel being dropped). All functions are thread
+safe.
+"""
+
+import time
+from sys import maxint
+from threading import RLock
+
+from stem.util import enum
+
+# Logging runlevels. These are *very* commonly used so including shorter
+# aliases (so they can be referenced as log.DEBUG, log.WARN, etc).
+Runlevel = enum.Enum(*[(v, v) for v in ("DEBUG", "INFO", "NOTICE", "WARN", "ERR")])
+DEBUG, INFO, NOTICE, WARN, ERR = Runlevel.values()
+
+LOG_LOCK = RLock() # provides thread safety for logging operations
+MAX_LOG_SIZE = 1000 # maximum log entries per runlevel to be persisted
+
+# chronologically ordered records of events for each runlevel, stored as tuples
+# consisting of: (time, message)
+_backlog = dict([(level, []) for level in Runlevel.values()])
+
+# mapping of runlevels to the listeners interested in receiving events from it
+_listeners = dict([(level, []) for level in Runlevel.values()])
+
+def log(level, msg, event_time = None):
+ """
+ Registers an event, directing it to interested listeners and preserving it in
+ the backlog.
+
+ Arguments:
+ level (Runlevel) - runlevel corresponding to the message severity
+ msg (str) - string associated with the message
+ event_time (int) - unix time at which the event occurred, current time if
+ undefined
+ """
+
+ if event_time == None: event_time = time.time()
+
+ LOG_LOCK.acquire()
+ try:
+ new_event = (event_time, msg)
+ event_backlog = _backlog[level]
+
+ # inserts the new event into the backlog
+ if not event_backlog or event_time >= event_backlog[-1][0]:
+ # newest event - append to end
+ event_backlog.append(new_event)
+ elif event_time <= event_backlog[0][0]:
+ # oldest event - insert at start
+ event_backlog.insert(0, new_event)
+ else:
+ # somewhere in the middle - start checking from the end
+ for i in range(len(event_backlog) - 1, -1, -1):
+ if event_backlog[i][0] <= event_time:
+ event_backlog.insert(i + 1, new_event)
+ break
+
+ # truncates backlog if too long
+ to_delete = len(event_backlog) - MAX_LOG_SIZE
+ if to_delete > 0: del event_backlog[:to_delete]
+
+ # notifies listeners
+ for callback in _listeners[level]:
+ callback(level, msg, event_time)
+ finally:
+ LOG_LOCK.release()
+
+def add_listener(levels, callback, dump_backlog = False):
+ """
+ Directs future events to the given callback function.
+
+ Arguments:
+ levels (list) - runlevels for the listener to be notified of
+ callback (functor) - functor to accept the events, of the form:
+ my_function(runlevel, msg, time)
+ dump_backlog (bool) - if true then this passes prior events to the callback
+ function (in chronological order) before returning
+ """
+
+ LOG_LOCK.acquire()
+ try:
+ for level in levels:
+ if not callback in _listeners[level]:
+ _listeners[level].append(callback)
+
+ if dump_backlog:
+ for level, msg, event_time in _get_entries(levels):
+ callback(level, msg, event_time)
+ finally:
+ LOG_LOCK.release()
+
+def remove_listener(level, callback):
+ """
+ Prevents a listener from being notified of further events.
+
+ Arguments:
+ level (Runlevel) - runlevel for the listener to be removed from
+ callback (functor) - functor to be removed
+
+ Returns:
+ True if a listener was removed, False otherwise
+ """
+
+ if callback in _listeners[level]:
+ _listeners[level].remove(callback)
+ return True
+ else: return False
+
+def _get_entries(levels):
+ """
+ Generator for providing past events belonging to the given runlevels (in
+ chronological order). This should be used under the LOG_LOCK to prevent
+ concurrent modifications.
+
+ Arguments:
+ levels (list) - runlevels for which events are provided
+ """
+
+ # drops any runlevels if there aren't entries in it
+ to_remove = [level for level in levels if not _backlog[level]]
+ for level in to_remove: levels.remove(level)
+
+ # tracks where unprocessed entries start in the backlog
+ backlog_ptr = dict([(level, 0) for level in levels])
+
+ while levels:
+ earliest_level, earliest_msg, earliest_time = None, "", maxint
+
+ # finds the earliest unprocessed event
+ for level in levels:
+ entry = _backlog[level][backlog_ptr[level]]
+
+ if entry[0] < earliest_time:
+ earliest_level, earliest_msg, earliest_time = level, entry[1], entry[0]
+
+ yield (earliest_level, earliest_msg, earliest_time)
+
+ # removes runlevel if there aren't any more entries
+ backlog_ptr[earliest_level] += 1
+ if len(_backlog[earliest_level]) <= backlog_ptr[earliest_level]:
+ levels.remove(earliest_level)
+
diff --git a/stem/util/term.py b/stem/util/term.py
index 7f3449e..a2465a2 100644
--- a/stem/util/term.py
+++ b/stem/util/term.py
@@ -2,7 +2,7 @@
Utilities for working with the terminal.
"""
-import enum
+from stem.util import enum
TERM_COLORS = ("BLACK", "RED", "GREEN", "YELLOW", "BLUE", "MAGENTA", "CYAN", "WHITE")