• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

zopefoundation / Products.SiteErrorLog / 4063246703

pending completion
4063246703

push

github

GitHub
Drop support for Python 2.7, 3.5, 3.6 (#32)

69 of 99 branches covered (69.7%)

Branch coverage included in aggregate %.

8 of 8 new or added lines in 3 files covered. (100.0%)

457 of 496 relevant lines covered (92.14%)

0.92 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

78.21
/src/Products/SiteErrorLog/SiteErrorLog.py
1
##############################################################################
2
#
3
# Copyright (c) 2001, 2002 Zope Foundation and Contributors.
4
# All Rights Reserved.
5
#
6
# This software is subject to the provisions of the Zope Public License,
7
# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
8
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
9
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
10
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
11
# FOR A PARTICULAR PURPOSE.
12
#
13
##############################################################################
14
"""Site error log module.
1✔
15
"""
16

17
import logging
1✔
18
import os
1✔
19
import sys
1✔
20
import time
1✔
21
from _thread import allocate_lock
1✔
22
from random import random
1✔
23

24
from AccessControl.class_init import InitializeClass
1✔
25
from AccessControl.SecurityInfo import ClassSecurityInfo
1✔
26
from AccessControl.SecurityManagement import getSecurityManager
1✔
27
from AccessControl.unauthorized import Unauthorized
1✔
28
from Acquisition import aq_acquire
1✔
29
from Acquisition import aq_base
1✔
30
from App.Dialogs import MessageDialog
1✔
31
from OFS.SimpleItem import SimpleItem
1✔
32
from Products.PageTemplates.PageTemplateFile import PageTemplateFile
1✔
33
from transaction.interfaces import TransientError
1✔
34
from zExceptions.ExceptionFormatter import format_exception
1✔
35
from zope.component import adapter
1✔
36
from zope.event import notify
1✔
37
from ZPublisher.interfaces import IPubFailure
1✔
38

39
from .interfaces import ErrorRaisedEvent
1✔
40

41

42
LOG = logging.getLogger('Zope.SiteErrorLog')
1✔
43

44
# Permission names
45
use_error_logging = 'Log Site Errors'
1✔
46
log_to_event_log = 'Log to the Event Log'
1✔
47

48
# We want to restrict the rate at which errors are sent to the Event Log
49
# because we know that these errors can be generated quick enough to
50
# flood some zLOG backends. zLOG is used to notify someone of a problem,
51
# not to record every instance.
52
# This dictionary maps exception name to a value which encodes when we
53
# can next send the error with that name into the event log. This dictionary
54
# is shared between threads and instances. Concurrent access will not
55
# do much harm.
56
_rate_restrict_pool = {}
1✔
57

58
# The number of seconds that must elapse on average between sending two
59
# exceptions of the same name into the the Event Log. one per minute.
60
_rate_restrict_period = 60
1✔
61

62
# The number of exceptions to allow in a burst before the above limit
63
# kicks in. We allow five exceptions, before limiting them to one per
64
# minute.
65
_rate_restrict_burst = 5
1✔
66

67
_www = os.path.join(os.path.dirname(__file__), 'www')
1✔
68

69
# temp_logs holds the logs.
70
temp_logs = {}  # { oid -> [ traceback string ] }
1✔
71

72
cleanup_lock = allocate_lock()
1✔
73

74

75
class SiteErrorLog(SimpleItem):
1✔
76
    """Site error log class.  You can put an error log anywhere in the tree
77
    and exceptions in that area will be posted to the site error log.
78
    """
79
    meta_type = 'Site Error Log'
1✔
80
    id = 'error_log'
1✔
81
    zmi_icon = 'fas fa-bug'
1✔
82
    zmi_show_add_dialog = False
1✔
83

84
    keep_entries = 20
1✔
85
    copy_to_zlog = True
1✔
86

87
    security = ClassSecurityInfo()
1✔
88

89
    manage_options = (
1✔
90
        {'label': 'Log', 'action': 'manage_main'},
91
    ) + SimpleItem.manage_options
92

93
    security.declareProtected(use_error_logging, 'manage_main')  # NOQA: D001
1✔
94
    manage_main = PageTemplateFile('main.pt', _www)
1✔
95

96
    security.declareProtected(use_error_logging, 'showEntry')  # NOQA: D001
1✔
97
    showEntry = PageTemplateFile('showEntry.pt', _www)
1✔
98

99
    @security.private
1✔
100
    def manage_beforeDelete(self, item, container):
1✔
101
        if item is self:
1!
102
            try:
1✔
103
                del container.__error_log__
1✔
104
            except AttributeError:
×
105
                pass
×
106

107
    @security.private
1✔
108
    def manage_afterAdd(self, item, container):
1✔
109
        if item is self:
1!
110
            container.__error_log__ = aq_base(self)
1✔
111

112
    def _setId(self, id):
1✔
113
        if id != self.id:
×
114
            raise ValueError(MessageDialog(
×
115
                title='Invalid Id',
116
                message='Cannot change the id of a SiteErrorLog',
117
                action='./manage_main'))
118

119
    def _getLog(self):
1✔
120
        """Returns the log for this object.
121

122
        Careful, the log is shared between threads.
123
        """
124
        log = temp_logs.get(self._p_oid, None)
1✔
125
        if log is None:
1✔
126
            log = []
1✔
127
            temp_logs[self._p_oid] = log
1✔
128
        return log
1✔
129

130
    @security.protected(use_error_logging)
1✔
131
    def forgetEntry(self, id, REQUEST=None):
1✔
132
        """Removes an entry from the error log."""
133
        log = self._getLog()
1✔
134
        cleanup_lock.acquire()
1✔
135
        i = 0
1✔
136
        for entry in log:
1✔
137
            if entry['id'] == id:
1✔
138
                del log[i]
1✔
139
            i += 1
1✔
140
        cleanup_lock.release()
1✔
141
        if REQUEST is not None:
1!
142
            REQUEST.RESPONSE.redirect(
×
143
                '%s/manage_main?manage_tabs_message='
144
                'Error+log+entry+was+removed.' %
145
                self.absolute_url())
146

147
    # Exceptions that happen all the time, so we dont need
148
    # to log them. Eventually this should be configured
149
    # through-the-web.
150
    _ignored_exceptions = ('Unauthorized', 'NotFound', 'Redirect')
1✔
151

152
    @security.private
1✔
153
    def raising(self, info):
1✔
154
        """Log an exception.
155

156
        Called by SimpleItem's exception handler.
157
        Returns the url to view the error log entry
158
        """
159
        now = time.time()
1✔
160
        try:
1✔
161
            tb_text = None
1✔
162
            tb_html = None
1✔
163

164
            strtype = str(getattr(info[0], '__name__', info[0]))
1✔
165
            if strtype in self._ignored_exceptions:
1✔
166
                return
1✔
167

168
            if not isinstance(info[2], str):
1!
169
                tb_text = ''.join(
1✔
170
                    format_exception(*info, **{'as_html': 0}))
171
                tb_html = ''.join(
1✔
172
                    format_exception(*info, **{'as_html': 1}))
173
            else:
174
                tb_text = info[2]
×
175

176
            request = getattr(self, 'REQUEST', None)
1✔
177
            url = None
1✔
178
            username = None
1✔
179
            userid = None
1✔
180
            req_html = None
1✔
181
            try:
1✔
182
                strv = str(info[1])
1✔
183
            except Exception:
×
184
                strv = '<unprintable %s object>' % type(info[1]).__name__
×
185
            if request:
1!
186
                url = request.get('URL', '?')
1✔
187
                usr = getSecurityManager().getUser()
1✔
188
                username = usr.getUserName()
1✔
189
                userid = usr.getId()
1✔
190
                try:
1✔
191
                    req_html = str(request)
1✔
192
                except Exception:
×
193
                    pass
×
194
                if strtype == 'NotFound':
1✔
195
                    strv = url
1✔
196
                    next = request['TraversalRequestNameStack']
1✔
197
                    if next:
1!
198
                        next = list(next)
×
199
                        next.reverse()
×
200
                        strv = '{} [ /{} ]'.format(strv, '/'.join(next))
×
201

202
            log = self._getLog()
1✔
203
            entry_id = str(now) + str(random())  # Low chance of collision
1✔
204
            log.append({
1✔
205
                'type': strtype,
206
                'value': strv,
207
                'time': now,
208
                'id': entry_id,
209
                'tb_text': tb_text,
210
                'tb_html': tb_html,
211
                'username': username,
212
                'userid': userid,
213
                'url': url,
214
                'req_html': req_html})
215

216
            cleanup_lock.acquire()
1✔
217
            try:
1✔
218
                if len(log) >= self.keep_entries:
1!
219
                    del log[:-self.keep_entries]
×
220
            finally:
221
                cleanup_lock.release()
1✔
222
        except Exception:
×
223
            LOG.error('Error while logging', exc_info=sys.exc_info())
×
224
        else:
225
            notify(ErrorRaisedEvent(log[-1]))
1✔
226
            if self.copy_to_zlog:
1!
227
                self._do_copy_to_zlog(now, strtype, entry_id,
1✔
228
                                      str(url), tb_text)
229
            return f'{self.absolute_url()}/showEntry?id={entry_id}'
1✔
230

231
    def _do_copy_to_zlog(self, now, strtype, entry_id, url, tb_text):
1✔
232
        when = _rate_restrict_pool.get(strtype, 0)
1✔
233
        if now > when:
1!
234
            next_when = max(when,
1✔
235
                            now - _rate_restrict_burst * _rate_restrict_period)
236
            next_when += _rate_restrict_period
1✔
237
            _rate_restrict_pool[strtype] = next_when
1✔
238
            LOG.error(f'{entry_id} {url}\n{tb_text.rstrip()}')
1✔
239

240
    @security.protected(use_error_logging)
1✔
241
    def getProperties(self):
1✔
242
        return {
1✔
243
            'keep_entries': self.keep_entries,
244
            'copy_to_zlog': self.copy_to_zlog,
245
            'ignored_exceptions': self._ignored_exceptions,
246
        }
247

248
    @security.protected(log_to_event_log)
1✔
249
    def checkEventLogPermission(self):
1✔
250
        if not getSecurityManager().checkPermission(log_to_event_log, self):
1!
251
            raise Unauthorized('You do not have the "%s" permission.' %
×
252
                               log_to_event_log)
253
        return 1
1✔
254

255
    @security.protected(use_error_logging)
1✔
256
    def setProperties(self, keep_entries, copy_to_zlog=0,
1✔
257
                      ignored_exceptions=(), RESPONSE=None):
258
        """Sets the properties of this site error log.
259
        """
260
        copy_to_zlog = not not copy_to_zlog
1✔
261
        if copy_to_zlog and not self.copy_to_zlog:
1!
262
            # Before turning on event logging, check the permission.
263
            self.checkEventLogPermission()
×
264
        self.keep_entries = int(keep_entries)
1✔
265
        self.copy_to_zlog = copy_to_zlog
1✔
266
        # filter out empty lines
267
        # ensure we don't save exception objects but exceptions instead
268
        self._ignored_exceptions = tuple(
1✔
269
            [_f for _f in map(str, ignored_exceptions) if _f])
270
        if RESPONSE is not None:
1✔
271
            RESPONSE.redirect(
1✔
272
                '%s/manage_main?manage_tabs_message=Changed+properties.' %
273
                self.absolute_url())
274

275
    @security.protected(use_error_logging)
1✔
276
    def getLogEntries(self):
1✔
277
        """Returns the entries in the log, most recent first.
278

279
        Makes a copy to prevent changes.
280
        """
281
        # List incomprehension ;-)
282
        res = [entry.copy() for entry in self._getLog()]
1✔
283
        res.reverse()
1✔
284
        return res
1✔
285

286
    @security.protected(use_error_logging)
1✔
287
    def getLogEntryById(self, id):
1✔
288
        """Returns the specified log entry.
289

290
        Makes a copy to prevent changes.  Returns None if not found.
291
        """
292
        for entry in self._getLog():
×
293
            if entry['id'] == id:
×
294
                return entry.copy()
×
295
        return None
×
296

297
    @security.protected(use_error_logging)
1✔
298
    def getLogEntryAsText(self, id, RESPONSE=None):
1✔
299
        """Returns the specified log entry.
300

301
        Makes a copy to prevent changes.  Returns None if not found.
302
        """
303
        entry = self.getLogEntryById(id)
×
304
        if entry is None:
×
305
            return 'Log entry not found or expired'
×
306
        if RESPONSE is not None:
×
307
            RESPONSE.setHeader('Content-Type', 'text/plain')
×
308
        return entry['tb_text']
×
309

310

311
InitializeClass(SiteErrorLog)
1✔
312

313

314
def manage_addErrorLog(dispatcher, RESPONSE=None):
1✔
315
    """Add a site error log to a container."""
316
    log = SiteErrorLog()
1✔
317
    dispatcher._setObject(log.id, log)
1✔
318
    if RESPONSE is not None:
1!
319
        RESPONSE.redirect(
×
320
            dispatcher.DestinationURL()
321
            + '/manage_main?manage_tabs_message=Error+Log+Added.')
322

323

324
@adapter(IPubFailure)
1✔
325
def IPubFailureSubscriber(event):
1✔
326
    """ Handles an IPubFailure event triggered by the WSGI Publisher.
327
        This handler forwards the event to the SiteErrorLog object
328
        closest to the published object that the error occured with,
329
        it logs no error if no published object was found.
330
    """
331
    request = event.request
1✔
332
    published = request.get('PUBLISHED')
1✔
333

334
    if published is None:  # likely a traversal problem
1✔
335
        parents = request.get('PARENTS')
1✔
336
        if parents:
1!
337
            # partially emulate successful traversal
338
            published = request['PUBLISHED'] = parents.pop(0)
1✔
339
    if published is None:
1!
340
        return
×
341

342
    published = getattr(published, '__self__', published)  # method --> object
1✔
343

344
    # Filter out transient errors like ConflictErrors that can be
345
    # retried, just log a short message instead.
346
    if isinstance(event.exc_info[1], TransientError) and \
1✔
347
       request.supports_retry():
348
        LOG.info('{} at {}: {}. Request will be retried.'.format(
1✔
349
                 event.exc_info[0].__name__,
350
                 request.get('PATH_INFO') or '<unknown>',
351
                 str(event.exc_info[1])))
352
        return
1✔
353

354
    try:
1✔
355
        error_log = aq_acquire(published, '__error_log__', containment=1)
1✔
356
    except AttributeError:
×
357
        pass
×
358
    else:
359
        error_log.raising(event.exc_info)
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc