Commit a72aba12 authored by Jean-François Roche's avatar Jean-François Roche

SiteErrorLog is now a package, remove code from here

parent 9e4fc5a3
##############################################################################
#
# Copyright (c) 2001, 2002 Zope Foundation and Contributors.
# All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE.
#
##############################################################################
"""Site error log module.
"""
import os
import sys
import time
import logging
from random import random
from thread import allocate_lock
from AccessControl.class_init import InitializeClass
from AccessControl.SecurityInfo import ClassSecurityInfo
from AccessControl.SecurityManagement import getSecurityManager
from AccessControl.unauthorized import Unauthorized
from Acquisition import aq_base
from App.Dialogs import MessageDialog
from OFS.SimpleItem import SimpleItem
from Products.PageTemplates.PageTemplateFile import PageTemplateFile
from zExceptions.ExceptionFormatter import format_exception
LOG = logging.getLogger('Zope.SiteErrorLog')
# Permission names
use_error_logging = 'Log Site Errors'
log_to_event_log = 'Log to the Event Log'
# We want to restrict the rate at which errors are sent to the Event Log
# because we know that these errors can be generated quick enough to
# flood some zLOG backends. zLOG is used to notify someone of a problem,
# not to record every instance.
# This dictionary maps exception name to a value which encodes when we
# can next send the error with that name into the event log. This dictionary
# is shared between threads and instances. Concurrent access will not
# do much harm.
_rate_restrict_pool = {}
# The number of seconds that must elapse on average between sending two
# exceptions of the same name into the the Event Log. one per minute.
_rate_restrict_period = 60
# The number of exceptions to allow in a burst before the above limit
# kicks in. We allow five exceptions, before limiting them to one per
# minute.
_rate_restrict_burst = 5
_www = os.path.join(os.path.dirname(__file__), 'www')
# temp_logs holds the logs.
temp_logs = {} # { oid -> [ traceback string ] }
cleanup_lock = allocate_lock()
class SiteErrorLog (SimpleItem):
"""Site error log class. You can put an error log anywhere in the tree
and exceptions in that area will be posted to the site error log.
"""
meta_type = 'Site Error Log'
id = 'error_log'
keep_entries = 20
copy_to_zlog = True
security = ClassSecurityInfo()
manage_options = (
{'label': 'Log', 'action': 'manage_main'},
) + SimpleItem.manage_options
security.declareProtected(use_error_logging, 'manage_main')
manage_main = PageTemplateFile('main.pt', _www)
security.declareProtected(use_error_logging, 'showEntry')
showEntry = PageTemplateFile('showEntry.pt', _www)
security.declarePrivate('manage_beforeDelete')
def manage_beforeDelete(self, item, container):
if item is self:
try:
del container.__error_log__
except AttributeError:
pass
security.declarePrivate('manage_afterAdd')
def manage_afterAdd(self, item, container):
if item is self:
container.__error_log__ = aq_base(self)
def _setId(self, id):
if id != self.id:
raise ValueError(MessageDialog(
title='Invalid Id',
message='Cannot change the id of a SiteErrorLog',
action='./manage_main'))
def _getLog(self):
"""Returns the log for this object.
Careful, the log is shared between threads.
"""
log = temp_logs.get(self._p_oid, None)
if log is None:
log = []
temp_logs[self._p_oid] = log
return log
security.declareProtected(use_error_logging, 'forgetEntry')
def forgetEntry(self, id, REQUEST=None):
"""Removes an entry from the error log."""
log = self._getLog()
cleanup_lock.acquire()
i=0
for entry in log:
if entry['id'] == id:
del log[i]
i += 1
cleanup_lock.release()
if REQUEST is not None:
REQUEST.RESPONSE.redirect(
'%s/manage_main?manage_tabs_message=Error+log+entry+was+removed.' %
self.absolute_url())
# Exceptions that happen all the time, so we dont need
# to log them. Eventually this should be configured
# through-the-web.
_ignored_exceptions = ( 'Unauthorized', 'NotFound', 'Redirect' )
security.declarePrivate('raising')
def raising(self, info):
"""Log an exception.
Called by SimpleItem's exception handler.
Returns the url to view the error log entry
"""
try:
now = time.time()
try:
tb_text = None
tb_html = None
strtype = str(getattr(info[0], '__name__', info[0]))
if strtype in self._ignored_exceptions:
return
if not isinstance(info[2], basestring):
tb_text = ''.join(
format_exception(*info, **{'as_html': 0}))
tb_html = ''.join(
format_exception(*info, **{'as_html': 1}))
else:
tb_text = info[2]
request = getattr(self, 'REQUEST', None)
url = None
username = None
userid = None
req_html = None
try:
strv = str(info[1])
except:
strv = '<unprintable %s object>' % type(info[1]).__name__
if request:
url = request.get('URL', '?')
usr = getSecurityManager().getUser()
username = usr.getUserName()
userid = usr.getId()
try:
req_html = str(request)
except:
pass
if strtype == 'NotFound':
strv = url
next = request['TraversalRequestNameStack']
if next:
next = list(next)
next.reverse()
strv = '%s [ /%s ]' % (strv, '/'.join(next))
log = self._getLog()
entry_id = str(now) + str(random()) # Low chance of collision
log.append({
'type': strtype,
'value': strv,
'time': now,
'id': entry_id,
'tb_text': tb_text,
'tb_html': tb_html,
'username': username,
'userid': userid,
'url': url,
'req_html': req_html,
})
cleanup_lock.acquire()
try:
if len(log) >= self.keep_entries:
del log[:-self.keep_entries]
finally:
cleanup_lock.release()
except:
LOG.error('Error while logging', exc_info=sys.exc_info())
else:
if self.copy_to_zlog:
self._do_copy_to_zlog(now,strtype,entry_id,str(url),tb_text)
return '%s/showEntry?id=%s' % (self.absolute_url(), entry_id)
finally:
info = None
def _do_copy_to_zlog(self,now,strtype,entry_id,url,tb_text):
when = _rate_restrict_pool.get(strtype,0)
if now>when:
next_when = max(when, now-_rate_restrict_burst*_rate_restrict_period)
next_when += _rate_restrict_period
_rate_restrict_pool[strtype] = next_when
LOG.error('%s %s\n%s' % (entry_id, url, tb_text.rstrip()))
security.declareProtected(use_error_logging, 'getProperties')
def getProperties(self):
return {
'keep_entries': self.keep_entries,
'copy_to_zlog': self.copy_to_zlog,
'ignored_exceptions': self._ignored_exceptions,
}
security.declareProtected(log_to_event_log, 'checkEventLogPermission')
def checkEventLogPermission(self):
if not getSecurityManager().checkPermission(log_to_event_log, self):
raise Unauthorized, ('You do not have the "%s" permission.' %
log_to_event_log)
return 1
security.declareProtected(use_error_logging, 'setProperties')
def setProperties(self, keep_entries, copy_to_zlog=0,
ignored_exceptions=(), RESPONSE=None):
"""Sets the properties of this site error log.
"""
copy_to_zlog = not not copy_to_zlog
if copy_to_zlog and not self.copy_to_zlog:
# Before turning on event logging, check the permission.
self.checkEventLogPermission()
self.keep_entries = int(keep_entries)
self.copy_to_zlog = copy_to_zlog
self._ignored_exceptions = tuple(
filter(None, map(str, ignored_exceptions)))
if RESPONSE is not None:
RESPONSE.redirect(
'%s/manage_main?manage_tabs_message=Changed+properties.' %
self.absolute_url())
security.declareProtected(use_error_logging, 'getLogEntries')
def getLogEntries(self):
"""Returns the entries in the log, most recent first.
Makes a copy to prevent changes.
"""
# List incomprehension ;-)
res = [entry.copy() for entry in self._getLog()]
res.reverse()
return res
security.declareProtected(use_error_logging, 'getLogEntryById')
def getLogEntryById(self, id):
"""Returns the specified log entry.
Makes a copy to prevent changes. Returns None if not found.
"""
for entry in self._getLog():
if entry['id'] == id:
return entry.copy()
return None
security.declareProtected(use_error_logging, 'getLogEntryAsText')
def getLogEntryAsText(self, id, RESPONSE=None):
"""Returns the specified log entry.
Makes a copy to prevent changes. Returns None if not found.
"""
entry = self.getLogEntryById(id)
if entry is None:
return 'Log entry not found or expired'
if RESPONSE is not None:
RESPONSE.setHeader('Content-Type', 'text/plain')
return entry['tb_text']
InitializeClass(SiteErrorLog)
def manage_addErrorLog(dispatcher, RESPONSE=None):
"""Add a site error log to a container."""
log = SiteErrorLog()
dispatcher._setObject(log.id, log)
if RESPONSE is not None:
RESPONSE.redirect(
dispatcher.DestinationURL() +
'/manage_main?manage_tabs_message=Error+Log+Added.' )
##############################################################################
#
# Copyright (c) 2001, 2002 Zope Foundation and Contributors.
# All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE.
#
##############################################################################
import SiteErrorLog
def initialize(context):
context.registerClass(SiteErrorLog.SiteErrorLog,
constructors=(SiteErrorLog.manage_addErrorLog,),
permission=SiteErrorLog.use_error_logging)
"""SiteErrorLog tests
"""
from Testing.makerequest import makerequest
import Zope2
Zope2.startup()
import transaction
import sys
import unittest
import logging
class SiteErrorLogTests(unittest.TestCase):
def setUp(self):
transaction.begin()
self.app = makerequest(Zope2.app())
try:
if not hasattr(self.app, 'error_log'):
# If ZopeLite was imported, we have no default error_log
from Products.SiteErrorLog.SiteErrorLog import SiteErrorLog
self.app._setObject('error_log', SiteErrorLog())
self.app.manage_addDTMLMethod('doc', '')
self.logger = logging.getLogger('Zope.SiteErrorLog')
self.log = logging.handlers.BufferingHandler(sys.maxint)
self.logger.addHandler(self.log)
self.old_level = self.logger.level
self.logger.setLevel(logging.ERROR)
except:
self.tearDown()
def tearDown(self):
self.logger.removeHandler(self.log)
self.logger.setLevel(self.old_level)
transaction.abort()
self.app._p_jar.close()
def testInstantiation(self):
# Retrieve the error_log by ID
sel_ob = getattr(self.app, 'error_log', None)
# Does the error log exist?
self.assert_(sel_ob is not None)
# Is the __error_log__ hook in place?
self.assert_(self.app.__error_log__ == sel_ob)
# Right now there should not be any entries in the log
# but if another test fails and leaves something in the
# log (which belongs to app , we get a spurious error here.
# There's no real point in testing this anyway.
#self.assertEquals(len(sel_ob.getLogEntries()), 0)
def testSimpleException(self):
# Grab the Site Error Log and make sure it's empty
sel_ob = self.app.error_log
previous_log_length = len(sel_ob.getLogEntries())
# Fill the DTML method at self.root.doc with bogus code
dmeth = self.app.doc
dmeth.manage_upload(file="""<dtml-var expr="1/0">""")
# "Faking out" the automatic involvement of the Site Error Log
# by manually calling the method "raising" that gets invoked
# automatically in a normal web request environment.
try:
dmeth.__call__()
except ZeroDivisionError:
sel_ob.raising(sys.exc_info())
# Now look at the SiteErrorLog, it has one more log entry
self.assertEquals(len(sel_ob.getLogEntries()), previous_log_length+1)
def testForgetException(self):
elog = self.app.error_log
# Create a predictable error
try:
raise AttributeError, "DummyAttribute"
except AttributeError:
info = sys.exc_info()
elog.raising(info)
previous_log_length = len(elog.getLogEntries())
entries = elog.getLogEntries()
self.assertEquals(entries[0]['value'], "DummyAttribute")
# Kick it
elog.forgetEntry(entries[0]['id'])
# Really gone?
self.assertEquals(len(elog.getLogEntries()), previous_log_length-1)
def testIgnoredException(self):
# Grab the Site Error Log
sel_ob = self.app.error_log
previous_log_length = len(sel_ob.getLogEntries())
# Tell the SiteErrorLog to ignore ZeroDivisionErrors
current_props = sel_ob.getProperties()
ignored = list(current_props['ignored_exceptions'])
ignored.append('ZeroDivisionError')
sel_ob.setProperties( current_props['keep_entries']
, copy_to_zlog = current_props['copy_to_zlog']
, ignored_exceptions = ignored
)
# Fill the DTML method at self.root.doc with bogus code
dmeth = self.app.doc
dmeth.manage_upload(file="""<dtml-var expr="1/0">""")
# "Faking out" the automatic involvement of the Site Error Log
# by manually calling the method "raising" that gets invoked
# automatically in a normal web request environment.
try:
dmeth.__call__()
except ZeroDivisionError:
sel_ob.raising(sys.exc_info())
# Now look at the SiteErrorLog, it must have the same number of
# log entries
self.assertEquals(len(sel_ob.getLogEntries()), previous_log_length)
def testEntryID(self):
elog = self.app.error_log
# Create a predictable error
try:
raise AttributeError, "DummyAttribute"
except AttributeError:
info = sys.exc_info()
elog.raising(info)
entries = elog.getLogEntries()
entry_id = entries[0]['id']
self.assertTrue(entry_id in self.log.buffer[-1].msg,
(entry_id, self.log.buffer[-1].msg))
def testCleanup(self):
# Need to make sure that the __error_log__ hook gets cleaned up
self.app._delObject('error_log')
self.assertEquals(getattr(self.app, '__error_log__', None), None)
def test_suite():
suite = unittest.TestSuite()
suite.addTest(unittest.makeSuite(SiteErrorLogTests))
return suite
<h1 tal:replace="structure context/manage_page_header">Header</h1>
<h1 tal:replace="structure context/manage_tabs">Tabs</h1>
<p class="form-help">
This page lists the exceptions that have occurred in this site
recently. You can configure how many exceptions should be kept
and whether the exceptions should be copied to Zope's event log
file(s).
</p>
<form action="setProperties" method="post">
<table tal:define="props container/getProperties">
<tr>
<td align="left" valign="top">
<div class="form-label">
Number of exceptions to keep
</div>
</td>
<td align="left" valign="top">
<input type="text" name="keep_entries" size="40"
tal:attributes="value props/keep_entries" />
</td>
</tr>
<tr>
<td align="left" valign="top">
<div class="form-label">
Copy exceptions to the event log
</div>
</td>
<td align="left" valign="top">
<input type="checkbox" name="copy_to_zlog"
tal:attributes="checked props/copy_to_zlog;
disabled not:container/checkEventLogPermission|nothing" />
</td>
</tr>
<tr>
<td align="left" valign="top">
<div class="form-label">
Ignored exception types
</div>
</td>
<td align="left" valign="top">
<textarea name="ignored_exceptions:lines" cols="40" rows="3"
tal:content="python: '\n'.join(props['ignored_exceptions'])"></textarea>
</td>
</tr>
<tr>
<td align="left" valign="top">
</td>
<td align="left" valign="top">
<div class="form-element">
<input class="form-element" type="submit" name="submit"
value=" Save Changes " />
</div>
</td>
</tr>
</table>
<h3>Exception Log (most recent first)</h3>
<div tal:define="entries container/getLogEntries">
<em tal:condition="not:entries">
No exceptions logged.
</em>
<table tal:condition="entries">
<tr>
<th align="left">Time</th>
<th align="left">Username (User Id)</th>
<th align="left">Exception</th>
<th></th>
</tr>
<tr tal:repeat="entry entries">
<td valign="top" nowrap="nowrap">
<span tal:content="python: modules['DateTime'].DateTime(entry['time']).Time()">13:04:41</span>
</td>
<td>
<span tal:content="string: ${entry/username} (${entry/userid})">
joe (joe)
</span>
</td>
<td valign="top">
<a href="showEntry" tal:attributes="href string:showEntry?id=${entry/id}"
>
<span tal:content="entry/type">AttributeError</span>:
<span tal:define="value entry/value"
tal:content="python: len(value) &lt; 70 and value or value[:70] + '...'">
Application object has no attribute "zzope"</span>
</a>
</td>
<td><a href="#"
tal:attributes="href string:${context/absolute_url}/forgetEntry?id=${entry/id}"
>Forget this entry</a></td>
</tr>
</table>
</div>
</form>
<p>
<form action="manage_main" method="GET">
<input type="submit" name="submit" value=" Refresh " />
</form>
</p>
<h1 tal:replace="structure context/manage_page_footer">Footer</h1>
<h1 tal:replace="structure here/manage_page_header">Header</h1>
<h1 tal:replace="structure here/manage_tabs">Tabs</h1>
<h3>Exception traceback</h3>
<p>
<form action="manage_main" method="GET">
<input type="submit" name="submit" value=" Return to log " />
</form>
</p>
<div tal:define="entry python:container.getLogEntryById(request.get('id'))">
<em tal:condition="not:entry">
The specified log entry was not found. It may have expired.
</em>
<div tal:condition="entry">
<table>
<tr>
<th align="left" valign="top">Time</th>
<td tal:content="python: modules['DateTime'].DateTime(entry['time'])"></td>
</tr>
<tr>
<th align="left" valign="top">User Name (User Id)</th>
<td tal:content="string: ${entry/username} (${entry/userid})">joe (joe)</td>
</tr>
<tr>
<th align="left" valign="top">Request URL</th>
<td tal:content="entry/url">http://example.com</td>
</tr>
<tr>
<th align="left" valign="top">Exception Type</th>
<td tal:content="entry/type">AttributeError</td>
</tr>
<tr>
<th align="left" valign="top">Exception Value</th>
<td tal:content="entry/value">zzope</td>
</tr>
</table>
<div tal:condition="entry/tb_html" tal:content="structure entry/tb_html">
Traceback (HTML)
</div>
<pre tal:condition="not:entry/tb_html" tal:content="entry/tb_text">
Traceback (text)
</pre>
<p tal:condition="entry/tb_text"><a href="" tal:attributes="href
string:getLogEntryAsText?id=${entry/id}">Display
traceback as text</a></p>
<div tal:condition="entry/req_html">
<p>
<form action="manage_main" method="GET">
<input type="submit" name="submit" value=" Return to log " />
</form>
</p>
<h3>REQUEST</h3>
<div tal:replace="structure entry/req_html"></div>
</div>
</div>
<p>
<form action="manage_main" method="GET">
<input type="submit" name="submit" value=" Return to log " />
</form>
</p>
</div>
<h1 tal:replace="structure here/manage_page_footer">Footer</h1>
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment