oops-0.0.10/ 0000755 0001750 0001750 00000000000 11660033151 012426 5 ustar robertc robertc oops-0.0.10/setup.py 0000755 0001750 0001750 00000003363 11660033120 014144 0 ustar robertc robertc #!/usr/bin/env python
#
# Copyright (c) 2011, Canonical Ltd
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, version 3 only.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program. If not, see .
# GNU Lesser General Public License version 3 (see the file LICENSE).
from distutils.core import setup
import os.path
description = file(
os.path.join(os.path.dirname(__file__), 'README'), 'rb').read()
setup(name="oops",
version="0.0.10",
description=\
"OOPS report model and default allocation/[de]serialization.",
long_description=description,
maintainer="Launchpad Developers",
maintainer_email="launchpad-dev@lists.launchpad.net",
url="https://launchpad.net/python-oops",
packages=['oops'],
package_dir = {'':'.'},
classifiers = [
'Development Status :: 2 - Pre-Alpha',
'Intended Audience :: Developers',
'License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL)',
'Operating System :: OS Independent',
'Programming Language :: Python',
],
install_requires = [
'iso8601',
'pytz',
],
extras_require = dict(
test=[
'fixtures',
'testtools',
]
),
)
oops-0.0.10/README 0000644 0001750 0001750 00000010626 11660033016 013313 0 ustar robertc robertc **************************
python-oops: Error reports
**************************
Copyright (c) 2011, Canonical Ltd
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, version 3 only.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with this program. If not, see .
GNU Lesser General Public License version 3 (see the file LICENSE).
The oops project provides an in-memory model and basic serialisation,
deserialisation and allocation of OOPS reports. An OOPS report is a report
about something going wrong in a piece of software... thus, an 'oops' :)
This core package is rarely used directly: most programs or services that want
to generate OOPS reports will do so via a framework specific adapter (e.g.
python-oops_wsgi).
Dependencies
============
* Python 2.6+
Testing Dependencies
====================
* subunit (http://pypi.python.org/pypi/python-subunit) (optional)
* testtools (http://pypi.python.org/pypi/testtools)
Usage
=====
In Python, OOPS reports are dicts with some well known keys, but extensible
simply by adding as many additional keys asneeded. The only constraint is that
the resulting dict must be bson serializable : this is the standard to which
new serializers are held. Some existing serializers cannot handle this degree
of extensability and will ignore additional keys, and/or raise an error on keys
they expect but which contain unexpected data.
Typical usage:
* When initializing your script/app/server, create a Config object::
>>> from oops import Config
>>> config = Config()
* New reports will be based on the template report::
>>> config.template
{}
* You can edit the template report (which like all reports is just a dict)::
>>> config.template['branch_nick'] = 'mybranch'
>>> config.template['appname'] = 'demo'
* You can supply a callback (for instance, to capture your process memory usage
when the oops is created, or to override / tweak the information gathered by an
earlier callback)::
>>> mycallback = lambda report, context: None
>>> config.on_create.append(mycallback)
The context parameter is also just dict, and is passed to all the on_create
callbacks similarly to the report. This is used to support passing information
to the on_create hooks. For instance, the exc_info key is used to pass in
information about the exception being logged (if one was caught).
* Later on, when something has gone wrong and you want to create an OOPS
report::
>>> report = config.create(context=dict(exc_info=sys.exc_info()))
>>> report
{'appname': 'demo', 'branch_nick': 'mybranch'}
* And then send it off for storage::
>>> config.publish(report)
[]
* Note that publish returns a list - each item in the list is the id allocated
by the particular repository that recieved the report. (Id allocation is up
to the repository). Publishers should try to use report['id'] for the id, if it
is set. This is automatically set to the id returned by the previous publisher.
If publish returns None, then the report was filtered and not passed to any
publisher (see the api docs for more information).
>>> 'id' in report
False
>>> def demo_publish(report):
... return 'id 1'
>>> config.publishers.append(demo_publish)
>>> config.publish(report)
['id 1']
>>> report['id']
'id 1'
* The related project oops_datedir_repo contains a local disk based repository which
can be used as a publisher.
More coming soon.
Installation
============
Either run setup.py in an environment with all the dependencies available, or
add the working directory to your PYTHONPATH.
Development
===========
Upstream development takes place at https://launchpad.net/python-oops.
To setup a working area for development, if the dependencies are not
immediately available, you can use ./bootstrap.py to create bin/buildout, then
bin/py to get a python interpreter with the dependencies available.
To run the tests use the runner of your choice, the test suite is
oops.tests.test_suite.
For instance::
$ bin/py -m testtools.run oops.tests.test_suite
oops-0.0.10/PKG-INFO 0000644 0001750 0001750 00000013770 11660033151 013533 0 ustar robertc robertc Metadata-Version: 1.0
Name: oops
Version: 0.0.10
Summary: OOPS report model and default allocation/[de]serialization.
Home-page: https://launchpad.net/python-oops
Author: Launchpad Developers
Author-email: launchpad-dev@lists.launchpad.net
License: UNKNOWN
Description: **************************
python-oops: Error reports
**************************
Copyright (c) 2011, Canonical Ltd
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, version 3 only.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with this program. If not, see .
GNU Lesser General Public License version 3 (see the file LICENSE).
The oops project provides an in-memory model and basic serialisation,
deserialisation and allocation of OOPS reports. An OOPS report is a report
about something going wrong in a piece of software... thus, an 'oops' :)
This core package is rarely used directly: most programs or services that want
to generate OOPS reports will do so via a framework specific adapter (e.g.
python-oops_wsgi).
Dependencies
============
* Python 2.6+
Testing Dependencies
====================
* subunit (http://pypi.python.org/pypi/python-subunit) (optional)
* testtools (http://pypi.python.org/pypi/testtools)
Usage
=====
In Python, OOPS reports are dicts with some well known keys, but extensible
simply by adding as many additional keys asneeded. The only constraint is that
the resulting dict must be bson serializable : this is the standard to which
new serializers are held. Some existing serializers cannot handle this degree
of extensability and will ignore additional keys, and/or raise an error on keys
they expect but which contain unexpected data.
Typical usage:
* When initializing your script/app/server, create a Config object::
>>> from oops import Config
>>> config = Config()
* New reports will be based on the template report::
>>> config.template
{}
* You can edit the template report (which like all reports is just a dict)::
>>> config.template['branch_nick'] = 'mybranch'
>>> config.template['appname'] = 'demo'
* You can supply a callback (for instance, to capture your process memory usage
when the oops is created, or to override / tweak the information gathered by an
earlier callback)::
>>> mycallback = lambda report, context: None
>>> config.on_create.append(mycallback)
The context parameter is also just dict, and is passed to all the on_create
callbacks similarly to the report. This is used to support passing information
to the on_create hooks. For instance, the exc_info key is used to pass in
information about the exception being logged (if one was caught).
* Later on, when something has gone wrong and you want to create an OOPS
report::
>>> report = config.create(context=dict(exc_info=sys.exc_info()))
>>> report
{'appname': 'demo', 'branch_nick': 'mybranch'}
* And then send it off for storage::
>>> config.publish(report)
[]
* Note that publish returns a list - each item in the list is the id allocated
by the particular repository that recieved the report. (Id allocation is up
to the repository). Publishers should try to use report['id'] for the id, if it
is set. This is automatically set to the id returned by the previous publisher.
If publish returns None, then the report was filtered and not passed to any
publisher (see the api docs for more information).
>>> 'id' in report
False
>>> def demo_publish(report):
... return 'id 1'
>>> config.publishers.append(demo_publish)
>>> config.publish(report)
['id 1']
>>> report['id']
'id 1'
* The related project oops_datedir_repo contains a local disk based repository which
can be used as a publisher.
More coming soon.
Installation
============
Either run setup.py in an environment with all the dependencies available, or
add the working directory to your PYTHONPATH.
Development
===========
Upstream development takes place at https://launchpad.net/python-oops.
To setup a working area for development, if the dependencies are not
immediately available, you can use ./bootstrap.py to create bin/buildout, then
bin/py to get a python interpreter with the dependencies available.
To run the tests use the runner of your choice, the test suite is
oops.tests.test_suite.
For instance::
$ bin/py -m testtools.run oops.tests.test_suite
Platform: UNKNOWN
Classifier: Development Status :: 2 - Pre-Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL)
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python
oops-0.0.10/oops/ 0000755 0001750 0001750 00000000000 11660033151 013406 5 ustar robertc robertc oops-0.0.10/oops/createhooks.py 0000644 0001750 0001750 00000007422 11660033016 016274 0 ustar robertc robertc # Copyright (c) 2010, 2011, Canonical Ltd
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, version 3 only.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program. If not, see .
# GNU Lesser General Public License version 3 (see the file LICENSE).
"""Various hooks that can be used to populate OOPS reports.
The default_hooks list contains some innocuous hooks which most reporters will
want.
"""
__all__ = [
'attach_exc_info',
'attach_date',
'attach_hostname',
'copy_reporter',
'copy_topic',
'copy_url',
'default_hooks',
'safe_unicode',
]
__metaclass__ = type
import datetime
import socket
import traceback
from pytz import utc
# Used to detect missing keys.
_sentinel = object()
# (Prep for python3 - should be set conditional on version
strtypes = (basestring,)
def _simple_copy(key):
"""Curry a simple hook that copies a key from context to report."""
def copy_key(report, context):
value = context.get(key, _sentinel)
if value is not _sentinel:
report[key] = value
copy_key.__doc__ = (
"Copy the %s field from context to report, if present." % key)
return copy_key
copy_reporter = _simple_copy('reporter')
copy_topic = _simple_copy('topic')
copy_url = _simple_copy('url')
def safe_unicode(obj):
"""Used to reliably get *a* string for an object.
This is called on objects like exceptions, where bson won't be able to
serialize it, but a representation is needed for the report. It is
exposed a convenience for other on_create hook authors.
"""
if isinstance(obj, unicode):
return obj
# A call to str(obj) could raise anything at all.
# We'll ignore these errors, and print something
# useful instead, but also log the error.
# We disable the pylint warning for the blank except.
try:
value = unicode(obj)
except:
value = u'' % (
unicode(type(obj).__name__))
# Some objects give back bytestrings to __unicode__...
if isinstance(value, str):
value = value.decode('latin-1')
return value
def attach_date(report, context):
"""Set the time key in report to a datetime of now."""
report['time'] = datetime.datetime.now(utc)
def attach_exc_info(report, context):
"""Attach exception info to the report.
This reads the 'exc_info' key from the context and sets the:
* type
* value
* tb_text
keys in the report.
exc_info must be a tuple, but it can contain either live exception
information or simple strings (allowing exceptions that have been
serialised and received over the network to be reported).
"""
info = context.get('exc_info')
if info is None:
return
report['type'] = getattr(info[0], '__name__', info[0])
report['value'] = safe_unicode(info[1])
if isinstance(info[2], strtypes):
tb_text = info[2]
else:
tb_text = u''.join(map(safe_unicode, traceback.format_tb(info[2])))
report['tb_text'] = tb_text
def attach_hostname(report, context):
"""Add the machine's hostname to report in the 'hostname' key."""
report['hostname'] = socket.gethostname()
# hooks that are installed into Config objects by default.
default_hooks = [
attach_exc_info,
attach_date,
copy_reporter,
copy_topic,
copy_url,
attach_hostname,
]
oops-0.0.10/oops/publishers.py 0000644 0001750 0001750 00000002440 11660033016 016140 0 ustar robertc robertc # Copyright (c) 2011, Canonical Ltd
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, version 3 only.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program. If not, see .
# GNU Lesser General Public License version 3 (see the file LICENSE).
"""Generic publisher support and utility code."""
__metaclass__ = type
__all__ = [
'publish_new_only',
]
def publish_new_only(publisher):
"""Wraps a publisher with a check that the report has not had an id set.
This permits having fallback publishers that only publish if the earlier
one failed.
For instance:
>>> config.publishers.append(amqp_publisher)
>>> config.publishers.append(publish_new_only(datedir_repo.publish))
"""
def result(report):
if report.get('id'):
return None
return publisher(report)
return result
oops-0.0.10/oops/config.py 0000644 0001750 0001750 00000017644 11660033016 015241 0 ustar robertc robertc #
# Copyright (c) 2010, 2011, Canonical Ltd
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, version 3 only.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program. If not, see .
# GNU Lesser General Public License version 3 (see the file LICENSE).
"""The primary interface for clients creating OOPS reports.
Typical usage:
* Configure the library::
>>> from oops import Config
>>> config = Config()
>>> def demo_publish(report):
... return 'id 1'
>>> config.publishers.append(demo_publish)
This allows aggregation of oops reports from different programs into one
oops-tools install.
>>> config.template['reporter'] = 'myprogram'
* Create a report::
>>> report = config.create()
* And then send it off for storage::
>>> config.publish(report)
['id 1']
>>> report
{'id': 'id 1', 'template': 'myprogram'}
* See the Config object pydoc for more information.
The OOPS report is a dictionary, and must be bson serializable. This permits
the inclusion of binary data in the report, and provides cross-language
compatibility.
A minimal report can be empty, but this is fairly useless and may even be
rejected by some repositories.
Some well known keys used by Launchpad in its OOPS reports::
* id: The name of this error report.
* type: The type of the exception that occurred.
* value: The value of the exception that occurred.
* time: The time at which the exception occurred.
* hostname: The hostname of the machine the oops was created on. (Set by default)
* branch_nick: The branch nickname.
* revno: The revision number of the branch.
* tb_text: A text version of the traceback.
* username: The user associated with the request.
* url: The URL for the failed request.
* req_vars: The request variables. This should be a dict of simple string ->
string mappings. The strings and their values should be either unicode or
url escaped ascii bytestrings. Older versions of the oops toolchain emitted
this variable as a list of two-tuples e.g. (key, value). Code expecting to
receive or process old reports should accept both dicts and tuples. Modern
or new code can just expect a dict.
* branch_nick: A name for the branch of code that was running when the report
was triggered.
* revno: The revision that the branch was at.
* reporter: Describes the program creating the report. For instance you might
put the name of the program, or its website - as long as its distinct from
other reporters being sent to a single analysis server. For dynamically
scaled services with multiple instances, the reporter will usually be the
same for a single set of identical instances. e.g. all the instances in one
Amazon EC2 availability zone might be given the same reporter. Differentiated
backend services for the same front end site would usually get different
reporters as well. (e.g. auth, cache, render, ...)
* topic: The subject or context for the report. With a command line tool you
might put the subcommand here, with a web site you might put the template (as
opposed to the url). This is used as a weak correlation hint: reports from the
same topic are more likely to have the same cause than reports from different
topics.
* timeline: A sequence of (start, stop, category, detail) tuples describing the
events leading up to the OOPS. One way to populate this is the oops-timeline
package. Consumers should not assume the length of the tuple to be fixed -
additional fields may be added in future to the right hand side (e.g.
backtraces).
"""
__all__ = [
'Config',
]
__metaclass__ = type
from copy import deepcopy
from createhooks import default_hooks
class Config:
"""The configuration for the OOPS system.
:ivar on_create: A list of callables to call when making a new report. Each
will be called in series with the new report and a creation context
dict. The return value of the callbacks is ignored.
:ivar filters: A list of callables to call when filtering a report. Each
will be called in series with a report that is about to be published.
If the filter returns true (that is not None, 0, '' or False), then
the report will not be published, and the call to publish will return
None to the user.
:ivar publishers: A list of callables to call when publishing a report.
Each will be called in series with the report to publish. Their return
value will be assigned to the reports 'id' key : if a publisher
allocates a different id than a prior publisher, only the last
publisher in the list will have its id present in the report at the
end. See the publish() method for more information.
"""
def __init__(self):
self.filters = []
self.on_create = list(default_hooks)
self.template = {}
self.publishers = []
def create(self, context=None):
"""Create an OOPS.
The current template is copied to make the new report, and the new
report is then passed to all the on_create callbacks for population.
If a callback raises an exception, that will propgate to the caller.
:param context: A dict of information that the on_create callbacks can
use in populating the report. For instance, the attach_exception
callback looks for an exc_info key in the context and uses that
to add information to the report. If context is None, an empty dict
is created and used with the callbacks.
:return: A fresh OOPS.
"""
if context is None:
context = {}
result = deepcopy(self.template)
[callback(result, context) for callback in self.on_create]
return result
def publish(self, report):
"""Publish a report.
Each publisher is passed the report to publish. Publishers should
return the id they allocated-or-used for the report, which gets
automatically put into the report for them. All of the ids are also
returned to the caller, allowing them to handle the case when multiple
publishers allocated different ids. The id from the last publisher is
left in the report's id key. This is done for three reasons:
* As a convenience for the common case when only one publisher is
present.
* This allows the next publisher in the chain to reuse that id if it
wishes.
* Showing users that a report was made only needs one id and this means
other code doesn't need to arbitrarily pick one - it is done by
publish().
If a publisher raises an exception, that will propagate to the caller.
If a publisher returns anything non-True (that is None, 0, False, ''),
it indicates that the publisher did not publish the report and no change
will be made to the report's 'id' key for that publisher.
This permits fallback chains. For instance the first publisher may
choose not to publish a report (e.g. the place it would publish to is
offline) and the second publisher can detect that by checking if there
is an id key.
:return: A list of the allocated ids.
"""
for report_filter in self.filters:
if report_filter(report):
return None
result = []
for publisher in self.publishers:
id = publisher(report)
if id:
report['id'] = id
result.append(id)
else:
result.append(None)
return result
oops-0.0.10/oops/__init__.py 0000644 0001750 0001750 00000003077 11660033126 015530 0 ustar robertc robertc #
# Copyright (c) 2011, Canonical Ltd
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, version 3 only.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program. If not, see .
# GNU Lesser General Public License version 3 (see the file LICENSE).
# same format as sys.version_info: "A tuple containing the five components of
# the version number: major, minor, micro, releaselevel, and serial. All
# values except releaselevel are integers; the release level is 'alpha',
# 'beta', 'candidate', or 'final'. The version_info value corresponding to the
# Python version 2.0 is (2, 0, 0, 'final', 0)." Additionally we use a
# releaselevel of 'dev' for unreleased under-development code.
#
# If the releaselevel is 'alpha' then the major/minor/micro components are not
# established at this point, and setup.py will use a version of next-$(revno).
# If the releaselevel is 'final', then the tarball will be major.minor.micro.
# Otherwise it is major.minor.micro~$(revno).
__version__ = (0, 0, 10, 'beta', 0)
__all__ = [
'Config',
'publish_new_only',
]
from oops.config import Config
from oops.publishers import publish_new_only