Skip to content

Commit c491d68

Browse files
committed
Adding handler base and logrecord stuff
1 parent 1d634a4 commit c491d68

5 files changed

Lines changed: 338 additions & 12 deletions

File tree

stackify/__init__.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,12 @@
1111

1212
MAX_BATCH = 100
1313

14+
QUEUE_SIZE = 1000
1415

1516
import logging
17+
import inspect
18+
19+
DEFAULT_LEVEL = logging.ERROR
1620

1721
LOGGING_LEVELS = {
1822
logging.CRITICAL: 'CRITICAL',
@@ -31,3 +35,28 @@
3135
from stackify.application import ApiConfiguration
3236
from stackify.http import HTTPClient
3337

38+
from stackify.handler import StackifyHandler
39+
40+
41+
def getLogger(name=None, **kwargs):
42+
if not name:
43+
try:
44+
frame = inspect.stack()[1]
45+
module = inspect.getmodule(frame[0])
46+
name = module.__name__
47+
except IndexError:
48+
name = 'stackify-python-unknown'
49+
50+
logger = logging.getLogger(name)
51+
52+
if not [isinstance(x, StackifyHandler) for x in logger.handlers]:
53+
internal_log.debug('Creating handler for logger {0}'.format(name))
54+
handler = StackifyHandler(**kwargs)
55+
logger.addHandler(handler)
56+
57+
if logger.getEffectiveLevel() == logging.NOTSET:
58+
logger.setLevel(DEFAULT_LEVEL)
59+
60+
return logger
61+
62+

stackify/error.py

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,14 @@ def __init__(self):
1515
self.StackTrace = [] # array of TraceFrames
1616
self.InnerError = None # cause?
1717

18-
def load_stack(self):
19-
type_, value, tb = sys.exc_info()
18+
def load_stack(self, exc_info=None):
19+
if not exc_info:
20+
type_, value, tb = sys.exc_info()
21+
else:
22+
type_, value, tb = exc_info
23+
2024
stacks = traceback.extract_tb(tb)
25+
2126
self.ErrorType = type_.__name__
2227
self.Message = str(value)
2328
self.SourceMethod = stacks[-1][2]
@@ -54,13 +59,18 @@ def __init__(self):
5459
class StackifyError(JSONObject):
5560
def __init__(self):
5661
self.EnvironmentDetail = None # environment detail object
57-
self.OccurredEpochMillis = int(time.time() * 1000)
62+
self.OccurredEpochMillis = None
5863
self.Error = None # ErrorItem object
5964
self.WebRequestDetail = None # WebRequestDetail object
6065
self.CustomerName = None
6166
self.UserName = None
6267

63-
def load_exception(self):
68+
def load_exception(self, exc_info=None):
6469
self.Error = ErrorItem()
65-
self.Error.load_stack()
70+
self.Error.load_stack(exc_info)
71+
72+
def from_record(self, record):
73+
self.load_exception(record.exc_info)
74+
self.OccurredEpochMillis = int(record.created * 1000)
75+
6676

stackify/handler.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import logging
2+
import threading
3+
4+
try:
5+
from logging.handlers import QueueHandler, QueueListener
6+
except:
7+
from stackify.handler_backport import QueueHandler, QueueListener
8+
9+
try:
10+
import Queue as queue
11+
except ImportError:
12+
import queue
13+
14+
from stackify import QUEUE_SIZE
15+
from stackify.log import LogMsg
16+
from stackify.error import ErrorItem
17+
from stackify.http import HTTPClient
18+
19+
20+
21+
class StackifyHandler(QueueHandler):
22+
'''
23+
A handler class to format and queue log messages for later
24+
transmission to Stackify servers.
25+
'''
26+
27+
def __init__(self, queue_=None):
28+
if queue_ is None:
29+
queue_ = queue.Queue(QUEUE_SIZE)
30+
31+
super(StackifyHandler, self).__init__(queue_)
32+
33+
self.listener = StackifyListener(queue_)
34+
35+
def enqueue(self, record):
36+
'''
37+
Put a new record on the queue. If it's full, evict an item.
38+
'''
39+
try:
40+
self.queue.put_nowait(record)
41+
logger = logging.getLogger(__name__)
42+
logger.debug('put record ' + record.toJSON())
43+
except queue.Full:
44+
logger = logging.getLogger(__name__)
45+
logger.warn('StackifyHandler queue is full, evicting oldest record')
46+
self.queue.get_nowait()
47+
self.queue.put_nowait(record)
48+
49+
def prepare(self, record):
50+
print(record.__dict__)
51+
msg = LogMsg()
52+
msg.from_record(record)
53+
54+
return msg
55+
56+
57+
class StackifyListener(QueueListener):
58+
'''
59+
A listener to read queued log messages and send them to Stackify.
60+
'''
61+
62+
def __init__(self, queue_):
63+
super(StackifyListener, self).__init__(queue_)
64+

stackify/handler_backport.py

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
# Copyright 2001-2013 by Vinay Sajip. All Rights Reserved.
2+
#
3+
# Permission to use, copy, modify, and distribute this software and its
4+
# documentation for any purpose and without fee is hereby granted,
5+
# provided that the above copyright notice appear in all copies and that
6+
# both that copyright notice and this permission notice appear in
7+
# supporting documentation, and that the name of Vinay Sajip
8+
# not be used in advertising or publicity pertaining to distribution
9+
# of the software without specific, written prior permission.
10+
# VINAY SAJIP DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING
11+
# ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
12+
# VINAY SAJIP BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR
13+
# ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER
14+
# IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
15+
# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
16+
17+
18+
import logging
19+
import threading
20+
21+
22+
class QueueHandler(logging.Handler):
23+
"""
24+
This handler sends events to a queue. Typically, it would be used together
25+
with a multiprocessing Queue to centralise logging to file in one process
26+
(in a multi-process application), so as to avoid file write contention
27+
between processes.
28+
29+
This code is new in Python 3.2, but this class can be copy pasted into
30+
user code for use with earlier Python versions.
31+
"""
32+
33+
def __init__(self, queue):
34+
"""
35+
Initialise an instance, using the passed queue.
36+
"""
37+
logging.Handler.__init__(self)
38+
self.queue = queue
39+
40+
def enqueue(self, record):
41+
"""
42+
Enqueue a record.
43+
44+
The base implementation uses put_nowait. You may want to override
45+
this method if you want to use blocking, timeouts or custom queue
46+
implementations.
47+
"""
48+
self.queue.put_nowait(record)
49+
50+
def prepare(self, record):
51+
"""
52+
Prepares a record for queuing. The object returned by this method is
53+
enqueued.
54+
55+
The base implementation formats the record to merge the message
56+
and arguments, and removes unpickleable items from the record
57+
in-place.
58+
59+
You might want to override this method if you want to convert
60+
the record to a dict or JSON string, or send a modified copy
61+
of the record while leaving the original intact.
62+
"""
63+
# The format operation gets traceback text into record.exc_text
64+
# (if there's exception data), and also puts the message into
65+
# record.message. We can then use this to replace the original
66+
# msg + args, as these might be unpickleable. We also zap the
67+
# exc_info attribute, as it's no longer needed and, if not None,
68+
# will typically not be pickleable.
69+
self.format(record)
70+
record.msg = record.message
71+
record.args = None
72+
record.exc_info = None
73+
return record
74+
75+
def emit(self, record):
76+
"""
77+
Emit a record.
78+
79+
Writes the LogRecord to the queue, preparing it for pickling first.
80+
"""
81+
try:
82+
self.enqueue(self.prepare(record))
83+
except Exception:
84+
self.handleError(record)
85+
86+
87+
class QueueListener(object):
88+
"""
89+
This class implements an internal threaded listener which watches for
90+
LogRecords being added to a queue, removes them and passes them to a
91+
list of handlers for processing.
92+
"""
93+
_sentinel = None
94+
95+
def __init__(self, queue, *handlers):
96+
"""
97+
Initialise an instance with the specified queue and
98+
handlers.
99+
"""
100+
self.queue = queue
101+
self.handlers = handlers
102+
self._stop = threading.Event()
103+
self._thread = None
104+
105+
def dequeue(self, block):
106+
"""
107+
Dequeue a record and return it, optionally blocking.
108+
109+
The base implementation uses get. You may want to override this method
110+
if you want to use timeouts or work with custom queue implementations.
111+
"""
112+
return self.queue.get(block)
113+
114+
def start(self):
115+
"""
116+
Start the listener.
117+
118+
This starts up a background thread to monitor the queue for
119+
LogRecords to process.
120+
"""
121+
self._thread = t = threading.Thread(target=self._monitor)
122+
t.setDaemon(True)
123+
t.start()
124+
125+
def prepare(self , record):
126+
"""
127+
Prepare a record for handling.
128+
129+
This method just returns the passed-in record. You may want to
130+
override this method if you need to do any custom marshalling or
131+
manipulation of the record before passing it to the handlers.
132+
"""
133+
return record
134+
135+
def handle(self, record):
136+
"""
137+
Handle a record.
138+
139+
This just loops through the handlers offering them the record
140+
to handle.
141+
"""
142+
record = self.prepare(record)
143+
for handler in self.handlers:
144+
handler.handle(record)
145+
146+
def _monitor(self):
147+
"""
148+
Monitor the queue for records, and ask the handler
149+
to deal with them.
150+
151+
This method runs on a separate, internal thread.
152+
The thread will terminate if it sees a sentinel object in the queue.
153+
"""
154+
q = self.queue
155+
has_task_done = hasattr(q, 'task_done')
156+
while not self._stop.isSet():
157+
try:
158+
record = self.dequeue(True)
159+
if record is self._sentinel:
160+
break
161+
self.handle(record)
162+
if has_task_done:
163+
q.task_done()
164+
except queue.Empty:
165+
pass
166+
# There might still be records in the queue.
167+
while True:
168+
try:
169+
record = self.dequeue(False)
170+
if record is self._sentinel:
171+
break
172+
self.handle(record)
173+
if has_task_done:
174+
q.task_done()
175+
except queue.Empty:
176+
break
177+
178+
def enqueue_sentinel(self):
179+
"""
180+
This is used to enqueue the sentinel record.
181+
182+
The base implementation uses put_nowait. You may want to override this
183+
method if you want to use timeouts or work with custom queue
184+
implementations.
185+
"""
186+
self.queue.put_nowait(self._sentinel)
187+
188+
def stop(self):
189+
"""
190+
Stop the listener.
191+
192+
This asks the thread to terminate, and then waits for it to do so.
193+
Note that if you don't call this before your application exits, there
194+
may be some records still left on the queue, which won't be processed.
195+
"""
196+
self._stop.set()
197+
self.enqueue_sentinel()
198+
self._thread.join()
199+
self._thread = None
200+

0 commit comments

Comments
 (0)