launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #14426
[Merge] lp:~stevenk/launchpad/destroy-librarianformatter into lp:launchpad
Steve Kowalik has proposed merging lp:~stevenk/launchpad/destroy-librarianformatter into lp:launchpad.
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
Related bugs:
Bug #641103 in Launchpad itself: "LibrarianFormatter should die"
https://bugs.launchpad.net/launchpad/+bug/641103
Bug #665307 in Launchpad itself: "cronscripts/expire-bugtasks.py fails trying to put backtrace in librarian"
https://bugs.launchpad.net/launchpad/+bug/665307
For more details, see:
https://code.launchpad.net/~stevenk/launchpad/destroy-librarianformatter/+merge/135311
Kill LibrarianFormatter and its two doctests entirely.
The new plan is that OopsHandler will log the OOPS created (at level INFO) after it has done so.
With a script raising an uncaught Exception:
2012-11-21 03:36:40 ERROR Unhandled exception
2012-11-21 03:36:40 INFO OOPS-432e7c3e29181f82f8352b061429d202
And with a script logging something at ERROR:
2012-11-21 03:38:22 ERROR Causing OOPS
2012-11-21 03:38:22 INFO OOPS-ba8ec0f55e9b71f94ce43b65eee2d6c5
I have removed the catch-all exception from cronscripts/expire-bugtasks.py since now OopsHandler will now deal with it. I have also checked the other scripts in cronscripts/*, and none of them use a catch-all exception.
--
https://code.launchpad.net/~stevenk/launchpad/destroy-librarianformatter/+merge/135311
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~stevenk/launchpad/destroy-librarianformatter into lp:launchpad.
=== modified file 'cronscripts/expire-bugtasks.py'
--- cronscripts/expire-bugtasks.py 2012-01-01 03:14:54 +0000
+++ cronscripts/expire-bugtasks.py 2012-11-21 04:28:21 +0000
@@ -1,10 +1,8 @@
#!/usr/bin/python -S
#
-# Copyright 2009 Canonical Ltd. This software is licensed under the
+# Copyright 2009-2012 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
-# pylint: disable-msg=C0103,W0403
-
"""Expire all old, Incomplete bugs tasks that are unassigned in Malone.
Only bug task for project that use Malone may be expired. The expiration
@@ -47,16 +45,9 @@
# Avoid circular import.
from lp.registry.interfaces.distribution import IDistributionSet
target = getUtility(IDistributionSet).getByName('ubuntu')
- try:
- janitor = BugJanitor(
- log=self.logger, target=target, limit=self.options.limit)
- janitor.expireBugTasks(self.txn)
- except Exception:
- # We use a catchall here because we don't know (and don't care)
- # about the particular error--we'll just log it to as an Oops.
- self.logger.error(
- 'An error occured trying to expire bugtasks.', exc_info=1)
- raise
+ janitor = BugJanitor(
+ log=self.logger, target=target, limit=self.options.limit)
+ janitor.expireBugTasks(self.txn)
if __name__ == '__main__':
=== modified file 'lib/lp/services/scripts/base.py'
--- lib/lp/services/scripts/base.py 2012-06-29 08:40:05 +0000
+++ lib/lp/services/scripts/base.py 2012-11-21 04:28:21 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2011 Canonical Ltd. This software is licensed under the
+# Copyright 2009-2012 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
__metaclass__ = type
@@ -384,8 +384,7 @@
"""
self.lock_or_die(blocking=blocking)
try:
- self.run(
- use_web_security=use_web_security, isolation=isolation)
+ self.run(use_web_security=use_web_security, isolation=isolation)
finally:
self.unlock(skip_delete=skip_delete)
@@ -413,7 +412,8 @@
# self.name is used instead of the name argument, since it may have
# have been overridden by command-line parameters or by
# overriding the name property.
- logging.getLogger().addHandler(OopsHandler(self.name))
+ oops_hdlr = OopsHandler(self.name, logger=self.logger)
+ logging.getLogger().addHandler(oops_hdlr)
def get_last_activity(self):
"""Return the last activity, if any."""
=== modified file 'lib/lp/services/scripts/logger.py'
--- lib/lp/services/scripts/logger.py 2012-10-12 11:49:10 +0000
+++ lib/lp/services/scripts/logger.py 2012-11-21 04:28:21 +0000
@@ -1,8 +1,6 @@
-# Copyright 2009-2011 Canonical Ltd. This software is licensed under the
+# Copyright 2009-2012 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
-# pylint: disable-msg=W0702
-
"""Logging setup for scripts.
Don't import from this module. Import it from lp.services.scripts.
@@ -10,7 +8,7 @@
__metaclass__ = type
-# Don't import stuff from this module. Import it from canonical.scripts
+# Don't import stuff from this module. Import it from lp.services.scripts
__all__ = [
'DEBUG2',
'DEBUG3',
@@ -30,9 +28,6 @@
from contextlib import contextmanager
-from cStringIO import StringIO
-from datetime import timedelta
-import hashlib
import logging
from logging.handlers import WatchedFileHandler
from optparse import OptionParser
@@ -42,19 +37,10 @@
import time
from traceback import format_exception_only
-from zope.component import getUtility
from zope.exceptions.log import Formatter
from lp.services.config import config
-from lp.services.librarian.interfaces.client import (
- ILibrarianClient,
- UploadFailed,
- )
from lp.services.log import loglevels
-from lp.services.utils import (
- compress_hash,
- utc_now,
- )
from lp.services.webapp.errorlog import (
globalErrorUtility,
ScriptRequest,
@@ -75,12 +61,13 @@
class OopsHandler(logging.Handler):
"""Handler to log to the OOPS system."""
- def __init__(self, script_name, level=logging.WARN):
+ def __init__(self, script_name, level=logging.WARN, logger=None):
logging.Handler.__init__(self, level)
# Context for OOPS reports.
self.request = ScriptRequest(
[('script_name', script_name), ('path', sys.argv[0])])
self.setFormatter(LaunchpadFormatter())
+ self.logger = logger
def emit(self, record):
"""Emit a record as an OOPS."""
@@ -91,6 +78,8 @@
msg = record.getMessage()
with globalErrorUtility.oopsMessage(msg):
globalErrorUtility.raising(info, self.request)
+ if self.logger:
+ self.logger.info(self.request.oopsid)
except Exception:
self.handleError(record)
@@ -109,58 +98,9 @@
# Output should be UTC.
self.converter = time.gmtime
-
-class LibrarianFormatter(LaunchpadFormatter):
- """A logging.Formatter that stores tracebacks in the Librarian and emits
- a URL rather than emitting the traceback directly.
-
- The traceback will be emitted as a fallback if the Librarian cannot be
- contacted.
-
- XXX bug=641103 StuartBishop -- This class should die. Remove it and
- replace with LaunchpadFormatter, fixing the test fallout.
- """
-
def formatException(self, ei):
- """Format the exception and store it in the Librian.
-
- Returns the URL, or the formatted exception if the Librarian is
- not available.
- """
- traceback = LaunchpadFormatter.formatException(self, ei)
- # Uncomment this line to stop exception storage in the librarian.
- # Useful for debugging tests.
- # return traceback
- try:
- librarian = getUtility(ILibrarianClient)
- except LookupError:
- return traceback
-
- exception_string = ''
- try:
- exception_string = str(ei[1]).encode('ascii')
- except:
- pass
- if not exception_string:
- exception_string = ei[0].__name__
-
- expiry = utc_now() + timedelta(days=90)
- try:
- filename = compress_hash(hashlib.sha1(traceback)) + '.txt'
- url = librarian.remoteAddFile(
- filename, len(traceback), StringIO(traceback),
- 'text/plain;charset=%s' % sys.getdefaultencoding(),
- expires=expiry)
- return ' -> %s (%s)' % (url, exception_string)
- except UploadFailed:
- return traceback
- except Exception:
- # Exceptions raised by the Formatter get swallowed, but we want
- # to know about them. Since we are already spitting out exception
- # information, we can stuff our own problems in there too.
- return '%s\n\nException raised in formatter:\n%s\n' % (
- traceback,
- LaunchpadFormatter.formatException(self, sys.exc_info()))
+ # We don't want the traceback, so do nothing.
+ pass
class LogLevelNudger:
@@ -393,9 +333,8 @@
root_logger.removeHandler(hdlr)
-def _logger(
- level, out_stream, name=None,
- log_file=None, log_file_level=logging.DEBUG, milliseconds=False):
+def _logger(level, out_stream, name=None, log_file=None,
+ log_file_level=logging.DEBUG, milliseconds=False):
"""Create the actual logger instance, logging at the given level
if name is None, it will get args[0] without the extension (e.g. gina).
@@ -423,12 +362,7 @@
# logs.
root_logger.setLevel(0)
hdlr.setLevel(level)
- if milliseconds:
- # Python default datefmt includes milliseconds.
- formatter = LibrarianFormatter(datefmt=None)
- else:
- # Launchpad default datefmt does not include milliseconds.
- formatter = LibrarianFormatter()
+ formatter = LaunchpadFormatter()
hdlr.setFormatter(formatter)
root_logger.addHandler(hdlr)
=== removed file 'lib/lp/services/scripts/tests/librarianformatter.txt'
--- lib/lp/services/scripts/tests/librarianformatter.txt 2011-12-28 17:03:06 +0000
+++ lib/lp/services/scripts/tests/librarianformatter.txt 1970-01-01 00:00:00 +0000
@@ -1,142 +0,0 @@
-
-The LibrarianFormatter works just like a normal logger.Formatter if there
-is no Librarian available or active
-
->>> from lp.services.scripts.logger import LibrarianFormatter
-
-This is a helper to quickly construct a Logger instance with a particular
-formatter. Each call generates a unique Logger.
-
->>> _count = 0
->>> def make_logger(formatter_class):
-... global _count
-... _count += 1
-... logger = logging.getLogger('log%d' % _count)
-... output = StringIO()
-... handler = logging.StreamHandler(output)
-... # Note - no timestamp so we can make valid comparisons!
-... formatter = formatter_class(fmt='%(levelname)s %(message)s')
-... handler.setFormatter(formatter)
-... logger.addHandler(handler)
-... logger.setLevel(logging.DEBUG)
-... logger.propagate = False
-... return logger, output
-
-Create an exception we can reuse
-
->>> try:
-... raise RuntimeError('An Exception')
-... except RuntimeError:
-... exception = sys.exc_info()
-
-Because no Librarian is running, output from the two loggers should be
-identical. Note the overhead involved in attempting to contact the
-Librarian is not significant.
-
->>> from lp.testing.layers import LibrarianLayer
->>> LibrarianLayer.hide()
-
->>> from lp.services.utils import utc_now
-
->>> normal_log, normal_out= make_logger(logging.Formatter)
->>> librarian_log, librarian_out = make_logger(LibrarianFormatter)
->>> import time
->>> start_time = time.time()
->>> for log in (normal_log, librarian_log):
-... log.error('Blah!', exc_info=exception)
->>> end_time = time.time()
->>> (end_time - start_time) < 5.0
-True
-
->>> print normal_out.getvalue()
-ERROR Blah!
-Traceback (most recent call last):
-...
-RuntimeError: An Exception
-<BLANKLINE>
-
->>> print librarian_out.getvalue()
-ERROR Blah!
-Traceback (most recent call last):
-...
-RuntimeError: An Exception
-<BLANKLINE>
-
->>> normal_out.getvalue() == librarian_out.getvalue()
-True
-
-Now if we fire up the Librarian, the LibrarianFormatter should start
-storing tracebacks there instead, reducing the verbosity of spam from
-cronjobs
-
->>> LibrarianLayer.reveal()
->>> librarian_log, librarian_out = make_logger(LibrarianFormatter)
->>> librarian_log.error('Oops', exc_info=exception)
->>> print librarian_out.getvalue()
-ERROR Oops
- -> http://.../....txt (An Exception)
->>> url = librarian_out.getvalue().splitlines()[-1].split()[1:2][0]
->>> print url
-http://.../....txt
->>> print urlopen(url).read()
-Traceback (most recent call last):
- ...
-RuntimeError: An Exception
-
-As the librarian commits to the test database in a subprocess, we need
-to force the DatabaseLayer to fully tear down and restore the database
-after this test.
-
->>> from lp.testing.layers import DatabaseLayer
->>> DatabaseLayer.force_dirty_database()
-
-We keep exceptions from expiring for about 3 months.
-
->>> match = re.search('/(\d+)/', url)
->>> alias_id = match.group(1)
->>> from lp.services.librarian.model import LibraryFileAlias
->>> transaction.abort() # To see db changes made by the librarian
->>> alias = LibraryFileAlias.get(alias_id)
->>> alias.expires > utc_now() + timedelta(days=89)
-True
-
-Note that we also need to remain informative with dud exceptions, such as
-those with a non-ASCII string representation or no string representation
-at all
-
->>> librarian_log, librarian_out = make_logger(LibrarianFormatter)
->>> class Dud(Exception):
-... pass
->>> try:
-... raise Dud()
-... except Dud:
-... librarian_log.exception('Dud1')
->>> try:
-... raise Dud(u'\N{BIOHAZARD SIGN}'.encode('utf8'))
-... except Dud:
-... librarian_log.exception('Dud2')
->>> print librarian_out.getvalue()
-ERROR Dud1
- -> http://.../....txt (Dud)
-ERROR Dud2
- -> http://.../....txt (Dud)
-<BLANKLINE>
-
-The end result of this is to not have scripts display exceptions to
-stderr, instead reporting URLs and greatly reducing the verbosity.
-
->>> script = os.path.join(this_directory, 'raiseexception.py')
->>> import subprocess
->>> p = subprocess.Popen([sys.executable, script], stdin=subprocess.PIPE,
-... stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
->>> (out, err) = p.communicate()
->>> p.returncode
-0
->>> print out
-Script Output
-ERROR Oops
- -> http://.../....txt (Aargh)
-ERROR Root oops
- -> http://.../....txt (Aargh)
-
-
=== removed file 'lib/lp/services/scripts/tests/librarianformatter_noca.txt'
--- lib/lp/services/scripts/tests/librarianformatter_noca.txt 2011-12-21 19:44:48 +0000
+++ lib/lp/services/scripts/tests/librarianformatter_noca.txt 1970-01-01 00:00:00 +0000
@@ -1,66 +0,0 @@
->>> import logging
->>> import sys
->>> from cStringIO import StringIO
-
-The LibrarianFormatter works just like a normal logger.Formatter if there
-is no Librarian available or active
-
->>> from lp.services.scripts.logger import LibrarianFormatter
-
-This is a helper to quickly construct a Logger instance with a particular
-formatter. Each call generates a unique Logger.
-
->>> _count = 0
->>> def make_logger(formatter_class):
-... global _count
-... _count += 1
-... logger = logging.getLogger('log%d' % _count)
-... output = StringIO()
-... handler = logging.StreamHandler(output)
-... # Note - no timestamp so we can make valid comparisons!
-... formatter = formatter_class(fmt='%(levelname)s %(message)s')
-... handler.setFormatter(formatter)
-... logger.addHandler(handler)
-... logger.setLevel(logging.DEBUG)
-... return logger, output
-
-Now setup two identical Logger instances except for their formatter
-
->>> normal_log, normal_out = make_logger(logging.Formatter)
->>> librarian_log, librarian_out = make_logger(LibrarianFormatter)
-
-Output from the standard formatter should match the output from
-LibrianFormatter at this stage, as we have no component architecture
-loaded and no Librarian running.
-
-First, create an exception we can reuse
-
->>> try:
-... raise RuntimeError('An Exception')
-... except RuntimeError:
-... exception = sys.exc_info()
-... for log in (normal_log, librarian_log):
-... log.info('Some crap')
-... log.error('Oops!', exc_info=exception)
-... log.error('Error %d occurred', 42)
->>> normal_out.getvalue() == librarian_out.getvalue()
-True
->>> print normal_out.getvalue()
-INFO Some crap
-ERROR Oops!
-Traceback (most recent call last):
- File "<doctest librarianformatter_noca.txt[...]>", line 2, in <module>
- raise RuntimeError('An Exception')
-RuntimeError: An Exception
-ERROR Error 42 occurred
-<BLANKLINE>
->>> print librarian_out.getvalue()
-INFO Some crap
-ERROR Oops!
-Traceback (most recent call last):
- File "<doctest librarianformatter_noca.txt[...]>", line 2, in <module>
- raise RuntimeError('An Exception')
-RuntimeError: An Exception
-ERROR Error 42 occurred
-<BLANKLINE>
-
=== removed file 'lib/lp/services/scripts/tests/raiseexception.py'
--- lib/lp/services/scripts/tests/raiseexception.py 2011-12-21 20:23:01 +0000
+++ lib/lp/services/scripts/tests/raiseexception.py 1970-01-01 00:00:00 +0000
@@ -1,35 +0,0 @@
-# Copyright 2009 Canonical Ltd. This software is licensed under the
-# GNU Affero General Public License version 3 (see the file LICENSE).
-
-"""This script is called from librarianformatter.txt to
-demonstrate a script using the LibrarianFormatter
-"""
-
-__metaclass__ = type
-
-import logging
-from optparse import OptionParser
-import sys
-
-from lp.services.scripts import (
- execute_zcml_for_scripts,
- logger,
- logger_options,
- )
-
-
-if __name__ == '__main__':
- parser = OptionParser()
- logger_options(parser)
- (options, args) = parser.parse_args()
- log = logger(options)
- # Test the root logger too, because some code is using it
- root_log = logging.getLogger()
- execute_zcml_for_scripts()
- print >> sys.stderr, 'Script Output'
- try:
- raise RuntimeError('Aargh')
- except RuntimeError:
- log.exception('Oops')
- root_log.exception('Root oops')
-
=== removed file 'lib/lp/services/scripts/tests/test_librarianformatter.py'
--- lib/lp/services/scripts/tests/test_librarianformatter.py 2011-12-28 17:03:06 +0000
+++ lib/lp/services/scripts/tests/test_librarianformatter.py 1970-01-01 00:00:00 +0000
@@ -1,45 +0,0 @@
-# Copyright 2009 Canonical Ltd. This software is licensed under the
-# GNU Affero General Public License version 3 (see the file LICENSE).
-
-"""Module docstring goes here."""
-
-__metaclass__ = type
-
-from datetime import (
- datetime,
- timedelta,
- )
-import logging
-import os.path
-import re
-from StringIO import StringIO
-import sys
-import time
-from urllib2 import urlopen
-
-from pytz import utc
-import transaction
-
-from lp.testing import (
- ANONYMOUS,
- login,
- logout,
- )
-from lp.testing.layers import LaunchpadFunctionalLayer
-from lp.testing.systemdocs import LayeredDocFileSuite
-
-
-this_directory = os.path.dirname(__file__)
-
-def setUp(test):
- # Suck this modules environment into the test environment
- test.globs.update(globals())
- login(ANONYMOUS)
-
-def tearDown(test):
- logout()
-
-def test_suite():
- return LayeredDocFileSuite(
- 'librarianformatter.txt', setUp=setUp, tearDown=tearDown,
- layer=LaunchpadFunctionalLayer)
=== removed file 'lib/lp/services/scripts/tests/test_librarianformatter_noca.py'
--- lib/lp/services/scripts/tests/test_librarianformatter_noca.py 2011-12-28 17:03:06 +0000
+++ lib/lp/services/scripts/tests/test_librarianformatter_noca.py 1970-01-01 00:00:00 +0000
@@ -1,22 +0,0 @@
-# Copyright 2009 Canonical Ltd. This software is licensed under the
-# GNU Affero General Public License version 3 (see the file LICENSE).
-
-"""Python harness for librarianformatter_noca.txt."""
-
-__metaclass__ = type
-
-from lp.testing import reset_logging
-from lp.testing.systemdocs import LayeredDocFileSuite
-
-
-def setUp(test):
- # Suck this modules environment into the test environment
- reset_logging()
-
-def tearDown(test):
- reset_logging()
-
-def test_suite():
- return LayeredDocFileSuite(
- 'librarianformatter_noca.txt',
- setUp=setUp, tearDown=tearDown, stdout_logging=False)
Follow ups