├── tests
├── utils
│ ├── __init__.py
│ ├── json
│ │ ├── __init__.py
│ │ └── tests.py
│ ├── wsgi
│ │ ├── __init__.py
│ │ └── tests.py
│ ├── encoding
│ │ └── __init__.py
│ ├── stacks
│ │ ├── __init__.py
│ │ └── tests.py
│ ├── compat.py
│ ├── tests.py
│ └── lru_tests.py
├── client
│ └── __init__.py
├── config
│ ├── __init__.py
│ └── tests.py
├── contrib
│ ├── __init__.py
│ ├── async
│ │ ├── tests.py
│ │ └── __init__.py
│ ├── celery
│ │ ├── __init__.py
│ │ └── tests.py
│ ├── django
│ │ ├── __init__.py
│ │ ├── testapp
│ │ │ ├── templates
│ │ │ │ ├── 404.html
│ │ │ │ ├── error.html
│ │ │ │ ├── jinja2
│ │ │ │ │ └── jinja2_template.html
│ │ │ │ └── list_users.html
│ │ │ ├── __init__.py
│ │ │ ├── models.py
│ │ │ ├── celery.py
│ │ │ ├── middleware.py
│ │ │ ├── urls.py
│ │ │ └── views.py
│ │ ├── fake2
│ │ │ └── __init__.py
│ │ └── fake1
│ │ │ └── __init__.py
│ ├── flask
│ │ ├── __init__.py
│ │ └── templates
│ │ │ └── users.html
│ ├── pylons
│ │ ├── __init__.py
│ │ └── tests.py
│ ├── twisted
│ │ ├── __init__.py
│ │ └── tests.py
│ └── zerorpc
│ │ ├── __init__.py
│ │ └── zeropc_tests.py
├── events
│ ├── __init__.py
│ └── tests.py
├── handlers
│ ├── __init__.py
│ ├── logbook
│ │ ├── __init__.py
│ │ └── logbook_tests.py
│ └── logging
│ │ └── __init__.py
├── middleware
│ ├── __init__.py
│ └── tests.py
├── processors
│ └── __init__.py
├── transports
│ ├── __init__.py
│ ├── test_http.py
│ └── test_urllib3.py
├── instrumentation
│ ├── __init__.py
│ ├── django_tests
│ │ └── __init__.py
│ ├── jinja2_tests
│ │ ├── __init__.py
│ │ ├── mytemplate.html
│ │ └── jinja2_tests.py
│ ├── base_tests.py
│ ├── botocore_tests.py
│ ├── dbapi2_tests.py
│ ├── python_memcached_tests.py
│ ├── sqlite_tests.py
│ ├── urllib3_tests.py
│ ├── requests_tests.py
│ └── mysql_tests.py
├── __init__.py
├── helpers.py
└── asyncio
│ ├── test_asyncio_http.py
│ └── test_asyncio_client.py
├── opbeat
├── contrib
│ ├── __init__.py
│ ├── django
│ │ ├── management
│ │ │ ├── __init__.py
│ │ │ └── commands
│ │ │ │ └── __init__.py
│ │ ├── apps.py
│ │ ├── __init__.py
│ │ ├── celery
│ │ │ ├── __init__.py
│ │ │ └── models.py
│ │ ├── middleware
│ │ │ └── wsgi.py
│ │ ├── handlers.py
│ │ └── utils.py
│ ├── asyncio
│ │ ├── __init__.py
│ │ └── client.py
│ ├── paste.py
│ ├── flask
│ │ └── utils.py
│ ├── rq
│ │ └── __init__.py
│ ├── twisted
│ │ └── __init__.py
│ ├── pylons
│ │ └── __init__.py
│ ├── celery
│ │ └── __init__.py
│ └── zerorpc
│ │ └── __init__.py
├── instrumentation
│ ├── __init__.py
│ ├── packages
│ │ ├── __init__.py
│ │ ├── django
│ │ │ ├── __init__.py
│ │ │ └── template.py
│ │ ├── jinja2.py
│ │ ├── zlib.py
│ │ ├── mysql.py
│ │ ├── botocore.py
│ │ ├── urllib3.py
│ │ ├── pylibmc.py
│ │ ├── redis.py
│ │ ├── sqlite.py
│ │ ├── requests.py
│ │ ├── python_memcached.py
│ │ ├── psycopg2.py
│ │ └── pymongo.py
│ ├── control.py
│ └── register.py
├── version.py
├── transport
│ ├── __init__.py
│ ├── exceptions.py
│ ├── base.py
│ ├── asyncio.py
│ ├── http_urllib3.py
│ └── http.py
├── handlers
│ ├── __init__.py
│ ├── logbook.py
│ └── logging.py
├── __init__.py
├── utils
│ ├── wrapt
│ │ ├── __init__.py
│ │ ├── LICENSE
│ │ └── arguments.py
│ ├── module_import.py
│ ├── compat.py
│ ├── deprecation.py
│ ├── opbeat_json.py
│ ├── __init__.py
│ ├── lru.py
│ └── wsgi.py
├── conf
│ ├── __init__.py
│ └── defaults.py
├── middleware.py
├── processors.py
└── events.py
├── test_requirements
├── requirements-cpython.txt
├── requirements-pypy.txt
├── requirements-python-3.txt
├── requirements-python-2.txt
├── requirements-asyncio.txt
├── requirements-flask-0.10.txt
├── requirements-flask-0.11.txt
├── requirements-flask-0.12.txt
├── requirements-zerorpc.txt
├── requirements-django-2.0.txt
├── requirements-django-1.10.txt
├── requirements-django-1.11.txt
├── requirements-django-1.4.txt
├── requirements-django-1.5.txt
├── requirements-django-1.6.txt
├── requirements-django-1.7.txt
├── requirements-django-1.8.txt
├── requirements-django-1.9.txt
├── requirements-django-master.txt
└── requirements-base.txt
├── AUTHORS
├── MANIFEST.in
├── docs
├── config
│ ├── index.rst
│ ├── wsgi.rst
│ ├── logbook.rst
│ ├── zerorpc.rst
│ ├── pylons.rst
│ ├── pyramid.rst
│ ├── flask.rst
│ ├── logging.rst
│ └── other.rst
├── install
│ └── index.rst
├── index.rst
├── contributing
│ └── index.rst
├── make.bat
└── Makefile
├── travis
├── run_docker.sh
├── build_manylinux_wheels.sh
└── run_tests.sh
├── Makefile
├── .gitignore
├── tox.ini
├── setup.cfg
├── README.rst
├── LICENSE
├── .travis.yml
└── conftest.py
/tests/utils/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/opbeat/contrib/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/client/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/config/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/contrib/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/contrib/async/tests.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/events/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/handlers/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/middleware/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/processors/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/transports/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/utils/json/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/utils/wsgi/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/contrib/async/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/contrib/celery/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/contrib/django/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/contrib/flask/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/contrib/pylons/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/contrib/twisted/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/contrib/zerorpc/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/instrumentation/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/utils/encoding/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/utils/stacks/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/opbeat/instrumentation/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
1 | VERSION = 1.0
2 |
--------------------------------------------------------------------------------
/tests/handlers/logbook/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/handlers/logging/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/opbeat/instrumentation/packages/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/opbeat/contrib/django/management/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/contrib/django/testapp/templates/404.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/instrumentation/django_tests/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/instrumentation/jinja2_tests/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/opbeat/contrib/django/management/commands/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/opbeat/instrumentation/packages/django/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test_requirements/requirements-cpython.txt:
--------------------------------------------------------------------------------
1 | psycopg2
--------------------------------------------------------------------------------
/test_requirements/requirements-pypy.txt:
--------------------------------------------------------------------------------
1 | psycopg2cffi
--------------------------------------------------------------------------------
/test_requirements/requirements-python-3.txt:
--------------------------------------------------------------------------------
1 | python3-memcached
--------------------------------------------------------------------------------
/opbeat/contrib/asyncio/__init__.py:
--------------------------------------------------------------------------------
1 | from .client import Client
2 |
--------------------------------------------------------------------------------
/AUTHORS:
--------------------------------------------------------------------------------
1 | http://github.com/dcramer/raven/contributors
2 |
3 | and Opbeat
--------------------------------------------------------------------------------
/test_requirements/requirements-python-2.txt:
--------------------------------------------------------------------------------
1 | unittest2
2 | python-memcached
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include setup.py README.rst MANIFEST.in LICENSE
2 | global-exclude *~
3 |
--------------------------------------------------------------------------------
/opbeat/version.py:
--------------------------------------------------------------------------------
1 | __version__ = (3, 6, 1)
2 | VERSION = '.'.join(map(str, __version__))
3 |
--------------------------------------------------------------------------------
/test_requirements/requirements-asyncio.txt:
--------------------------------------------------------------------------------
1 | aiohttp
2 | pytest-asyncio
3 | pytest-mock
4 |
--------------------------------------------------------------------------------
/test_requirements/requirements-flask-0.10.txt:
--------------------------------------------------------------------------------
1 | Flask>=0.10,<0.11
2 | -r requirements-base.txt
3 |
--------------------------------------------------------------------------------
/test_requirements/requirements-flask-0.11.txt:
--------------------------------------------------------------------------------
1 | Flask>=0.11,<0.12
2 | -r requirements-base.txt
3 |
--------------------------------------------------------------------------------
/test_requirements/requirements-flask-0.12.txt:
--------------------------------------------------------------------------------
1 | Flask>=0.12,<0.13
2 | -r requirements-base.txt
3 |
--------------------------------------------------------------------------------
/test_requirements/requirements-zerorpc.txt:
--------------------------------------------------------------------------------
1 | pyzmq==13.1.0
2 | gevent==1.1b1
3 | zerorpc>=0.4.0,<0.5
--------------------------------------------------------------------------------
/tests/contrib/django/fake2/__init__.py:
--------------------------------------------------------------------------------
1 |
2 |
3 | class FakeException(BaseException):
4 | pass
5 |
--------------------------------------------------------------------------------
/test_requirements/requirements-django-2.0.txt:
--------------------------------------------------------------------------------
1 | Django>=2.0,<2.1
2 | django-celery
3 | -r requirements-base.txt
4 |
--------------------------------------------------------------------------------
/tests/contrib/django/testapp/templates/error.html:
--------------------------------------------------------------------------------
1 | Foo Bar
2 | Baz
3 | {% invalid template tag %}
4 | 42
5 | 4711
--------------------------------------------------------------------------------
/test_requirements/requirements-django-1.10.txt:
--------------------------------------------------------------------------------
1 | Django>=1.10,<1.11
2 | django-celery
3 | -r requirements-base.txt
4 |
--------------------------------------------------------------------------------
/test_requirements/requirements-django-1.11.txt:
--------------------------------------------------------------------------------
1 | Django>=1.11b1,<1.12
2 | django-celery
3 | -r requirements-base.txt
4 |
--------------------------------------------------------------------------------
/test_requirements/requirements-django-1.4.txt:
--------------------------------------------------------------------------------
1 | Django>=1.4.21,<1.5
2 | django-celery
3 | -r requirements-base.txt
4 |
--------------------------------------------------------------------------------
/test_requirements/requirements-django-1.5.txt:
--------------------------------------------------------------------------------
1 | Django>=1.5.12,<1.6
2 | django-celery
3 | -r requirements-base.txt
4 |
--------------------------------------------------------------------------------
/test_requirements/requirements-django-1.6.txt:
--------------------------------------------------------------------------------
1 | Django>=1.6.11,<1.7
2 | django-celery
3 | -r requirements-base.txt
4 |
--------------------------------------------------------------------------------
/test_requirements/requirements-django-1.7.txt:
--------------------------------------------------------------------------------
1 | Django>=1.7.9,<1.8
2 | django-celery
3 | -r requirements-base.txt
4 |
--------------------------------------------------------------------------------
/test_requirements/requirements-django-1.8.txt:
--------------------------------------------------------------------------------
1 | Django>=1.8.3,<1.9
2 | django-celery
3 | -r requirements-base.txt
4 |
--------------------------------------------------------------------------------
/test_requirements/requirements-django-1.9.txt:
--------------------------------------------------------------------------------
1 | Django>=1.9,<1.10
2 | django-celery
3 | -r requirements-base.txt
4 |
--------------------------------------------------------------------------------
/tests/contrib/django/testapp/templates/jinja2/jinja2_template.html:
--------------------------------------------------------------------------------
1 |
2 | {% macro foo() %}42{% endmacro %}23
3 |
--------------------------------------------------------------------------------
/tests/instrumentation/jinja2_tests/mytemplate.html:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/docs/config/index.rst:
--------------------------------------------------------------------------------
1 | Configuration
2 | =============
3 |
4 | .. csv-table::
5 | :class: page-info
6 |
7 | "Page updated: 23rd July 2013", ""
--------------------------------------------------------------------------------
/test_requirements/requirements-django-master.txt:
--------------------------------------------------------------------------------
1 | -e git://github.com/django/django.git@master#egg=Django
2 | django-celery
3 | -r requirements-base.txt
4 |
--------------------------------------------------------------------------------
/tests/contrib/django/testapp/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | from __future__ import absolute_import
4 |
5 | from .celery import app as celery_app
6 |
--------------------------------------------------------------------------------
/tests/contrib/flask/templates/users.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | {% for user in users %}
4 | - {{user}}
5 | {% endfor %}
6 |
7 |
8 |
--------------------------------------------------------------------------------
/tests/contrib/django/fake1/__init__.py:
--------------------------------------------------------------------------------
1 |
2 |
3 | class FakeException(BaseException):
4 | pass
5 |
6 |
7 | class OtherFakeException(BaseException):
8 | pass
9 |
--------------------------------------------------------------------------------
/opbeat/transport/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | from opbeat.transport.http import AsyncHTTPTransport, HTTPTransport
4 |
5 |
6 | default = [HTTPTransport, AsyncHTTPTransport]
7 |
--------------------------------------------------------------------------------
/opbeat/instrumentation/control.py:
--------------------------------------------------------------------------------
1 | from opbeat.instrumentation import register
2 |
3 |
4 | def instrument():
5 | for obj in register.get_instrumentation_objects():
6 | obj.instrument()
7 |
--------------------------------------------------------------------------------
/tests/utils/compat.py:
--------------------------------------------------------------------------------
1 |
2 | try:
3 | from unittest2 import TestCase
4 | from unittest2 import skipIf
5 | except ImportError:
6 | from unittest import TestCase
7 | from unittest import skipIf
8 |
--------------------------------------------------------------------------------
/opbeat/contrib/django/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 |
3 |
4 | class OpbeatConfig(AppConfig):
5 | name = 'opbeat.contrib.django'
6 | label = 'opbeat.contrib.django'
7 | verbose_name = 'Opbeat'
8 |
--------------------------------------------------------------------------------
/tests/contrib/django/testapp/templates/list_users.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | | Users |
4 | {% for user in users %}
5 | | {{ user }} |
6 | {% endfor %}
7 |
8 |
9 |
--------------------------------------------------------------------------------
/opbeat/contrib/paste.py:
--------------------------------------------------------------------------------
1 | from opbeat.base import Client
2 | from opbeat.middleware import Opbeat
3 |
4 |
5 | def opbeat_filter_factory(app, global_conf, **kwargs):
6 | client = Client(**kwargs)
7 | return Opbeat(app, client)
8 |
--------------------------------------------------------------------------------
/tests/utils/tests.py:
--------------------------------------------------------------------------------
1 | from opbeat.utils.deprecation import deprecated
2 |
3 |
4 | @deprecated("alternative")
5 | def deprecated_function():
6 | pass
7 |
8 |
9 | def test_deprecation():
10 | deprecated_function()
11 |
--------------------------------------------------------------------------------
/opbeat/handlers/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | opbeat.handlers
3 | ~~~~~~~~~~~~~~~~~~~~~~~
4 |
5 | :copyright: (c) 2011-2012 Opbeat
6 |
7 | Large portions are
8 | :copyright: (c) 2010 by the Sentry Team, see AUTHORS for more details.
9 | :license: BSD, see LICENSE for more details.
10 | """
11 |
--------------------------------------------------------------------------------
/travis/run_docker.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | if [ -z ${TRAVIS_TAG+x} ]; then
3 | echo "Not a tagged build, skipping building wheels";
4 | else
5 | mkdir -p wheelhouse;
6 | docker run --rm -v `pwd`:/io $DOCKER_IMAGE $PRE_CMD /io/travis/build_manylinux_wheels.sh;
7 | ls wheelhouse/;
8 | fi
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | isort:
2 | isort -rc -vb .
3 |
4 | test:
5 | if [ "$$TRAVIS_PYTHON_VERSION" != "3.5" ]; then \
6 | py.test --isort --ignore=tests/asyncio; \
7 | else py.test --isort; fi
8 |
9 | coverage:
10 | coverage run runtests.py --include=opbeat/* && \
11 | coverage html --omit=*/migrations/* -d cover
12 |
13 | .PHONY: isort test coverage
14 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.pyc
2 | *.log
3 | *.egg
4 | *.db
5 | *.pid
6 | .coverage
7 | .DS_Store
8 | .idea
9 | pip-log.txt
10 | /*.egg-info
11 | /build
12 | /cover
13 | /dist
14 | /example_project/local_settings.py
15 | /docs/html
16 | /docs/doctrees
17 | /example_project/*.db
18 | opbeat/utils/wrapt/_wrappers.so
19 | coverage
20 | .tox
21 | .eggs
22 | .cache
23 | /testdb.sql
24 | venv
--------------------------------------------------------------------------------
/tox.ini:
--------------------------------------------------------------------------------
1 | # Tox (http://codespeak.net/~hpk/tox/) is a tool for running tests
2 | # in multiple virtualenvs. This configuration file will run the
3 | # test suite on all supported python versions. To use it, "pip install tox"
4 | # and then run "tox" from this directory.
5 |
6 | [tox]
7 | envlist = py{26,27,33,34,35,py}
8 |
9 | [testenv]
10 | commands = python setup.py test -a "{posargs}"
11 |
--------------------------------------------------------------------------------
/opbeat/contrib/django/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | opbeat.contrib.django
3 | ~~~~~~~~~~~~~~~~~~~~
4 |
5 | :copyright: (c) 2011-2012 Opbeat
6 |
7 | Large portions are
8 | :copyright: (c) 2010 by the Sentry Team, see AUTHORS for more details.
9 | :license: BSD, see LICENSE for more details.
10 | """
11 |
12 | default_app_config = 'opbeat.contrib.django.apps.OpbeatConfig'
13 |
14 | from opbeat.contrib.django.client import *
15 |
--------------------------------------------------------------------------------
/opbeat/transport/exceptions.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 |
4 | class InvalidScheme(ValueError):
5 | """
6 | Raised when a transport is constructed using a URI which is not
7 | handled by the transport
8 | """
9 |
10 |
11 | class DuplicateScheme(Exception):
12 | """
13 | Raised when registering a handler for a particular scheme which
14 | is already registered
15 | """
16 | pass
17 |
--------------------------------------------------------------------------------
/tests/utils/lru_tests.py:
--------------------------------------------------------------------------------
1 | from __future__ import absolute_import
2 |
3 | from opbeat.utils.lru import LRUCache
4 | from tests.utils.compat import TestCase
5 |
6 |
7 | class LRUTest(TestCase):
8 | def test_insert_overflow(self):
9 |
10 | lru = LRUCache(4)
11 |
12 | for x in range(6):
13 | lru.set(x)
14 |
15 | self.assertFalse(lru.has_key(1))
16 | for x in range(2, 6):
17 | self.assertTrue(lru.has_key(x))
18 |
--------------------------------------------------------------------------------
/docs/install/index.rst:
--------------------------------------------------------------------------------
1 | Install
2 | =======
3 |
4 | If you haven't already, start by downloading opbeat. The easiest way is with **pip**
5 |
6 | .. code-block:: bash
7 |
8 | pip install opbeat
9 |
10 | Requirements
11 | ------------
12 |
13 | If you installed using pip or setuptools you shouldn't need to worry about requirements.
14 | Otherwise you will need to install the following packages in your environment,
15 | if you are using Python 2.6.
16 |
17 | - ``simplejson``
18 |
--------------------------------------------------------------------------------
/opbeat/contrib/django/celery/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | opbeat.contrib.django.celery
3 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
4 |
5 | :copyright: (c) 2011-2012 Opbeat
6 |
7 | Large portions are
8 | :copyright: (c) 2010 by the Sentry Team, see AUTHORS for more details.
9 | :license: BSD, see LICENSE for more details.
10 | """
11 |
12 | from opbeat.contrib.celery import CeleryMixin
13 | from opbeat.contrib.django import DjangoClient
14 |
15 |
16 | class CeleryClient(CeleryMixin, DjangoClient):
17 | pass
18 |
--------------------------------------------------------------------------------
/opbeat/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | opbeat
3 | ~~~~~
4 |
5 | :copyright: (c) 2011-2012 Opbeat
6 |
7 | Large portions are
8 | :copyright: (c) 2010 by the Sentry Team, see AUTHORS for more details.
9 | :license: BSD, see LICENSE for more details.
10 | """
11 |
12 | __all__ = ('VERSION', 'Client')
13 |
14 | try:
15 | VERSION = __import__('pkg_resources') \
16 | .get_distribution('opbeat').version
17 | except Exception as e:
18 | VERSION = 'unknown'
19 |
20 | from opbeat.base import *
21 | from opbeat.conf import *
22 | from opbeat.traces import *
23 |
--------------------------------------------------------------------------------
/tests/contrib/django/testapp/models.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import absolute_import
3 |
4 | from django import VERSION as DJANGO_VERSION
5 | from django.db import models
6 |
7 | if DJANGO_VERSION >= (1, 5):
8 | from django.contrib.auth.models import AbstractBaseUser, BaseUserManager
9 |
10 | class MyUser(AbstractBaseUser):
11 | USERNAME_FIELD = 'my_username'
12 | my_username = models.CharField(max_length=30)
13 |
14 | objects = BaseUserManager()
15 |
16 | class Meta:
17 | abstract = False
18 |
--------------------------------------------------------------------------------
/opbeat/instrumentation/packages/jinja2.py:
--------------------------------------------------------------------------------
1 | from opbeat.instrumentation.packages.base import AbstractInstrumentedModule
2 | from opbeat.traces import trace
3 |
4 |
5 | class Jinja2Instrumentation(AbstractInstrumentedModule):
6 | name = 'jinja2'
7 |
8 | instrument_list = [
9 | ("jinja2", "Template.render"),
10 | ]
11 |
12 | def call(self, module, method, wrapped, instance, args, kwargs):
13 | signature = instance.name or instance.filename
14 | with trace(signature, "template.jinja2"):
15 | return wrapped(*args, **kwargs)
16 |
--------------------------------------------------------------------------------
/opbeat/instrumentation/packages/zlib.py:
--------------------------------------------------------------------------------
1 | from opbeat.instrumentation.packages.base import AbstractInstrumentedModule
2 | from opbeat.traces import trace
3 |
4 |
5 | class ZLibInstrumentation(AbstractInstrumentedModule):
6 | name = 'zlib'
7 | instrument_list = [
8 | ('zlib', 'compress'),
9 | ('zlib', 'decompress'),
10 | ]
11 |
12 | def call(self, module, method, wrapped, instance, args, kwargs):
13 | wrapped_name = module + "." + method
14 | with trace(wrapped_name, "compression.zlib"):
15 | return wrapped(*args, **kwargs)
16 |
--------------------------------------------------------------------------------
/test_requirements/requirements-base.txt:
--------------------------------------------------------------------------------
1 | py==1.4.30
2 | pytest==3.0.6
3 | pytest-capturelog==0.7
4 | pytest-django==2.8.0
5 | pytest-benchmark==2.5.0
6 | apipkg==1.4
7 | execnet==1.4.1
8 | isort==4.2.2
9 | pytest-cache==1.0
10 | pytest-isort==0.1.0
11 |
12 | urllib3
13 | certifi
14 | Jinja2
15 | Logbook
16 | MarkupSafe
17 | WebOb
18 | Werkzeug
19 | amqp==1.4.9
20 | anyjson
21 | argparse
22 | billiard
23 | blinker>=1.1
24 | boto3
25 | celery<4
26 | greenlet
27 | itsdangerous
28 | kombu<4
29 | mock
30 | msgpack-python
31 | pep8
32 | redis
33 | requests
34 | urllib3-mock
35 | pymongo
36 | Twisted
37 |
38 | pytz
39 |
--------------------------------------------------------------------------------
/opbeat/contrib/django/celery/models.py:
--------------------------------------------------------------------------------
1 | """
2 | opbeat.contrib.django.celery.models
3 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
4 |
5 | :copyright: (c) 2011-2012 Opbeat
6 |
7 | Large portions are
8 | :copyright: (c) 2010 by the Sentry Team, see AUTHORS for more details.
9 | :license: BSD, see LICENSE for more details.
10 | """
11 |
12 | from django.conf import settings
13 | from django.core.exceptions import ImproperlyConfigured
14 |
15 | if 'djcelery' not in settings.INSTALLED_APPS:
16 | raise ImproperlyConfigured("Put 'djcelery' in your "
17 | "INSTALLED_APPS setting in order to use the sentry celery client.")
18 |
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [nosetests]
2 | exclude=^(start|stop)_test_server
3 | verbose=2
4 |
5 | [bdist_wheel]
6 | universal=0
7 |
8 | [pytest]
9 | python_files=tests.py test_*.py *_tests.py
10 | isort_ignore=
11 | opbeat/transport/asyncio.py
12 | opbeat/contrib/asyncio/client.py
13 |
14 | [isort]
15 | line_length=80
16 | indent=' '
17 | not_skip=__init__.py
18 | skip=wrapt,setup.py,six.py
19 | multi_line_output=0
20 | known_standard_library=importlib,types
21 | known_django=django
22 | known_first_party=opbeat,tests
23 | known_third_party=pytest,flask
24 | default_section=FIRSTPARTY
25 | sections=FUTURE,STDLIB,DJANGO,THIRDPARTY,FIRSTPARTY,LOCALFOLDER
26 |
--------------------------------------------------------------------------------
/docs/config/wsgi.rst:
--------------------------------------------------------------------------------
1 | Configuring ``WSGI`` Middleware
2 | ===============================
3 |
4 | .. csv-table::
5 | :class: page-info
6 |
7 | "Page updated: 23rd July 2013", ""
8 |
9 | opbeat includes a simple to use WSGI middleware.
10 |
11 | ::
12 |
13 | from opbeat import Client
14 | from opbeat.middleware import Opbeat
15 |
16 | application = Opbeat(
17 | application,
18 | Client(organization_id='', app_id='', secret_token='')
19 | )
20 |
21 | .. container:: note
22 |
23 | Many frameworks will not propagate exceptions to the underlying WSGI middleware by default.
24 |
--------------------------------------------------------------------------------
/opbeat/contrib/flask/utils.py:
--------------------------------------------------------------------------------
1 | try:
2 | import urlparse
3 | except ImportError:
4 | import urllib.parse as urlparse
5 |
6 | from opbeat.utils.wsgi import get_environ, get_headers
7 |
8 |
9 | def get_data_from_request(request):
10 | urlparts = urlparse.urlsplit(request.url)
11 |
12 | return {
13 | 'http': {
14 | 'url': '%s://%s%s' % (urlparts.scheme, urlparts.netloc, urlparts.path),
15 | 'query_string': urlparts.query,
16 | 'method': request.method,
17 | 'data': request.form,
18 | 'headers': dict(get_headers(request.environ)),
19 | 'env': dict(get_environ(request.environ)),
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/tests/contrib/pylons/tests.py:
--------------------------------------------------------------------------------
1 | from __future__ import absolute_import
2 |
3 | from opbeat.contrib.pylons import Opbeat
4 | from tests.utils.compat import TestCase
5 |
6 |
7 | def example_app(environ, start_response):
8 | raise ValueError('hello world')
9 |
10 |
11 | class MiddlewareTest(TestCase):
12 | def setUp(self):
13 | self.app = example_app
14 |
15 | def test_init(self):
16 | config = {
17 | 'opbeat.servers': 'http://localhost/api/store',
18 | 'opbeat.organization_id': 'p' * 32,
19 | 'opbeat.app_id': 'p' * 32,
20 | 'opbeat.secret_token': 'a' * 32,
21 | }
22 | middleware = Opbeat(self.app, config)
23 |
--------------------------------------------------------------------------------
/tests/contrib/django/testapp/celery.py:
--------------------------------------------------------------------------------
1 | from __future__ import absolute_import
2 |
3 | from django.conf import settings
4 |
5 | from celery import Celery
6 |
7 | from opbeat.contrib.celery import register_signal
8 | from opbeat.contrib.django.models import client, logger, register_handlers
9 |
10 | app = Celery('testapp')
11 |
12 | app.config_from_object('django.conf:settings')
13 | app.autodiscover_tasks(lambda: settings.INSTALLED_APPS)
14 |
15 |
16 | # hook up Opbeat
17 |
18 |
19 | try:
20 | register_signal(client)
21 | except Exception as e:
22 | logger.exception('Failed installing celery hook: %s' % e)
23 |
24 | if 'opbeat.contrib.django' in settings.INSTALLED_APPS:
25 | register_handlers()
26 |
--------------------------------------------------------------------------------
/opbeat/utils/wrapt/__init__.py:
--------------------------------------------------------------------------------
1 | __version_info__ = ('1', '10', '2')
2 | __version__ = '.'.join(__version_info__)
3 |
4 | from .wrappers import (ObjectProxy, CallableObjectProxy, FunctionWrapper,
5 | BoundFunctionWrapper, WeakFunctionProxy, resolve_path, apply_patch,
6 | wrap_object, wrap_object_attribute, function_wrapper,
7 | wrap_function_wrapper, patch_function_wrapper,
8 | transient_function_wrapper)
9 |
10 | from .decorators import (adapter_factory, AdapterFactory, decorator,
11 | synchronized)
12 |
13 | from .importer import (register_post_import_hook, when_imported,
14 | discover_post_import_hooks)
15 |
16 | try:
17 | from inspect import getcallargs
18 | except ImportError:
19 | from .arguments import getcallargs
20 |
--------------------------------------------------------------------------------
/tests/helpers.py:
--------------------------------------------------------------------------------
1 | from opbeat.base import Client
2 |
3 |
4 | def get_tempstoreclient(organization_id="1", app_id="2",
5 | secret_token="test_key", **kwargs):
6 | return TempStoreClient(organization_id=organization_id, app_id=app_id,
7 | secret_token=secret_token, **kwargs)
8 |
9 |
10 | class TempStoreClient(Client):
11 | def __init__(self,
12 | servers=None, organization_id=None, app_id=None,
13 | secret_token=None, **kwargs):
14 | self.events = []
15 | super(TempStoreClient, self).__init__(
16 | servers=servers, organization_id=organization_id, app_id=app_id,
17 | secret_token=secret_token, **kwargs)
18 |
19 | def send(self, **kwargs):
20 | self.events.append(kwargs)
21 |
--------------------------------------------------------------------------------
/docs/config/logbook.rst:
--------------------------------------------------------------------------------
1 | Configuring ``logbook``
2 | =======================
3 |
4 | .. csv-table::
5 | :class: page-info
6 |
7 | "Page updated: 23rd July 2013", ""
8 |
9 | opbeat provides a `logbook `_ handler which will pipe
10 | messages to Opbeat.
11 |
12 | First you'll need to configure a handler::
13 |
14 | from opbeat.handlers.logbook import OpbeatHandler
15 |
16 | # Manually specify a client
17 | client = Client(...)
18 | handler = OpbeatHandler(client)
19 |
20 | Finally, bind your handler to your context::
21 |
22 | from opbeat.handlers.logbook import OpbeatHandler
23 |
24 | client = Client(...)
25 | opbeat_handler = OpbeatHandler(client)
26 | with opbeat_handler.applicationbound():
27 | # everything logged here will go to sentry.
28 | ...
29 |
--------------------------------------------------------------------------------
/tests/events/tests.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import absolute_import
3 |
4 | from mock import Mock
5 |
6 | from opbeat.events import Message
7 | from tests.utils.compat import TestCase
8 |
9 |
10 | class MessageTest(TestCase):
11 | def test_to_string(self):
12 | unformatted_message = 'My message from %s about %s'
13 | client = Mock()
14 | message = Message(client)
15 | message.logger = Mock()
16 | data = {
17 | 'param_message': {
18 | 'message': unformatted_message,
19 | }
20 | }
21 |
22 | self.assertEqual(message.to_string(data), unformatted_message)
23 |
24 | data['param_message']['params'] = (1, 2)
25 | self.assertEqual(message.to_string(data),
26 | unformatted_message % (1, 2))
27 |
--------------------------------------------------------------------------------
/tests/instrumentation/base_tests.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | from opbeat.instrumentation.packages.base import AbstractInstrumentedModule
4 |
5 |
6 | class _TestInstrumentNonExistingFunctionOnModule(AbstractInstrumentedModule):
7 | name = "test_non_existing_function_instrumentation"
8 | instrument_list = [
9 | ("os.path", "non_existing_function")
10 | ]
11 |
12 |
13 | class _TestInstrumentNonExistingMethod(AbstractInstrumentedModule):
14 | name = "test_non_existing_method_instrumentation"
15 | instrument_list = [
16 | ("dict", "non_existing_method")
17 | ]
18 |
19 |
20 | def test_instrument_nonexisting_method_on_module():
21 | _TestInstrumentNonExistingFunctionOnModule().instrument()
22 |
23 |
24 | def test_instrument_nonexisting_method():
25 | _TestInstrumentNonExistingMethod().instrument()
26 |
--------------------------------------------------------------------------------
/opbeat/contrib/asyncio/client.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | import urllib.parse
3 |
4 | from opbeat.base import Client
5 |
6 |
7 | class Client(Client):
8 |
9 | def handle_transport_response(self, task):
10 | try:
11 | url = task.result()
12 | except Exception as exc:
13 | self.handle_transport_fail(exception=exc)
14 | else:
15 | self.handle_transport_success(url=url)
16 |
17 | def _send_remote(self, url, data, headers=None):
18 | if headers is None:
19 | headers = {}
20 | parsed = urllib.parse.urlparse(url)
21 | transport = self._get_transport(parsed)
22 | loop = asyncio.get_event_loop()
23 | task = loop.create_task(
24 | transport.send(data, headers, timeout=self.timeout))
25 | task.add_done_callback(self.handle_transport_response)
26 |
--------------------------------------------------------------------------------
/opbeat/contrib/django/middleware/wsgi.py:
--------------------------------------------------------------------------------
1 | """
2 | opbeat.contrib.django.middleware.wsgi
3 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
4 |
5 | :copyright: (c) 2011-2012 Opbeat
6 |
7 | Large portions are
8 | :copyright: (c) 2010 by the Sentry Team, see AUTHORS for more details.
9 | :license: BSD, see LICENSE for more details.
10 | """
11 |
12 | from opbeat.middleware import Opbeat
13 |
14 |
15 | class Opbeat(Opbeat):
16 | """
17 | Identical to the default WSGI middleware except that
18 | the client comes dynamically via ``get_client
19 |
20 | >>> from opbeat.contrib.django.middleware.wsgi import Opbeat
21 | >>> application = Opbeat(application)
22 | """
23 | def __init__(self, application):
24 | self.application = application
25 |
26 | @property
27 | def client(self):
28 | from opbeat.contrib.django.models import client
29 | return client
30 |
--------------------------------------------------------------------------------
/tests/contrib/django/testapp/middleware.py:
--------------------------------------------------------------------------------
1 | try:
2 | from django.utils.deprecation import MiddlewareMixin
3 | except ImportError:
4 | # no-op class for Django < 1.10
5 | class MiddlewareMixin(object):
6 | pass
7 |
8 |
9 | class BrokenRequestMiddleware(MiddlewareMixin):
10 | def process_request(self, request):
11 | raise ImportError('request')
12 |
13 |
14 | class BrokenResponseMiddleware(MiddlewareMixin):
15 | def process_response(self, request, response):
16 | raise ImportError('response')
17 |
18 |
19 | class BrokenViewMiddleware(MiddlewareMixin):
20 | def process_view(self, request, func, args, kwargs):
21 | raise ImportError('view')
22 |
23 |
24 | class MetricsNameOverrideMiddleware(MiddlewareMixin):
25 | def process_response(self, request, response):
26 | request._opbeat_transaction_name = 'foobar'
27 | return response
28 |
--------------------------------------------------------------------------------
/tests/contrib/twisted/tests.py:
--------------------------------------------------------------------------------
1 | from __future__ import absolute_import
2 |
3 | from twisted.python.failure import Failure
4 |
5 | from opbeat.contrib.twisted import OpbeatLogObserver
6 | from tests.helpers import get_tempstoreclient
7 | from tests.utils.compat import TestCase
8 |
9 |
10 | class TwistedLogObserverTest(TestCase):
11 | def setUp(self):
12 | self.client = get_tempstoreclient()
13 |
14 | def test_observer(self):
15 | observer = OpbeatLogObserver(client=self.client)
16 | try:
17 | 1 / 0
18 | except ZeroDivisionError:
19 | failure = Failure()
20 | event = dict(log_failure=failure)
21 | observer(event)
22 |
23 | cli_event = self.client.events.pop(0)
24 | self.assertEquals(cli_event['exception']['type'], 'ZeroDivisionError')
25 | self.assertTrue('zero' in cli_event['exception']['value'])
26 |
--------------------------------------------------------------------------------
/opbeat/contrib/rq/__init__.py:
--------------------------------------------------------------------------------
1 | def register_opbeat(client, worker):
2 | """Given a Opbeat client and an RQ worker, registers exception handlers
3 | with the worker so exceptions are logged to Opbeat.
4 |
5 | E.g.:
6 |
7 | from opbeat.contrib.django.models import client
8 | from opbeat.contrib.rq import register_opbeat
9 |
10 | worker = Worker(map(Queue, listen))
11 | register_opbeat(client, worker)
12 | worker.work()
13 |
14 | """
15 | def send_to_opbeat(job, *exc_info):
16 | client.capture_exception(
17 | exc_info=exc_info,
18 | extra={
19 | 'job_id': job.id,
20 | 'func': job.func_name,
21 | 'args': job.args,
22 | 'kwargs': job.kwargs,
23 | 'description': job.description,
24 | }
25 | )
26 |
27 | worker.push_exc_handler(send_to_opbeat)
28 |
--------------------------------------------------------------------------------
/opbeat/instrumentation/packages/mysql.py:
--------------------------------------------------------------------------------
1 | from opbeat.instrumentation.packages.dbapi2 import (ConnectionProxy,
2 | CursorProxy,
3 | DbApi2Instrumentation,
4 | extract_signature)
5 |
6 |
7 | class MySQLCursorProxy(CursorProxy):
8 | provider_name = 'mysql'
9 |
10 | def extract_signature(self, sql):
11 | return extract_signature(sql)
12 |
13 |
14 | class MySQLConnectionProxy(ConnectionProxy):
15 | cursor_proxy = MySQLCursorProxy
16 |
17 |
18 | class MySQLInstrumentation(DbApi2Instrumentation):
19 | name = 'mysql'
20 |
21 | instrument_list = [
22 | ("MySQLdb", "connect"),
23 | ]
24 |
25 | def call(self, module, method, wrapped, instance, args, kwargs):
26 | return MySQLConnectionProxy(wrapped(*args, **kwargs))
27 |
--------------------------------------------------------------------------------
/tests/config/tests.py:
--------------------------------------------------------------------------------
1 | from __future__ import absolute_import
2 |
3 | import logging
4 |
5 | import mock
6 |
7 | from opbeat.conf import setup_logging
8 | from tests.utils.compat import TestCase
9 |
10 |
11 | class SetupLoggingTest(TestCase):
12 | def test_basic_not_configured(self):
13 | with mock.patch('logging.getLogger', spec=logging.getLogger) as getLogger:
14 | logger = getLogger()
15 | logger.handlers = []
16 | handler = mock.Mock()
17 | result = setup_logging(handler)
18 | self.assertTrue(result)
19 |
20 | def test_basic_already_configured(self):
21 | with mock.patch('logging.getLogger', spec=logging.getLogger) as getLogger:
22 | handler = mock.Mock()
23 | logger = getLogger()
24 | logger.handlers = [handler]
25 | result = setup_logging(handler)
26 | self.assertFalse(result)
27 |
--------------------------------------------------------------------------------
/travis/build_manylinux_wheels.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | set -e -x
4 |
5 | export SKIP_ZERORPC=1
6 | export OPBEAT_WRAPT_EXTENSIONS="true"
7 |
8 | # Compile wheels
9 | for PYBIN in /opt/python/*/bin; do
10 | if [[ $PYBIN == *cp26* ]]; then
11 | continue
12 | fi
13 | ${PYBIN}/pip install -r /io/test_requirements/requirements-base.txt
14 | ${PYBIN}/pip install -r /io/test_requirements/requirements-python-$($PYBIN/python -c "import sys; print(sys.version_info[0])").txt
15 | ${PYBIN}/pip wheel /io/ -w wheelhouse/
16 | done
17 |
18 | # Bundle external shared libraries into the wheels
19 | for whl in wheelhouse/opbeat*.whl; do
20 | auditwheel repair $whl -w /io/wheelhouse/
21 | done
22 |
23 | # Install packages and test
24 | for PYBIN in /opt/python/*/bin/; do
25 | ${PYBIN}/pip install opbeat --no-index -f /io/wheelhouse
26 | (cd $HOME; ${PYBIN}/py.test)
27 | done
28 |
29 | chmod 0777 /io/wheelhouse/*.whl
30 |
--------------------------------------------------------------------------------
/opbeat/contrib/twisted/__init__.py:
--------------------------------------------------------------------------------
1 | from twisted.logger import ILogObserver
2 | from zope.interface import implementer
3 |
4 | from opbeat.base import Client
5 |
6 |
7 | @implementer(ILogObserver)
8 | class OpbeatLogObserver(object):
9 | """
10 | A twisted log observer for Opbeat.
11 | Eg.:
12 |
13 | from opbeat.base import Client
14 | from twisted.logger import Logger
15 |
16 | client = Client(...)
17 | observer = OpbeatLogObserver(client=client)
18 | log = Logger(observer=observer)
19 |
20 | try:
21 | 1 / 0
22 | except:
23 | log.failure("Math is hard!")
24 | """
25 |
26 | def __init__(self, client=None, **kwargs):
27 | self.client = client or Client(**kwargs)
28 |
29 | def __call__(self, event):
30 | failure = event.get('log_failure')
31 | if failure is not None:
32 | self.client.capture_exception(
33 | (failure.type, failure.value, failure.getTracebackObject()),
34 | extra=event)
35 |
--------------------------------------------------------------------------------
/docs/index.rst:
--------------------------------------------------------------------------------
1 | Error logging: Python
2 | =====================
3 |
4 | .. csv-table::
5 | :class: page-info
6 |
7 | "Page updated: 23rd July 2013", ""
8 |
9 | Introduction
10 | ------------
11 | To send error logs to Opbeat, you must install an agent.
12 |
13 | This is the official Opbeat standalone Python agent. It is forked from `Raven `_.
14 |
15 | Requirements
16 | ------------
17 | - pip
18 | - simplejson (Only if you're using < Python 2.7)
19 |
20 |
21 | Installation
22 | ------------
23 |
24 | .. code::
25 | :class: language-bash
26 |
27 | # Install Opbeat
28 | $ pip install opbeat
29 |
30 | Configuration
31 | -------------
32 |
33 |
34 | .. toctree::
35 | :maxdepth: 1
36 |
37 | Django
38 | Flask
39 | Pylons
40 | Pyramid
41 | Logging
42 | Logbook
43 | WSGI Middle
44 | ZeroRPC
45 | Other
46 |
--------------------------------------------------------------------------------
/opbeat/utils/module_import.py:
--------------------------------------------------------------------------------
1 | import sys
2 | from importlib import import_module
3 |
4 | from opbeat.utils import six
5 |
6 |
7 | # From Django
8 | # https://github.com/django/django/blob/master/django/utils/module_loading.py
9 |
10 |
11 | def import_string(dotted_path):
12 | """
13 | Import a dotted module path and return the attribute/class designated by the
14 | last name in the path. Raise ImportError if the import failed.
15 | """
16 | try:
17 | module_path, class_name = dotted_path.rsplit('.', 1)
18 | except ValueError:
19 | msg = "%s doesn't look like a module path" % dotted_path
20 | six.reraise(ImportError, ImportError(msg), sys.exc_info()[2])
21 |
22 | module = import_module(module_path)
23 |
24 | try:
25 | return getattr(module, class_name)
26 | except AttributeError:
27 | msg = 'Module "%s" does not define a "%s" attribute/class' % (
28 | module_path, class_name)
29 | six.reraise(ImportError, ImportError(msg), sys.exc_info()[2])
30 |
--------------------------------------------------------------------------------
/travis/run_tests.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | set -ex
4 |
5 | PYTHON_MAJOR_VERSION=$(python -c "import sys; print(sys.version_info[0])");
6 | mkdir -p "$PIP_CACHE"
7 | mkdir -p wheelhouse
8 | psql -c 'create database opbeat_test;' -U postgres
9 | pip install -U pip
10 | pip install -r "test_requirements/requirements-${WEBFRAMEWORK}.txt" --cache-dir "${PIP_CACHE}"
11 | pip install -r "test_requirements/requirements-python-${PYTHON_MAJOR_VERSION}.txt" --cache-dir "${PIP_CACHE}"
12 | if [[ $TRAVIS_PYTHON_VERSION == '3.5' ]]; then
13 | pip install -r test_requirements/requirements-asyncio.txt --cache-dir "${PIP_CACHE}"
14 | fi
15 | if [[ $TRAVIS_PYTHON_VERSION == 'pypy' ]]; then
16 | pip install -r test_requirements/requirements-pypy.txt --cache-dir "${PIP_CACHE}"
17 | else
18 | pip install -r test_requirements/requirements-cpython.txt --cache-dir "${PIP_CACHE}"
19 | if [[ $PYTHON_MAJOR_VERSION == '2' ]]; then
20 | pip install -r test_requirements/requirements-zerorpc.txt --cache-dir "${PIP_CACHE}"
21 | fi
22 | fi
23 |
24 | make test
25 |
--------------------------------------------------------------------------------
/opbeat/utils/compat.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | import atexit
3 | import functools
4 |
5 | try:
6 | import urlparse
7 | except ImportError:
8 | from urllib import parse as urlparse
9 |
10 | try:
11 | from urllib2 import HTTPError
12 | except ImportError:
13 | from urllib.error import HTTPError
14 |
15 |
16 | def noop_decorator(func):
17 | @functools.wraps(func)
18 | def wrapped(*args, **kwargs):
19 | return func(*args, **kwargs)
20 | return wrapped
21 |
22 |
23 | def atexit_register(func):
24 | """
25 | Uses either uwsgi's atexit mechanism, or atexit from the stdlib.
26 |
27 | When running under uwsgi, using their atexit handler is more reliable,
28 | especially when using gevent
29 | :param func: the function to call at exit
30 | """
31 | try:
32 | import uwsgi
33 | orig = getattr(uwsgi, 'atexit', None)
34 |
35 | def uwsgi_atexit():
36 | if callable(orig):
37 | orig()
38 | func()
39 |
40 | uwsgi.atexit = uwsgi_atexit
41 | except ImportError:
42 | atexit.register(func)
43 |
--------------------------------------------------------------------------------
/opbeat/utils/deprecation.py:
--------------------------------------------------------------------------------
1 | import functools
2 | import warnings
3 |
4 | # https://wiki.python.org/moin/PythonDecoratorLibrary#Smart_deprecation_warnings_.28with_valid_filenames.2C_line_numbers.2C_etc..29
5 | # Updated to work with 2.6 and 3+.
6 | from opbeat.utils import six
7 |
8 |
9 | def deprecated(alternative=None):
10 | """This is a decorator which can be used to mark functions
11 | as deprecated. It will result in a warning being emitted
12 | when the function is used."""
13 | def real_decorator(func):
14 | @functools.wraps(func)
15 | def new_func(*args, **kwargs):
16 | msg = "Call to deprecated function {0}.".format(func.__name__)
17 | if alternative:
18 | msg += " Use {0} instead".format(alternative)
19 | warnings.warn_explicit(
20 | msg,
21 | category=DeprecationWarning,
22 | filename=six.get_function_code(func).co_filename,
23 | lineno=six.get_function_code(func).co_firstlineno + 1
24 | )
25 | return func(*args, **kwargs)
26 | return new_func
27 | return real_decorator
28 |
--------------------------------------------------------------------------------
/opbeat/instrumentation/packages/botocore.py:
--------------------------------------------------------------------------------
1 | from opbeat.instrumentation.packages.base import AbstractInstrumentedModule
2 | from opbeat.traces import trace
3 | from opbeat.utils.compat import urlparse
4 |
5 |
6 | class BotocoreInstrumentation(AbstractInstrumentedModule):
7 | name = 'botocore'
8 |
9 | instrument_list = [
10 | ('botocore.client', 'BaseClient._make_api_call'),
11 | ]
12 |
13 | def call(self, module, method, wrapped, instance, args, kwargs):
14 | if 'operation_name' in kwargs:
15 | operation_name = kwargs['operation_name']
16 | else:
17 | operation_name = args[0]
18 |
19 | target_endpoint = instance._endpoint.host
20 | parsed_url = urlparse.urlparse(target_endpoint)
21 | service, region, _ = parsed_url.hostname.split('.', 2)
22 |
23 | signature = '{}:{}'.format(service, operation_name)
24 | extra_data = {
25 | 'service': service,
26 | 'region': region,
27 | 'operation': operation_name,
28 | }
29 |
30 | with trace(signature, 'ext.http.aws', extra_data, leaf=True):
31 | return wrapped(*args, **kwargs)
32 |
--------------------------------------------------------------------------------
/tests/utils/json/tests.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import absolute_import
3 |
4 | import datetime
5 | import uuid
6 |
7 | from opbeat.utils import opbeat_json as json
8 | from opbeat.utils import six
9 | from tests.utils.compat import TestCase
10 |
11 |
12 | class JSONTest(TestCase):
13 | def test_uuid(self):
14 | res = uuid.uuid4()
15 | self.assertEquals(json.dumps(res), '"%s"' % res.hex)
16 |
17 | def test_datetime(self):
18 | res = datetime.datetime(day=1, month=1, year=2011, hour=1, minute=1, second=1)
19 | self.assertEquals(json.dumps(res), '"2011-01-01T01:01:01.000000Z"')
20 |
21 | def test_set(self):
22 | res = set(['foo', 'bar'])
23 | self.assertIn(json.dumps(res), ('["foo", "bar"]', '["bar", "foo"]'))
24 |
25 | def test_frozenset(self):
26 | res = frozenset(['foo', 'bar'])
27 | self.assertIn(json.dumps(res), ('["foo", "bar"]', '["bar", "foo"]'))
28 |
29 | def test_bytes(self):
30 | if six.PY2:
31 | res = bytes('foobar')
32 | else:
33 | res = bytes('foobar', encoding='ascii')
34 | self.assertEqual(json.dumps(res), '"foobar"')
35 |
--------------------------------------------------------------------------------
/opbeat/instrumentation/packages/urllib3.py:
--------------------------------------------------------------------------------
1 | from opbeat.instrumentation.packages.base import AbstractInstrumentedModule
2 | from opbeat.traces import trace
3 | from opbeat.utils import default_ports
4 |
5 |
6 | class Urllib3Instrumentation(AbstractInstrumentedModule):
7 | name = 'urllib3'
8 |
9 | instrument_list = [
10 | ("urllib3.connectionpool", "HTTPConnectionPool.urlopen"),
11 | ]
12 |
13 | def call(self, module, method, wrapped, instance, args, kwargs):
14 | if 'method' in kwargs:
15 | method = kwargs['method']
16 | else:
17 | method = args[0]
18 |
19 | host = instance.host
20 |
21 | if instance.port != default_ports.get(instance.scheme):
22 | host += ":" + str(instance.port)
23 |
24 | if 'url' in kwargs:
25 | url = kwargs['url']
26 | else:
27 | url = args[1]
28 |
29 | signature = method.upper() + " " + host
30 |
31 | url = instance.scheme + "://" + host + url
32 |
33 | with trace(signature, "ext.http.urllib3",
34 | {'url': url}, leaf=True):
35 | return wrapped(*args, **kwargs)
36 |
--------------------------------------------------------------------------------
/opbeat/contrib/django/handlers.py:
--------------------------------------------------------------------------------
1 | """
2 | opbeat.contrib.django.handlers
3 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
4 |
5 | :copyright: (c) 2011-2012 Opbeat
6 |
7 | Large portions are
8 | :copyright: (c) 2010 by the Sentry Team, see AUTHORS for more details.
9 | :license: BSD, see LICENSE for more details.
10 | """
11 |
12 | from __future__ import absolute_import
13 |
14 | import logging
15 |
16 | from opbeat.handlers.logging import OpbeatHandler as BaseOpbeatHandler
17 |
18 |
19 | class OpbeatHandler(BaseOpbeatHandler):
20 | def __init__(self, level=logging.NOTSET):
21 | logging.Handler.__init__(self, level=level)
22 |
23 | def _get_client(self):
24 | from opbeat.contrib.django.models import client
25 |
26 | return client
27 |
28 | client = property(_get_client)
29 |
30 | def _emit(self, record):
31 | from opbeat.contrib.django.middleware import OpbeatLogMiddleware
32 |
33 | # Fetch the request from a threadlocal variable, if available
34 | request = getattr(OpbeatLogMiddleware.thread, 'request', None)
35 | request = getattr(record, 'request', request)
36 |
37 | return super(OpbeatHandler, self)._emit(record, request=request)
38 |
--------------------------------------------------------------------------------
/opbeat/instrumentation/packages/pylibmc.py:
--------------------------------------------------------------------------------
1 | from opbeat.instrumentation.packages.base import AbstractInstrumentedModule
2 | from opbeat.traces import trace
3 |
4 |
5 | class PyLibMcInstrumentation(AbstractInstrumentedModule):
6 | name = 'pylibmc'
7 |
8 | instrument_list = [
9 | ("pylibmc", "Client.get"),
10 | ("pylibmc", "Client.get_multi"),
11 | ("pylibmc", "Client.set"),
12 | ("pylibmc", "Client.set_multi"),
13 | ("pylibmc", "Client.add"),
14 | ("pylibmc", "Client.replace"),
15 | ("pylibmc", "Client.append"),
16 | ("pylibmc", "Client.prepend"),
17 | ("pylibmc", "Client.incr"),
18 | ("pylibmc", "Client.decr"),
19 | ("pylibmc", "Client.gets"),
20 | ("pylibmc", "Client.cas"),
21 | ("pylibmc", "Client.delete"),
22 | ("pylibmc", "Client.delete_multi"),
23 | ("pylibmc", "Client.touch"),
24 | ("pylibmc", "Client.get_stats"),
25 | ]
26 |
27 | def call(self, module, method, wrapped, instance, args, kwargs):
28 | wrapped_name = self.get_wrapped_name(wrapped, instance, method)
29 | with trace(wrapped_name, "cache.memcached"):
30 | return wrapped(*args, **kwargs)
31 |
--------------------------------------------------------------------------------
/opbeat/instrumentation/packages/redis.py:
--------------------------------------------------------------------------------
1 | from opbeat.instrumentation.packages.base import AbstractInstrumentedModule
2 | from opbeat.traces import trace
3 |
4 |
5 | class RedisInstrumentation(AbstractInstrumentedModule):
6 | name = 'redis'
7 |
8 | instrument_list = [
9 | ("redis.client", "Redis.execute_command"),
10 | ("redis.client", "StrictRedis.execute_command"),
11 | ]
12 |
13 | def call(self, module, method, wrapped, instance, args, kwargs):
14 | if len(args) > 0:
15 | wrapped_name = str(args[0])
16 | else:
17 | wrapped_name = self.get_wrapped_name(wrapped, instance, method)
18 |
19 | with trace(wrapped_name, "cache.redis", leaf=True):
20 | return wrapped(*args, **kwargs)
21 |
22 |
23 | class RedisPipelineInstrumentation(AbstractInstrumentedModule):
24 | name = 'redis'
25 |
26 | instrument_list = [
27 | ("redis.client", "BasePipeline.execute"),
28 | ]
29 |
30 | def call(self, module, method, wrapped, instance, args, kwargs):
31 | wrapped_name = self.get_wrapped_name(wrapped, instance, method)
32 | with trace(wrapped_name, "cache.redis", leaf=True):
33 | return wrapped(*args, **kwargs)
34 |
--------------------------------------------------------------------------------
/opbeat/utils/opbeat_json.py:
--------------------------------------------------------------------------------
1 | """
2 | opbeat.utils.json
3 | ~~~~~~~~~~~~~~~~~~~~~~~~
4 |
5 | :copyright: (c) 2011-2012 Opbeat
6 |
7 | Large portions are
8 | :copyright: (c) 2010 by the Sentry Team, see AUTHORS for more details.
9 | :license: BSD, see LICENSE for more details.
10 | """
11 |
12 | import datetime
13 | import uuid
14 |
15 | try:
16 | import json
17 | except ImportError:
18 | import simplejson as json
19 |
20 |
21 | class BetterJSONEncoder(json.JSONEncoder):
22 | ENCODERS = {
23 | set: list,
24 | frozenset: list,
25 | datetime.datetime: lambda obj: obj.strftime('%Y-%m-%dT%H:%M:%S.%fZ'),
26 | uuid.UUID: lambda obj: obj.hex,
27 | bytes: lambda obj: obj.decode('utf-8', errors='replace')
28 | }
29 |
30 | def default(self, obj):
31 | if type(obj) in self.ENCODERS:
32 | return self.ENCODERS[type(obj)](obj)
33 | return super(BetterJSONEncoder, self).default(obj)
34 |
35 |
36 | def better_decoder(data):
37 | return data
38 |
39 |
40 | def dumps(value, **kwargs):
41 | return json.dumps(value, cls=BetterJSONEncoder, **kwargs)
42 |
43 |
44 | def loads(value, **kwargs):
45 | return json.loads(value, object_hook=better_decoder)
46 |
--------------------------------------------------------------------------------
/opbeat/instrumentation/packages/sqlite.py:
--------------------------------------------------------------------------------
1 | from opbeat.instrumentation.packages.dbapi2 import (ConnectionProxy,
2 | CursorProxy,
3 | DbApi2Instrumentation,
4 | extract_signature)
5 | from opbeat.traces import trace
6 |
7 |
8 | class SQLiteCursorProxy(CursorProxy):
9 | provider_name = 'sqlite'
10 |
11 | def extract_signature(self, sql):
12 | return extract_signature(sql)
13 |
14 |
15 | class SQLiteConnectionProxy(ConnectionProxy):
16 | cursor_proxy = SQLiteCursorProxy
17 |
18 |
19 | class SQLiteInstrumentation(DbApi2Instrumentation):
20 | name = 'sqlite'
21 |
22 | instrument_list = [
23 | ("sqlite3", "connect"),
24 | ("sqlite3.dbapi2", "connect"),
25 | ("pysqlite2.dbapi2", "connect"),
26 | ]
27 |
28 | def call(self, module, method, wrapped, instance, args, kwargs):
29 | signature = ".".join([module, method])
30 |
31 | if len(args) == 1:
32 | signature += " " + str(args[0])
33 |
34 | with trace(signature, "db.sqlite.connect"):
35 | return SQLiteConnectionProxy(wrapped(*args, **kwargs))
36 |
--------------------------------------------------------------------------------
/opbeat/instrumentation/packages/requests.py:
--------------------------------------------------------------------------------
1 | from opbeat.instrumentation.packages.base import AbstractInstrumentedModule
2 | from opbeat.traces import trace
3 | from opbeat.utils import default_ports
4 | from opbeat.utils.compat import urlparse
5 |
6 |
7 | def get_host_from_url(url):
8 | parsed_url = urlparse.urlparse(url)
9 | host = parsed_url.hostname or " "
10 |
11 | if (
12 | parsed_url.port and
13 | default_ports.get(parsed_url.scheme) != parsed_url.port
14 | ):
15 | host += ":" + str(parsed_url.port)
16 |
17 | return host
18 |
19 |
20 | class RequestsInstrumentation(AbstractInstrumentedModule):
21 | name = 'requests'
22 |
23 | instrument_list = [
24 | ("requests.sessions", "Session.send"),
25 | ]
26 |
27 | def call(self, module, method, wrapped, instance, args, kwargs):
28 | if 'request' in kwargs:
29 | request = kwargs['request']
30 | else:
31 | request = args[0]
32 |
33 | signature = request.method.upper()
34 | signature += " " + get_host_from_url(request.url)
35 |
36 | with trace(signature, "ext.http.requests",
37 | {'url': request.url}, leaf=True):
38 | return wrapped(*args, **kwargs)
39 |
--------------------------------------------------------------------------------
/opbeat/contrib/pylons/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | opbeat.contrib.pylons
3 | ~~~~~~~~~~~~~~~~~~~~
4 |
5 | :copyright: (c) 2011-2012 Opbeat
6 |
7 | Large portions are
8 | :copyright: (c) 2010 by the Sentry Team, see AUTHORS for more details.
9 | :license: BSD, see LICENSE for more details.
10 | """
11 | from opbeat.base import Client
12 | from opbeat.middleware import Opbeat as Middleware
13 |
14 |
15 | def list_from_setting(config, setting):
16 | value = config.get(setting)
17 | if not value:
18 | return None
19 | return value.split()
20 |
21 |
22 | class Opbeat(Middleware):
23 | def __init__(self, app, config, client_cls=Client):
24 | client = client_cls(
25 | servers=list_from_setting(config, 'opbeat.servers'),
26 | timeout=config.get('opbeat.timeout'),
27 | name=config.get('opbeat.name'),
28 | organization_id=config.get('opbeat.organization_id'),
29 | app_id=config.get('opbeat.app_id'),
30 | secret_token=config.get('opbeat.secret_token'),
31 | include_paths=list_from_setting(config, 'opbeat.include_paths'),
32 | exclude_paths=list_from_setting(config, 'opbeat.exclude_paths'),
33 | )
34 | super(Opbeat, self).__init__(app, client)
35 |
--------------------------------------------------------------------------------
/opbeat/instrumentation/packages/python_memcached.py:
--------------------------------------------------------------------------------
1 | from opbeat.instrumentation.packages.base import AbstractInstrumentedModule
2 | from opbeat.traces import trace
3 |
4 |
5 | class PythonMemcachedInstrumentation(AbstractInstrumentedModule):
6 | name = 'python_memcached'
7 |
8 | method_list = [
9 | 'add',
10 | 'append',
11 | 'cas',
12 | 'decr',
13 | 'delete',
14 | 'delete_multi',
15 | 'disconnect_all',
16 | 'flush_all',
17 | 'get',
18 | 'get_multi',
19 | 'get_slabs',
20 | 'get_stats',
21 | 'gets',
22 | 'incr',
23 | 'prepend',
24 | 'replace',
25 | 'set',
26 | 'set_multi',
27 | 'touch'
28 | ]
29 | # Took out 'set_servers', 'reset_cas', 'debuglog', 'check_key' and
30 | # 'forget_dead_hosts' because they involve no communication.
31 |
32 | def get_instrument_list(self):
33 | return [("memcache", "Client." + method) for method in self.method_list]
34 |
35 | def call(self, module, method, wrapped, instance, args, kwargs):
36 | name = self.get_wrapped_name(wrapped, instance, method)
37 |
38 | with trace(name, "cache.memcached"):
39 | return wrapped(*args, **kwargs)
40 |
--------------------------------------------------------------------------------
/docs/config/zerorpc.rst:
--------------------------------------------------------------------------------
1 | Configuring ZeroRPC
2 | ===================
3 |
4 | .. csv-table::
5 | :class: page-info
6 |
7 | "Page updated: 23rd July 2013", ""
8 |
9 | Setup
10 | -----
11 |
12 | The ZeroRPC integration comes as middleware for ZeroRPC. The middleware can be
13 | configured like the original opbeat client (using keyword arguments) and
14 | registered into ZeroRPC's context manager
15 |
16 | .. code::
17 | :class: language-python
18 |
19 | import zerorpc
20 |
21 | from opbeat.contrib.zerorpc import OpbeatMiddleware
22 |
23 | opbeat = OpbeatMiddleware(organization_id='', ...)
24 | zerorpc.Context.get_instance().register_middleware(opbeat)
25 |
26 | By default, the middleware will hide internal frames from ZeroRPC when it
27 | submits exceptions to Opbeat. This behavior can be disabled by passing the
28 | ``hide_zerorpc_frames`` parameter to the middleware
29 |
30 | .. code::
31 | :class: language-python
32 |
33 | opbeat = OpbeatMiddleware(hide_zerorpc_frames=False, organization_id='', ...)
34 |
35 | Caveats
36 | -------
37 |
38 | Sending an exception to Opbeat will basically block your RPC call.
39 | In any cases, a clean and long term solution would be to make opbeat requests
40 | to the Opbeat server asynchronous.
41 |
--------------------------------------------------------------------------------
/opbeat/transport/base.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | from opbeat.transport.exceptions import InvalidScheme
4 |
5 |
6 | class TransportException(Exception):
7 | def __init__(self, message, data=None, print_trace=True):
8 | super(TransportException, self).__init__(message)
9 | self.data = data
10 | self.print_trace = print_trace
11 |
12 |
13 | class Transport(object):
14 | """
15 | All transport implementations need to subclass this class
16 |
17 | You must implement a send method..
18 | """
19 | async_mode = False
20 | scheme = []
21 |
22 | def check_scheme(self, url):
23 | if url.scheme not in self.scheme:
24 | raise InvalidScheme()
25 |
26 | def send(self, data, headers):
27 | """
28 | You need to override this to do something with the actual
29 | data. Usually - this is sending to a server
30 | """
31 | raise NotImplementedError
32 |
33 | def close(self):
34 | """
35 | Cleans up resources and closes connection
36 | :return:
37 | """
38 | pass
39 |
40 |
41 | class AsyncTransport(Transport):
42 | async_mode = True
43 |
44 | def send_async(self, data, headers, success_callback=None, fail_callback=None):
45 | raise NotImplementedError
46 |
--------------------------------------------------------------------------------
/tests/instrumentation/botocore_tests.py:
--------------------------------------------------------------------------------
1 | import boto3
2 | import mock
3 |
4 | import opbeat
5 | import opbeat.instrumentation.control
6 | from opbeat.traces import trace
7 | from tests.helpers import get_tempstoreclient
8 | from tests.utils.compat import TestCase
9 |
10 |
11 | class InstrumentBotocoreTest(TestCase):
12 | def setUp(self):
13 | self.client = get_tempstoreclient()
14 | opbeat.instrumentation.control.instrument()
15 |
16 | @mock.patch("botocore.endpoint.Endpoint.make_request")
17 | def test_botocore_instrumentation(self, mock_make_request):
18 | mock_response = mock.Mock()
19 | mock_response.status_code = 200
20 | mock_make_request.return_value = (mock_response, {})
21 |
22 | self.client.begin_transaction("transaction.test")
23 | with trace("test_pipeline", "test"):
24 | session = boto3.Session(aws_access_key_id='foo',
25 | aws_secret_access_key='bar',
26 | region_name='us-west-2')
27 | ec2 = session.client('ec2')
28 | ec2.describe_instances()
29 | self.client.end_transaction("MyView")
30 |
31 | _, traces = self.client.instrumentation_store.get_all()
32 | self.assertIn('ec2:DescribeInstances', map(lambda x: x['signature'], traces))
33 |
--------------------------------------------------------------------------------
/opbeat/utils/wrapt/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2013, Graham Dumpleton
2 | All rights reserved.
3 |
4 | Redistribution and use in source and binary forms, with or without
5 | modification, are permitted provided that the following conditions are met:
6 |
7 | * Redistributions of source code must retain the above copyright notice, this
8 | list of conditions and the following disclaimer.
9 |
10 | * Redistributions in binary form must reproduce the above copyright notice,
11 | this list of conditions and the following disclaimer in the documentation
12 | and/or other materials provided with the distribution.
13 |
14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
15 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
16 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
17 | ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
18 | LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
19 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
20 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
21 | INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
22 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
23 | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
24 | POSSIBILITY OF SUCH DAMAGE.
25 |
--------------------------------------------------------------------------------
/README.rst:
--------------------------------------------------------------------------------
1 | Opbeat for Python
2 | =================
3 |
4 | .. image:: https://api.travis-ci.org/opbeat/opbeat_python.svg?branch=master
5 | :target: https://travis-ci.org/opbeat/opbeat_python
6 | :alt: Build Status
7 |
8 | .. image:: https://img.shields.io/pypi/v/opbeat.svg?style=flat
9 | :target: https://pypi.python.org/pypi/opbeat/
10 | :alt: Latest Version
11 |
12 | .. image:: https://img.shields.io/pypi/pyversions/opbeat.svg?style=flat
13 | :target: https://pypi.python.org/pypi/opbeat/
14 | :alt: Supported Python versions
15 |
16 |
17 | This is the official Python module for `Opbeat `_.
18 |
19 | It provides full out-of-the-box support for many of the popular frameworks,
20 | including Django, and Flask. Opbeat also includes drop-in support for any
21 | WSGI-compatible web application.
22 |
23 | Your application doesn't live on the web? No problem! Opbeat is easy to use in
24 | any Python application.
25 |
26 |
27 | Documentation
28 | -------------
29 |
30 | * `Documentation overview `_
31 | * `Get started with Django `_
32 | * `Get started with Flask `_
33 | * `Get started with a custom Python stack `_
34 |
35 |
36 | License
37 | -------
38 |
39 | BSD-3-Clause
40 |
41 |
42 | Made with ♥️ and ☕️ by Opbeat and our community.
43 |
--------------------------------------------------------------------------------
/opbeat/instrumentation/packages/django/template.py:
--------------------------------------------------------------------------------
1 | from opbeat.instrumentation.packages.base import AbstractInstrumentedModule
2 | from opbeat.traces import trace
3 |
4 |
5 | class DjangoTemplateInstrumentation(AbstractInstrumentedModule):
6 | name = 'django_template'
7 |
8 | instrument_list = [
9 | ("django.template", "Template.render"),
10 | ]
11 |
12 | def call(self, module, method, wrapped, instance, args, kwargs):
13 | name = getattr(instance, 'name', None)
14 |
15 | if not name:
16 | name = ''
17 | with trace(name, "template.django"):
18 | return wrapped(*args, **kwargs)
19 |
20 |
21 | class DjangoTemplateSourceInstrumentation(AbstractInstrumentedModule):
22 | name = 'django_template_source'
23 | instrument_list = [
24 | ('django.template.base', 'Parser.extend_nodelist')
25 | ]
26 |
27 | def call(self, module, method, wrapped, instance, args, kwargs):
28 | ret = wrapped(*args, **kwargs)
29 |
30 | if len(args) > 1:
31 | node = args[1]
32 | elif 'node' in kwargs:
33 | node = kwargs['node']
34 | else:
35 | return ret
36 |
37 | if len(args) > 2:
38 | token = args[2]
39 | elif 'token' in kwargs:
40 | token = kwargs['token']
41 | else:
42 | return ret
43 |
44 | if not hasattr(node, 'token') and hasattr(token, 'lineno'):
45 | node.token = token
46 |
47 | return ret
48 |
--------------------------------------------------------------------------------
/opbeat/conf/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | opbeat.conf
3 | ~~~~~~~~~~
4 |
5 | :copyright: (c) 2011-2012 Opbeat
6 |
7 | Large portions are
8 | :copyright: (c) 2010 by the Sentry Team, see AUTHORS for more details.
9 | :license: BSD, see LICENSE for more details.
10 | """
11 |
12 | import logging
13 |
14 | __all__ = ('setup_logging', )
15 |
16 |
17 | def setup_logging(handler, exclude=['opbeat',
18 | 'gunicorn',
19 | 'south',
20 | 'opbeat.errors']):
21 | """
22 | Configures logging to pipe to Opbeat.
23 |
24 | - ``exclude`` is a list of loggers that shouldn't go to Opbeat.
25 |
26 | For a typical Python install:
27 |
28 | >>> from opbeat.handlers.logging import OpbeatHandler
29 | >>> client = Opbeat(...)
30 | >>> setup_logging(OpbeatHandler(client))
31 |
32 | Within Django:
33 |
34 | >>> from opbeat.contrib.django.logging import OpbeatHandler
35 | >>> setup_logging(OpbeatHandler())
36 |
37 | Returns a boolean based on if logging was configured or not.
38 | """
39 | logger = logging.getLogger()
40 | if handler.__class__ in map(type, logger.handlers):
41 | return False
42 |
43 | logger.addHandler(handler)
44 |
45 | # Add StreamHandler to sentry's default so you can catch missed exceptions
46 | for logger_name in exclude:
47 | logger = logging.getLogger(logger_name)
48 | logger.propagate = False
49 | logger.addHandler(logging.StreamHandler())
50 |
51 | return True
52 |
--------------------------------------------------------------------------------
/opbeat/contrib/celery/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | opbeat.contrib.celery
3 | ~~~~~~~~~~~~~~~~~~~~
4 |
5 | :copyright: (c) 2011-2012 Opbeat
6 |
7 | Large portions are
8 | :copyright: (c) 2010 by the Sentry Team, see AUTHORS for more details.
9 | :license: BSD, see LICENSE for more details.
10 | """
11 | try:
12 | from celery.task import task
13 | except ImportError:
14 | from celery.decorators import task
15 | from celery import signals
16 |
17 | from opbeat.base import Client
18 |
19 |
20 | class CeleryMixin(object):
21 | def send_encoded(self, *args, **kwargs):
22 | "Errors through celery"
23 | self.send_raw.delay(*args, **kwargs)
24 |
25 | @task(routing_key='opbeat')
26 | def send_raw(self, *args, **kwargs):
27 | return super(CeleryMixin, self).send_encoded(*args, **kwargs)
28 |
29 |
30 | class CeleryClient(CeleryMixin, Client):
31 | pass
32 |
33 |
34 | class CeleryFilter(object):
35 | def filter(self, record):
36 | if record.funcName in ('_log_error',):
37 | return 0
38 | else:
39 | return 1
40 |
41 |
42 | def register_signal(client):
43 | def process_failure_signal(sender, task_id, exception, args, kwargs,
44 | traceback, einfo, **kw):
45 | client.capture_exception(
46 | extra={
47 | 'task_id': task_id,
48 | 'task': sender,
49 | 'args': args,
50 | 'kwargs': kwargs,
51 | })
52 | signals.task_failure.connect(process_failure_signal, weak=False)
53 |
--------------------------------------------------------------------------------
/tests/contrib/django/testapp/urls.py:
--------------------------------------------------------------------------------
1 | from __future__ import absolute_import
2 |
3 | import django
4 | from django.conf import settings
5 | from django.conf.urls import url
6 | from django.http import HttpResponse
7 |
8 | from tests.contrib.django.testapp import views
9 |
10 |
11 | def handler500(request):
12 | if getattr(settings, 'BREAK_THAT_500', False):
13 | raise ValueError('handler500')
14 | return HttpResponse('')
15 |
16 |
17 | urlpatterns = (
18 | url(r'^render-heavy-template$', views.render_template_view, name='render-heavy-template'),
19 | url(r'^render-user-template$', views.render_user_view, name='render-user-template'),
20 | url(r'^no-error$', views.no_error, name='opbeat-no-error'),
21 | url(r'^no-error-slash/$', views.no_error, name='opbeat-no-error-slash'),
22 | url(r'^fake-login$', views.fake_login, name='opbeat-fake-login'),
23 | url(r'^trigger-500$', views.raise_exc, name='opbeat-raise-exc'),
24 | url(r'^trigger-500-ioerror$', views.raise_ioerror, name='opbeat-raise-ioerror'),
25 | url(r'^trigger-500-decorated$', views.decorated_raise_exc, name='opbeat-raise-exc-decor'),
26 | url(r'^trigger-500-django$', views.django_exc, name='opbeat-django-exc'),
27 | url(r'^trigger-500-template$', views.template_exc, name='opbeat-template-exc'),
28 | url(r'^trigger-500-log-request$', views.logging_request_exc, name='opbeat-log-request-exc'),
29 | )
30 |
31 |
32 | if django.VERSION >= (1, 8):
33 | urlpatterns += url(r'^render-jinja2-template$', views.render_jinja2_template,
34 | name='render-jinja2-template'),
35 |
--------------------------------------------------------------------------------
/tests/instrumentation/dbapi2_tests.py:
--------------------------------------------------------------------------------
1 | from opbeat.instrumentation.packages.dbapi2 import Literal, scan, tokenize
2 |
3 |
4 | def test_scan_simple():
5 | sql = "Hello 'Peter Pan' at Disney World"
6 | tokens = tokenize(sql)
7 | actual = [t[1] for t in scan(tokens)]
8 | expected = ["Hello", Literal("'", "Peter Pan"), "at", "Disney", "World"]
9 | assert actual == expected
10 |
11 |
12 | def test_scan_with_escape_single_quote():
13 | sql = "Hello 'Peter\\' Pan' at Disney World"
14 | tokens = tokenize(sql)
15 | actual = [t[1] for t in scan(tokens)]
16 | expected = ["Hello", Literal("'", "Peter' Pan"), "at", "Disney", "World"]
17 | assert actual == expected
18 |
19 |
20 | def test_scan_with_escape_slash():
21 | sql = "Hello 'Peter Pan\\\\' at Disney World"
22 | tokens = tokenize(sql)
23 | actual = [t[1] for t in scan(tokens)]
24 | expected = ["Hello", Literal("'", "Peter Pan\\"), "at", "Disney", "World"]
25 | assert actual == expected
26 |
27 |
28 | def test_scan_double_quotes():
29 | sql = """Hello 'Peter'' Pan''' at Disney World"""
30 | tokens = tokenize(sql)
31 | actual = [t[1] for t in scan(tokens)]
32 | expected = ["Hello", Literal("'", "Peter' Pan'"), "at", "Disney", "World"]
33 | assert actual == expected
34 |
35 |
36 | def test_scan_double_quotes_at_end():
37 | sql = """Hello Peter Pan at Disney 'World'"""
38 | tokens = tokenize(sql)
39 | actual = [t[1] for t in scan(tokens)]
40 | expected = ["Hello", "Peter", "Pan", "at", "Disney", Literal("'", "World")]
41 | assert actual == expected
42 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | opbeat is copyright (c) Opbeat, David Cramer and individual contributers..
2 |
3 | opbeat is forked from Raven by David Cramer:
4 | Copyright (c) 2009 David Cramer and individual contributors.
5 | All rights reserved.
6 |
7 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
8 |
9 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
10 |
11 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
12 |
13 | 3. Neither the name of the opbeat nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
14 |
15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
--------------------------------------------------------------------------------
/tests/utils/stacks/tests.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import absolute_import
3 |
4 | from mock import Mock
5 |
6 | from opbeat.utils import six
7 | from opbeat.utils.stacks import get_culprit, get_stack_info
8 | from tests.utils.compat import TestCase
9 |
10 |
11 | class Context(object):
12 | def __init__(self, dict):
13 | self.dict = dict
14 |
15 | __getitem__ = lambda s, *a: s.dict.__getitem__(*a)
16 | __setitem__ = lambda s, *a: s.dict.__setitem__(*a)
17 | iterkeys = lambda s, *a: six.iterkeys(s.dict, *a)
18 |
19 |
20 | class StackTest(TestCase):
21 | def test_get_culprit_bad_module(self):
22 | culprit = get_culprit([{
23 | 'module': None,
24 | 'function': 'foo',
25 | }])
26 | self.assertEquals(culprit, '.foo')
27 |
28 | culprit = get_culprit([{
29 | 'module': 'foo',
30 | 'function': None,
31 | }])
32 | self.assertEquals(culprit, 'foo.')
33 |
34 | culprit = get_culprit([{
35 | }])
36 | self.assertEquals(culprit, '.')
37 |
38 | def test_bad_locals_in_frame(self):
39 | frame = Mock()
40 | frame.f_locals = Context({
41 | 'foo': 'bar',
42 | 'biz': 'baz',
43 | })
44 | frame.f_lineno = 1
45 | frame.f_globals = {}
46 | frame.f_code.co_filename = __file__.replace('.pyc', '.py')
47 | frame.f_code.co_name = __name__
48 | frames = [(frame, 1)]
49 | results = get_stack_info(frames)
50 | self.assertEquals(len(results), 1)
51 | result = results[0]
52 | self.assertTrue('vars' in result)
53 | vars = {
54 | "foo": "bar",
55 | "biz": "baz",
56 | }
57 | self.assertEquals(result['vars'], vars)
58 |
--------------------------------------------------------------------------------
/opbeat/middleware.py:
--------------------------------------------------------------------------------
1 | """
2 | opbeat.middleware
3 | ~~~~~~~~~~~~~~~~~~~~~~~~
4 |
5 | :copyright: (c) 2011-2012 Opbeat
6 |
7 | Large portions are
8 | :copyright: (c) 2010 by the Sentry Team, see AUTHORS for more details.
9 | :license: BSD, see LICENSE for more details.
10 | """
11 |
12 | import sys
13 |
14 | from opbeat.utils.wsgi import get_current_url, get_environ, get_headers
15 |
16 |
17 | class Opbeat(object):
18 | """
19 | A WSGI middleware which will attempt to capture any
20 | uncaught exceptions and send them to Opbeat.
21 |
22 | >>> from opbeat.base import Client
23 | >>> application = Opbeat(application, Client())
24 | """
25 | def __init__(self, application, client):
26 | self.application = application
27 | self.client = client
28 |
29 | def __call__(self, environ, start_response):
30 | try:
31 | for event in self.application(environ, start_response):
32 | yield event
33 | except Exception:
34 | exc_info = sys.exc_info()
35 | self.handle_exception(exc_info, environ)
36 | exc_info = None
37 | raise
38 |
39 | def handle_exception(self, exc_info, environ):
40 | event_id = self.client.capture(
41 | 'Exception',
42 | exc_info=exc_info,
43 | data={
44 | 'http': {
45 | 'method': environ.get('REQUEST_METHOD'),
46 | 'url': get_current_url(environ, strip_querystring=True),
47 | 'query_string': environ.get('QUERY_STRING'),
48 | # TODO
49 | # 'data': environ.get('wsgi.input'),
50 | 'headers': dict(get_headers(environ)),
51 | 'env': dict(get_environ(environ)),
52 | }
53 | }
54 | )
55 | return event_id
56 |
--------------------------------------------------------------------------------
/tests/contrib/celery/tests.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import absolute_import
3 |
4 | import mock
5 | import pytest
6 |
7 | from opbeat.contrib.celery import CeleryClient
8 | from tests.utils.compat import TestCase
9 |
10 | try:
11 | from celery.tests.utils import with_eager_tasks
12 | has_with_eager_tasks = True
13 | except ImportError:
14 | from opbeat.utils.compat import noop_decorator as with_eager_tasks
15 | has_with_eager_tasks = False
16 |
17 |
18 | class ClientTest(TestCase):
19 | def setUp(self):
20 | self.client = CeleryClient(
21 | organization_id='organization_id',
22 | app_id='app_id',
23 | secret_token='secret'
24 | )
25 |
26 | @mock.patch('opbeat.contrib.celery.CeleryClient.send_raw')
27 | def test_send_encoded(self, send_raw):
28 | self.client.send_encoded('foo')
29 |
30 | send_raw.delay.assert_called_once_with('foo')
31 |
32 | @mock.patch('opbeat.contrib.celery.CeleryClient.send_raw')
33 | def test_without_eager(self, send_raw):
34 | """
35 | Integration test to ensure it propagates all the way down
36 | and calls delay on the task.
37 | """
38 | self.client.capture('Message', message='test')
39 |
40 | self.assertEquals(send_raw.delay.call_count, 1)
41 |
42 | @pytest.mark.skipif(not has_with_eager_tasks,
43 | reason='with_eager_tasks is not available')
44 | @with_eager_tasks
45 | @mock.patch('opbeat.base.Client.send_encoded')
46 | def test_with_eager(self, send_encoded):
47 | """
48 | Integration test to ensure it propagates all the way down
49 | and calls the parent client's send_encoded method.
50 | """
51 | self.client.capture('Message', message='test')
52 |
53 | self.assertEquals(send_encoded.call_count, 1)
54 |
--------------------------------------------------------------------------------
/opbeat/instrumentation/register.py:
--------------------------------------------------------------------------------
1 | from opbeat.utils.module_import import import_string
2 |
3 | _cls_register = set([
4 | 'opbeat.instrumentation.packages.botocore.BotocoreInstrumentation',
5 | 'opbeat.instrumentation.packages.jinja2.Jinja2Instrumentation',
6 | 'opbeat.instrumentation.packages.psycopg2.Psycopg2Instrumentation',
7 | 'opbeat.instrumentation.packages.psycopg2.Psycopg2RegisterTypeInstrumentation',
8 | 'opbeat.instrumentation.packages.mysql.MySQLInstrumentation',
9 | 'opbeat.instrumentation.packages.pylibmc.PyLibMcInstrumentation',
10 | 'opbeat.instrumentation.packages.pymongo.PyMongoInstrumentation',
11 | 'opbeat.instrumentation.packages.pymongo.PyMongoBulkInstrumentation',
12 | 'opbeat.instrumentation.packages.pymongo.PyMongoCursorInstrumentation',
13 | 'opbeat.instrumentation.packages.python_memcached.PythonMemcachedInstrumentation',
14 | 'opbeat.instrumentation.packages.redis.RedisInstrumentation',
15 | 'opbeat.instrumentation.packages.redis.RedisPipelineInstrumentation',
16 | 'opbeat.instrumentation.packages.requests.RequestsInstrumentation',
17 | 'opbeat.instrumentation.packages.sqlite.SQLiteInstrumentation',
18 | 'opbeat.instrumentation.packages.urllib3.Urllib3Instrumentation',
19 |
20 | 'opbeat.instrumentation.packages.django.template.DjangoTemplateInstrumentation',
21 | 'opbeat.instrumentation.packages.django.template.DjangoTemplateSourceInstrumentation',
22 | ])
23 |
24 |
25 | def register(cls):
26 | _cls_register.add(cls)
27 |
28 | _instrumentation_singletons = {}
29 |
30 |
31 | def get_instrumentation_objects():
32 | for cls_str in _cls_register:
33 | if cls_str not in _instrumentation_singletons:
34 | cls = import_string(cls_str)
35 | _instrumentation_singletons[cls_str] = cls()
36 |
37 | obj = _instrumentation_singletons[cls_str]
38 | yield obj
39 |
--------------------------------------------------------------------------------
/docs/config/pylons.rst:
--------------------------------------------------------------------------------
1 | Configuring Pylons
2 | ==================
3 |
4 | .. csv-table::
5 | :class: page-info
6 |
7 | "Page updated: 23rd July 2013", ""
8 |
9 | WSGI Middleware
10 | ---------------
11 |
12 | A Pylons-specific middleware exists to enable easy configuration from settings:
13 |
14 | .. code::
15 |
16 | from opbeat.contrib.pylons import Opbeat
17 |
18 | application = Opbeat(application, config)
19 |
20 | Configuration is handled via the opbeat namespace:
21 |
22 | .. code::
23 | :class: language-ini
24 |
25 | [opbeat]
26 | organization_id=
27 | app_id=
28 | secret_token=
29 | include_paths=my.package,my.other.package,
30 | exclude_paths=my.package.crud
31 |
32 |
33 | Logger setup
34 | ------------
35 |
36 | Add the following lines to your project's `.ini` file to setup `OpbeatHandler`:
37 |
38 | .. code::
39 | :class: language-ini
40 |
41 | [loggers]
42 | keys = root, opbeat
43 |
44 | [handlers]
45 | keys = console, opbeat
46 |
47 | [formatters]
48 | keys = generic
49 |
50 | [logger_root]
51 | level = INFO
52 | handlers = console, opbeat
53 |
54 | [logger_opbeat]
55 | level = WARN
56 | handlers = console
57 | qualname = opbeat.errors
58 | propagate = 0
59 |
60 | [handler_console]
61 | class = StreamHandler
62 | args = (sys.stderr,)
63 | level = NOTSET
64 | formatter = generic
65 |
66 | [handler_opbeat]
67 | class = opbeat.handlers.logging.OpbeatHandler
68 | args = ('', '', '')
69 | level = NOTSET
70 | formatter = generic
71 |
72 | [formatter_generic]
73 | format = %(asctime)s,%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s
74 | datefmt = %H:%M:%S
75 |
76 | .. note::
77 |
78 | You may want to setup other loggers as well.
79 |
80 |
81 |
--------------------------------------------------------------------------------
/tests/middleware/tests.py:
--------------------------------------------------------------------------------
1 | from __future__ import absolute_import
2 |
3 | import webob
4 |
5 | from opbeat.middleware import Opbeat
6 | from tests.utils.compat import TestCase
7 |
8 | from ..helpers import get_tempstoreclient
9 |
10 |
11 | def example_app(environ, start_response):
12 | raise ValueError('hello world')
13 |
14 |
15 | class MiddlewareTest(TestCase):
16 | def setUp(self):
17 | self.app = example_app
18 |
19 | def test_error_handler(self):
20 | client = get_tempstoreclient()
21 | middleware = Opbeat(self.app, client=client)
22 |
23 | request = webob.Request.blank('/an-error?foo=bar')
24 | response = middleware(request.environ, lambda *args: None)
25 |
26 | with self.assertRaises(ValueError):
27 | list(response)
28 |
29 | self.assertEquals(len(client.events), 1)
30 | event = client.events.pop(0)
31 |
32 | self.assertTrue('exception' in event)
33 | exc = event['exception']
34 | self.assertEquals(exc['type'], 'ValueError')
35 | self.assertEquals(exc['value'], 'hello world')
36 | self.assertEquals(event['level'], "error")
37 | self.assertEquals(event['message'], 'ValueError: hello world')
38 |
39 | self.assertTrue('http' in event)
40 | http = event['http']
41 | self.assertEquals(http['url'], 'http://localhost/an-error')
42 | self.assertEquals(http['query_string'], 'foo=bar')
43 | self.assertEquals(http['method'], 'GET')
44 | headers = http['headers']
45 | self.assertTrue('Host' in headers, headers.keys())
46 | self.assertEquals(headers['Host'], 'localhost:80')
47 | env = http['env']
48 | self.assertTrue('SERVER_NAME' in env, env.keys())
49 | self.assertEquals(env['SERVER_NAME'], 'localhost')
50 | self.assertTrue('SERVER_PORT' in env, env.keys())
51 | self.assertEquals(env['SERVER_PORT'], '80')
52 |
--------------------------------------------------------------------------------
/docs/contributing/index.rst:
--------------------------------------------------------------------------------
1 | Contributing
2 | ============
3 |
4 | Want to contribute back to Opbeat? This page describes the general development flow,
5 | our philosophy, the test suite, and issue tracking.
6 |
7 | (Though it actually doesn't describe all of that, yet)
8 |
9 | Setting up an Environment
10 | -------------------------
11 |
12 | Opbeat is designed to run off of setuptools with minimal work. Because of this
13 | setting up a development environment for Opbeat requires only a few steps.
14 |
15 | The first thing you're going to want to do, is build a virtualenv and install
16 | any base dependancies.
17 |
18 | .. code-block:: bash
19 |
20 | virtualenv ~/.virtualenvs/opbeat
21 | source ~/.virtualenvs/opbeat/bin/activate
22 | python setup.py develop
23 |
24 | Running the Test Suite
25 | ----------------------
26 |
27 | The test suite is also powered off of setuptools, and can be run in two fashions. The
28 | easiest is to simply use setuptools and it's ``test`` command. This will handle installing
29 | any dependancies you're missing automatically.
30 |
31 | .. code-block:: bash
32 |
33 | python setup.py test
34 |
35 | If you've already installed the dependencies, or don't care about certain tests which will
36 | be skipped without them, you can also run tests in a more verbose way.
37 |
38 | .. code-block:: bash
39 |
40 | python runtests.py
41 |
42 | The ``runtests.py`` command has several options, and if you're familiar w/ Django you should feel
43 | right at home.
44 |
45 | .. code-block:: bash
46 |
47 | # Stop immediately on a failure
48 | python runtests.py --failfast
49 |
50 |
51 | Contributing Back Code
52 | ----------------------
53 |
54 | Ideally all patches should be sent as a pull request on GitHub, and include tests. If you're fixing a bug or making a large change the patch **must** include test coverage.
55 |
56 | You can see a list of open pull requests (pending changes) by visiting https://github.com/opbeat/opbeat_python/pulls
57 |
58 |
--------------------------------------------------------------------------------
/tests/contrib/django/testapp/views.py:
--------------------------------------------------------------------------------
1 | from __future__ import absolute_import
2 |
3 | import logging
4 |
5 | from django.contrib.auth.models import User
6 | from django.http import HttpResponse
7 | from django.shortcuts import get_object_or_404, render, render_to_response
8 |
9 | from opbeat.traces import trace
10 |
11 |
12 | def no_error(request):
13 | return HttpResponse('')
14 |
15 |
16 | def fake_login(request):
17 | return HttpResponse('')
18 |
19 |
20 | def django_exc(request):
21 | return get_object_or_404(Exception, pk=1)
22 |
23 |
24 | def raise_exc(request):
25 | raise Exception(request.GET.get('message', 'view exception'))
26 |
27 |
28 | def raise_ioerror(request):
29 | raise IOError(request.GET.get('message', 'view exception'))
30 |
31 |
32 | def decorated_raise_exc(request):
33 | return raise_exc(request)
34 |
35 |
36 | def template_exc(request):
37 | return render_to_response('error.html')
38 |
39 |
40 | def logging_request_exc(request):
41 | logger = logging.getLogger(__name__)
42 | try:
43 | raise Exception(request.GET.get('message', 'view exception'))
44 | except Exception as e:
45 | logger.error(e, exc_info=True, extra={'request': request})
46 | return HttpResponse('')
47 |
48 |
49 | def render_template_view(request):
50 | def something_expensive():
51 | with trace("something_expensive", "code"):
52 | return [User(username='Ron'), User(username='Beni')]
53 |
54 | return render(request, "list_users.html",
55 | {'users': something_expensive})
56 |
57 |
58 | def render_jinja2_template(request):
59 | return render(request, "jinja2_template.html")
60 |
61 |
62 | def render_user_view(request):
63 | def something_expensive():
64 | with trace("something_expensive", "code"):
65 | for i in range(100):
66 | users = list(User.objects.all())
67 | return users
68 |
69 | return render(request, "list_users.html",
70 | {'users': something_expensive})
71 |
--------------------------------------------------------------------------------
/tests/contrib/zerorpc/zeropc_tests.py:
--------------------------------------------------------------------------------
1 | import os
2 | import random
3 | import shutil
4 | import sys
5 | import tempfile
6 |
7 | import pytest
8 |
9 | from opbeat.contrib.zerorpc import OpbeatMiddleware
10 | from tests.helpers import get_tempstoreclient
11 | from tests.utils.compat import TestCase
12 |
13 | zerorpc = pytest.importorskip("zerorpc")
14 | gevent = pytest.importorskip("gevent")
15 |
16 |
17 |
18 | has_unsupported_pypy = (hasattr(sys, 'pypy_version_info')
19 | and sys.pypy_version_info < (2, 6))
20 |
21 |
22 | class ZeroRPCTest(TestCase):
23 | def setUp(self):
24 | self._socket_dir = tempfile.mkdtemp(prefix='opbeatzerorpcunittest')
25 | self._server_endpoint = 'ipc://{0}'.format(os.path.join(
26 | self._socket_dir, 'random_zeroserver'
27 | ))
28 |
29 | self._opbeat = get_tempstoreclient()
30 | zerorpc.Context.get_instance().register_middleware(OpbeatMiddleware(
31 | client=self._opbeat
32 | ))
33 |
34 | @pytest.mark.skipif(has_unsupported_pypy, reason='Failure with pypy < 2.6')
35 | def test_zerorpc_middleware_with_reqrep(self):
36 | self._server = zerorpc.Server(random)
37 | self._server.bind(self._server_endpoint)
38 | gevent.spawn(self._server.run)
39 |
40 | self._client = zerorpc.Client()
41 | self._client.connect(self._server_endpoint)
42 |
43 | try:
44 | self._client.choice([])
45 | except zerorpc.exceptions.RemoteError as ex:
46 | self.assertEqual(ex.name, 'IndexError')
47 | self.assertEqual(len(self._opbeat.events), 1)
48 | exc = self._opbeat.events[0]['exception']
49 | self.assertEqual(exc['type'], 'IndexError')
50 | frames = self._opbeat.events[0]['stacktrace']['frames']
51 | self.assertEqual(frames[0]['function'], 'choice')
52 | self.assertEqual(frames[0]['module'], 'random')
53 | return
54 | self.fail('An IndexError exception should have been raised an catched')
55 |
56 | def tearDown(self):
57 | self._client.close()
58 | self._server.close()
59 | shutil.rmtree(self._socket_dir, ignore_errors=True)
60 |
--------------------------------------------------------------------------------
/tests/instrumentation/python_memcached_tests.py:
--------------------------------------------------------------------------------
1 | import memcache
2 | import mock
3 |
4 | import opbeat
5 | import opbeat.instrumentation.control
6 | from opbeat.traces import trace
7 | from tests.helpers import get_tempstoreclient
8 | from tests.utils.compat import TestCase
9 |
10 |
11 | class InstrumentMemcachedTest(TestCase):
12 | def setUp(self):
13 | self.client = get_tempstoreclient()
14 | opbeat.instrumentation.control.instrument()
15 |
16 | @mock.patch("opbeat.traces.RequestsStore.should_collect")
17 | def test_memcached(self, should_collect):
18 | should_collect.return_value = False
19 | self.client.begin_transaction("transaction.test")
20 | with trace("test_memcached", "test"):
21 | conn = memcache.Client(['127.0.0.1:11211'], debug=0)
22 | conn.set("mykey", "a")
23 | assert "a" == conn.get("mykey")
24 | assert {"mykey": "a"} == conn.get_multi(["mykey", "myotherkey"])
25 | self.client.end_transaction("BillingView")
26 |
27 | transactions, traces = self.client.instrumentation_store.get_all()
28 |
29 | expected_signatures = ['transaction', 'test_memcached',
30 | 'Client.set', 'Client.get',
31 | 'Client.get_multi']
32 |
33 | self.assertEqual(set([t['signature'] for t in traces]),
34 | set(expected_signatures))
35 |
36 | # Reorder according to the kinds list so we can just test them
37 | sig_dict = dict([(t['signature'], t) for t in traces])
38 | traces = [sig_dict[k] for k in expected_signatures]
39 |
40 | self.assertEqual(traces[0]['signature'], 'transaction')
41 | self.assertEqual(traces[0]['kind'], 'transaction')
42 | self.assertEqual(traces[0]['transaction'], 'BillingView')
43 |
44 | self.assertEqual(traces[1]['signature'], 'test_memcached')
45 | self.assertEqual(traces[1]['kind'], 'test')
46 | self.assertEqual(traces[1]['transaction'], 'BillingView')
47 |
48 | self.assertEqual(traces[2]['signature'], 'Client.set')
49 | self.assertEqual(traces[2]['kind'], 'cache.memcached')
50 | self.assertEqual(traces[2]['transaction'], 'BillingView')
51 |
52 | self.assertEqual(len(traces), 5)
53 |
--------------------------------------------------------------------------------
/docs/config/pyramid.rst:
--------------------------------------------------------------------------------
1 | Configuring Pyramid
2 | ===================
3 |
4 | .. csv-table::
5 | :class: page-info
6 |
7 | "Page updated: 23rd July 2013", ""
8 |
9 | PasteDeploy Filter
10 | ------------------
11 |
12 | A filter factory for `PasteDeploy `_ exists to allow easily inserting opbeat into a WSGI pipeline:
13 |
14 | .. code::
15 | :class: language-ini
16 |
17 | [pipeline:main]
18 | pipeline =
19 | opbeat
20 | tm
21 | MyApp
22 |
23 | [filter:opbeat]
24 | use = egg:opbeat#paste_filter
25 | dsn = http://public:secret@example.com/1
26 | include_paths = my.package, my.other.package
27 | exclude_paths = my.package.crud
28 |
29 | In the ``[filter:opbeat]`` section, you must specify the entry-point for opbeat with the ``use =`` key. All other opbeat client parameters can be included in this section as well.
30 |
31 | See the `Pyramid PasteDeploy Configuration Documentation `_ for more information.
32 |
33 | Logger setup
34 | ------------
35 |
36 | Add the following lines to your project's `.ini` file to setup `OpbeatHandler`:
37 |
38 | .. code::
39 | :class: language-ini
40 |
41 | [loggers]
42 | keys = root, opbeat
43 |
44 | [handlers]
45 | keys = console, opbeat
46 |
47 | [formatters]
48 | keys = generic
49 |
50 | [logger_root]
51 | level = INFO
52 | handlers = console, opbeat
53 |
54 | [logger_opbeat]
55 | level = WARN
56 | handlers = console
57 | qualname = opbeat.errors
58 | propagate = 0
59 |
60 | [handler_console]
61 | class = StreamHandler
62 | args = (sys.stderr,)
63 | level = NOTSET
64 | formatter = generic
65 |
66 | [handler_opbeat]
67 | class = opbeat.handlers.logging.OpbeatHandler
68 | args = ('http://public:secret@example.com/1',)
69 | level = WARNING
70 | formatter = generic
71 |
72 | [formatter_generic]
73 | format = %(asctime)s,%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s
74 | datefmt = %H:%M:%S
75 |
76 | .. note::
77 |
78 | You may want to setup other loggers as well. See the `Pyramid Logging Documentation `_ for more information.
79 |
80 |
81 |
--------------------------------------------------------------------------------
/opbeat/utils/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | opbeat.utils
3 | ~~~~~~~~~~~~~~~~~~~
4 |
5 | :copyright: (c) 2011-2015 Opbeat
6 |
7 | Large portions are
8 | :copyright: (c) 2010 by the Sentry Team, see AUTHORS for more details.
9 | :license: BSD, see LICENSE for more details.
10 | """
11 | import os
12 |
13 | from opbeat.utils import six
14 |
15 |
16 | default_ports = {
17 | "https": 433,
18 | "http": 80,
19 | "postgresql": 5432
20 | }
21 |
22 |
23 | def varmap(func, var, context=None, name=None):
24 | """
25 | Executes ``func(key_name, value)`` on all values
26 | recurisively discovering dict and list scoped
27 | values.
28 | """
29 | if context is None:
30 | context = {}
31 | objid = id(var)
32 | if objid in context:
33 | return func(name, '<...>')
34 | context[objid] = 1
35 | if isinstance(var, dict):
36 | ret = dict((k, varmap(func, v, context, k)) for k, v in six.iteritems(var))
37 | elif isinstance(var, (list, tuple)):
38 | ret = [varmap(func, f, context, name) for f in var]
39 | else:
40 | ret = func(name, var)
41 | del context[objid]
42 | return ret
43 |
44 |
45 | def disabled_due_to_debug(opbeat_config, debug):
46 | """
47 | Compares module and app configs to determine whether to log to Opbeat
48 | :param opbeat_config: Dictionary containing module config
49 | :param debug: Boolean denoting app DEBUG state
50 | :return: Boolean True if logging is disabled
51 | """
52 | return debug and not opbeat_config.get('DEBUG', False)
53 |
54 |
55 | def get_name_from_func(func):
56 | # If no view was set we ignore the request
57 | module = func.__module__
58 |
59 | if hasattr(func, '__name__'):
60 | view_name = func.__name__
61 | else: # Fall back if there's no __name__
62 | view_name = func.__class__.__name__
63 |
64 | return '{0}.{1}'.format(module, view_name)
65 |
66 |
67 | def build_name_with_http_method_prefix(name, request):
68 | if name:
69 | return request.method + " " + name
70 | else:
71 | return name # 404
72 |
73 |
74 | def is_master_process():
75 | # currently only recognizes uwsgi master process
76 | try:
77 | import uwsgi
78 | return os.getpid() == uwsgi.masterpid()
79 | except ImportError:
80 | return False
81 |
--------------------------------------------------------------------------------
/tests/transports/test_http.py:
--------------------------------------------------------------------------------
1 | import socket
2 |
3 | import mock
4 | import pytest
5 |
6 | from opbeat.transport.base import TransportException
7 | from opbeat.transport.http import HTTPTransport
8 | from opbeat.utils import six
9 | from opbeat.utils.compat import HTTPError, urlparse
10 | from tests.utils.compat import TestCase
11 |
12 |
13 | class TestHttpFailures(TestCase):
14 | @mock.patch('opbeat.transport.http.urlopen')
15 | def test_send(self, mock_urlopen):
16 | transport = HTTPTransport(urlparse.urlparse('http://localhost:9999'))
17 | mock_response = mock.Mock(
18 | info=lambda: {'Location': 'http://example.com/foo'}
19 | )
20 | mock_urlopen.return_value = mock_response
21 | url = transport.send('x', {})
22 | assert url == 'http://example.com/foo'
23 | assert mock_response.close.call_count == 1
24 |
25 | @mock.patch('opbeat.transport.http.urlopen')
26 | def test_timeout(self, mock_urlopen):
27 | transport = HTTPTransport(urlparse.urlparse('http://localhost:9999'))
28 | mock_urlopen.side_effect = socket.timeout()
29 | with pytest.raises(TransportException) as exc_info:
30 | transport.send('x', {})
31 | assert 'timeout' in str(exc_info.value)
32 |
33 | @mock.patch('opbeat.transport.http.urlopen')
34 | def test_http_error(self, mock_urlopen):
35 | url, status, message, body = (
36 | 'http://localhost:9999', 418, "I'm a teapot", 'Nothing'
37 | )
38 | transport = HTTPTransport(urlparse.urlparse(url))
39 | mock_urlopen.side_effect = HTTPError(
40 | url, status, message, hdrs={}, fp=six.StringIO(body)
41 | )
42 | with pytest.raises(TransportException) as exc_info:
43 | transport.send('x', {})
44 | for val in (url, status, message, body):
45 | assert str(val) in str(exc_info.value)
46 |
47 | @mock.patch('opbeat.transport.http.urlopen')
48 | def test_generic_error(self, mock_urlopen):
49 | url, status, message, body = (
50 | 'http://localhost:9999', 418, "I'm a teapot", 'Nothing'
51 | )
52 | transport = HTTPTransport(urlparse.urlparse(url))
53 | mock_urlopen.side_effect = Exception('Oopsie')
54 | with pytest.raises(TransportException) as exc_info:
55 | transport.send('x', {})
56 | assert 'Oopsie' in str(exc_info.value)
57 |
--------------------------------------------------------------------------------
/opbeat/transport/asyncio.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 |
3 | import aiohttp
4 |
5 | from .base import TransportException
6 | from .http import HTTPTransport
7 |
8 |
9 | class AsyncioHTTPTransport(HTTPTransport):
10 | """
11 | HTTP Transport ready for asyncio
12 | """
13 |
14 | def __init__(self, parsed_url):
15 | self.check_scheme(parsed_url)
16 |
17 | self._parsed_url = parsed_url
18 | self._url = parsed_url.geturl()
19 | loop = asyncio.get_event_loop()
20 | self.client = aiohttp.ClientSession(loop=loop)
21 |
22 | async def send(self, data, headers, timeout=None):
23 | """Use synchronous interface, because this is a coroutine."""
24 |
25 | if timeout is None:
26 | timeout = defaults.TIMEOUT
27 | try:
28 | with aiohttp.Timeout(timeout):
29 | async with self.client.post(self._url,
30 | data=data,
31 | headers=headers) as response:
32 | assert response.status == 202
33 | except asyncio.TimeoutError as e:
34 | print_trace = True
35 | message = ("Connection to Opbeat server timed out "
36 | "(url: %s, timeout: %d seconds)" % (self._url, timeout))
37 | raise TransportException(message, data,
38 | print_trace=print_trace) from e
39 | except AssertionError as e:
40 | print_trace = True
41 | body = await response.read()
42 | if response.status == 429:
43 | message = 'Temporarily rate limited: '
44 | print_trace = False
45 | else:
46 | message = 'Unable to reach Opbeat server: '
47 | message += '%s (url: %s, body: %s)' % (e, self._url, body)
48 | raise TransportException(message, data,
49 | print_trace=print_trace) from e
50 | except Exception as e:
51 | print_trace = True
52 | message = 'Unable to reach Opbeat server: %s (url: %s)' % (
53 | e, self._url)
54 | raise TransportException(message, data,
55 | print_trace=print_trace) from e
56 | else:
57 | return response.headers.get('Location')
58 |
59 | def __del__(self):
60 | self.client.close()
61 |
--------------------------------------------------------------------------------
/docs/config/flask.rst:
--------------------------------------------------------------------------------
1 | Configuring Flask
2 | =================================
3 |
4 | .. csv-table::
5 | :class: page-info
6 |
7 | "Page updated: 4th April 2013", ""
8 |
9 | Setup
10 | -----
11 |
12 | Using Opbeat with Flask requires the blinker library to be installed. This is most easily installed using pip:
13 |
14 | .. code::
15 | :class: language-bash
16 |
17 | $ pip install blinker
18 |
19 | The first thing you'll need to do is to initialize opbeat under your application:
20 |
21 | .. code::
22 | :class: language-python
23 |
24 | from opbeat.contrib.flask import Opbeat
25 | opbeat = Opbeat(app, organization_id='', app_id='', secret_token='')
26 |
27 | If you don't specify the ``organization_id``, ``app_id`` and ``secret_token`` values, we will attempt to read it from your environment under the ``OPBEAT_ORGANIZATION_ID``, ``OPBEAT_APP_ID`` and ``OPBEAT_SECRET_TOKEN`` keys respectively.
28 |
29 | Building applications on the fly? You can use opbeat's ``init_app`` hook:
30 |
31 | .. code::
32 | :class: language-python
33 |
34 | opbeat = Opbeat(organization_id='', app_id='', secret_token='')
35 |
36 | def create_app():
37 | app = Flask(__name__)
38 | opbeat.init_app(app)
39 | return app
40 |
41 | Settings
42 | --------
43 |
44 | Additional settings for the client can be configured using ``OPBEAT`` in your application's configuration:
45 |
46 | .. code::
47 | :class: lang-python
48 |
49 | class MyConfig(object):
50 | OPBEAT = {
51 | 'ORGANIZATION_ID': '',
52 | 'APP_ID': '',
53 | 'SECRET_TOKEN': '',
54 | 'INCLUDE_PATHS': ['myproject']
55 | }
56 |
57 | Usage
58 | -----
59 |
60 | Once you've configured the Opbeat application it will automatically capture uncaught exceptions within Flask. If you want to send additional events, a couple of shortcuts are provided on the Opbeat Flask middleware object.
61 |
62 | Capture an arbitrary exception by calling ``captureException``:
63 |
64 | .. code::
65 | :class: lang-python
66 |
67 | >>> try:
68 | >>> 1 / 0
69 | >>> except ZeroDivisionError:
70 | >>> opbeat.captureException()
71 |
72 | Log a generic message with ``captureMessage``:
73 |
74 | .. code::
75 | :class: lang-python
76 |
77 | >>> opbeat.captureMessage('hello, world!')
78 |
--------------------------------------------------------------------------------
/opbeat/conf/defaults.py:
--------------------------------------------------------------------------------
1 | """
2 | opbeat.conf.defaults
3 | ~~~~~~~~~~~~~~~~~~~
4 |
5 | Represents the default values for all Opbeat settings.
6 |
7 | :copyright: (c) 2011-2012 Opbeat
8 |
9 | Large portions are
10 | :copyright: (c) 2010 by the Sentry Team, see AUTHORS for more details.
11 | :license: BSD, see LICENSE for more details.
12 | """
13 |
14 | import os
15 | import os.path
16 | import socket
17 |
18 | ROOT = os.path.normpath(os.path.join(os.path.dirname(__file__), os.pardir))
19 |
20 | # Allow local testing of Opbeat even if DEBUG is enabled
21 | DEBUG = False
22 |
23 | # This should be the schema+host of the Opbeat server
24 | SERVERS = ['https://intake.opbeat.com']
25 |
26 | # Error API path
27 | ERROR_API_PATH = '/api/v1/organizations/{0}/apps/{1}/errors/'
28 |
29 | # Transactions API path
30 | TRANSACTIONS_API_PATH = '/api/v1/organizations/{0}/apps/{1}/transactions/'
31 |
32 | TIMEOUT = 20
33 |
34 | # TODO: this is specific to Django
35 | CLIENT = 'opbeat.contrib.django.DjangoClient'
36 |
37 | HOSTNAME = socket.gethostname()
38 |
39 | # Credentials to authenticate with the Opbeat server
40 | ACCESS_TOKEN = None
41 |
42 | # Extending this allow you to ignore module prefixes when we attempt to
43 | # discover which function an error comes from (typically a view)
44 | EXCLUDE_PATHS = []
45 |
46 | # By default Opbeat only looks at modules in INSTALLED_APPS for drilling down
47 | # where an exception is located
48 | INCLUDE_PATHS = []
49 |
50 | # The maximum number of elements to store for a list-like structure.
51 | MAX_LENGTH_LIST = 50
52 |
53 | # The maximum length to store of a string-like structure.
54 | MAX_LENGTH_STRING = 400
55 |
56 | MAX_LENGTH_VALUES = {
57 | 'message': 200,
58 | 'server_name': 200,
59 | 'culprit': 100,
60 | 'logger': 60
61 | }
62 |
63 | # Automatically log frame stacks from all ``logging`` messages.
64 | AUTO_LOG_STACKS = False
65 |
66 | # Client-side data processors to apply
67 | PROCESSORS = (
68 | 'opbeat.processors.SanitizePasswordsProcessor',
69 | )
70 |
71 | # How often we send data to the metrics backend
72 | TRACES_SEND_FREQ_SECS = 60
73 |
74 | # Should data be sent to Opbeat asynchronously in a separate thread
75 | ASYNC_MODE = True
76 |
77 | # Should opbeat wrap middleware for better metrics detection
78 | INSTRUMENT_DJANGO_MIDDLEWARE = True
79 |
80 | SYNC_TRANSPORT_CLASS = 'opbeat.transport.http_urllib3.Urllib3Transport'
81 |
82 | ASYNC_TRANSPORT_CLASS = 'opbeat.transport.http_urllib3.AsyncUrllib3Transport'
83 |
--------------------------------------------------------------------------------
/tests/instrumentation/jinja2_tests/jinja2_tests.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | import mock
4 | from jinja2 import Environment, FileSystemLoader
5 | from jinja2.environment import Template
6 |
7 | import opbeat.instrumentation.control
8 | from tests.helpers import get_tempstoreclient
9 | from tests.utils.compat import TestCase
10 |
11 |
12 | class InstrumentJinja2Test(TestCase):
13 | def setUp(self):
14 | self.client = get_tempstoreclient()
15 | filedir = os.path.dirname(__file__)
16 | loader = FileSystemLoader(filedir)
17 | self.env = Environment(loader=loader)
18 | opbeat.instrumentation.control.instrument()
19 |
20 | @mock.patch("opbeat.traces.RequestsStore.should_collect")
21 | def test_from_file(self, should_collect):
22 | should_collect.return_value = False
23 | self.client.begin_transaction("transaction.test")
24 | template = self.env.get_template('mytemplate.html')
25 | template.render()
26 | self.client.end_transaction("MyView")
27 |
28 | transactions, traces = self.client.instrumentation_store.get_all()
29 |
30 | expected_signatures = ['transaction', 'mytemplate.html']
31 |
32 | self.assertEqual(set([t['signature'] for t in traces]),
33 | set(expected_signatures))
34 |
35 | # Reorder according to the kinds list so we can just test them
36 | sig_dict = dict([(t['signature'], t) for t in traces])
37 | traces = [sig_dict[k] for k in expected_signatures]
38 |
39 | self.assertEqual(traces[1]['signature'], 'mytemplate.html')
40 | self.assertEqual(traces[1]['kind'], 'template.jinja2')
41 | self.assertEqual(traces[1]['transaction'], 'MyView')
42 |
43 | def test_from_string(self):
44 | self.client.begin_transaction("transaction.test")
45 | template = Template("']
52 |
53 | self.assertEqual(set([t['signature'] for t in traces]),
54 | set(expected_signatures))
55 |
56 | # Reorder according to the kinds list so we can just test them
57 | sig_dict = dict([(t['signature'], t) for t in traces])
58 | traces = [sig_dict[k] for k in expected_signatures]
59 |
60 | self.assertEqual(traces[1]['signature'], '')
61 | self.assertEqual(traces[1]['kind'], 'template.jinja2')
62 | self.assertEqual(traces[1]['transaction'], 'test')
63 |
--------------------------------------------------------------------------------
/tests/instrumentation/sqlite_tests.py:
--------------------------------------------------------------------------------
1 | import sqlite3
2 |
3 | import mock
4 |
5 | import opbeat.instrumentation.control
6 | from tests.helpers import get_tempstoreclient
7 | from tests.utils.compat import TestCase
8 |
9 |
10 | class InstrumentSQLiteTest(TestCase):
11 | def setUp(self):
12 | self.client = get_tempstoreclient()
13 | opbeat.instrumentation.control.instrument()
14 |
15 | @mock.patch("opbeat.traces.RequestsStore.should_collect")
16 | def test_connect(self, should_collect):
17 | should_collect.return_value = False
18 | self.client.begin_transaction("transaction.test")
19 |
20 | conn = sqlite3.connect(":memory:")
21 | cursor = conn.cursor()
22 |
23 | cursor.execute("""CREATE TABLE testdb (id integer, username text)""")
24 | cursor.execute("""INSERT INTO testdb VALUES (1, "Ron")""")
25 | cursor.execute("""DROP TABLE testdb""")
26 |
27 | self.client.end_transaction("MyView")
28 |
29 | transactions, traces = self.client.instrumentation_store.get_all()
30 |
31 | expected_signatures = ['transaction', 'sqlite3.connect :memory:',
32 | 'CREATE TABLE', 'INSERT INTO testdb',
33 | 'DROP TABLE']
34 |
35 | self.assertEqual(set([t['signature'] for t in traces]),
36 | set(expected_signatures))
37 |
38 | # Reorder according to the kinds list so we can just test them
39 | sig_dict = dict([(t['signature'], t) for t in traces])
40 | traces = [sig_dict[k] for k in expected_signatures]
41 |
42 | self.assertEqual(traces[0]['signature'], 'transaction')
43 | self.assertEqual(traces[0]['kind'], 'transaction')
44 | self.assertEqual(traces[0]['transaction'], 'MyView')
45 |
46 | self.assertEqual(traces[1]['signature'], 'sqlite3.connect :memory:')
47 | self.assertEqual(traces[1]['kind'], 'db.sqlite.connect')
48 | self.assertEqual(traces[1]['transaction'], 'MyView')
49 |
50 | self.assertEqual(traces[2]['signature'], 'CREATE TABLE')
51 | self.assertEqual(traces[2]['kind'], 'db.sqlite.sql')
52 | self.assertEqual(traces[2]['transaction'], 'MyView')
53 |
54 | self.assertEqual(traces[3]['signature'], 'INSERT INTO testdb')
55 | self.assertEqual(traces[3]['kind'], 'db.sqlite.sql')
56 | self.assertEqual(traces[3]['transaction'], 'MyView')
57 |
58 | self.assertEqual(traces[4]['signature'], 'DROP TABLE')
59 | self.assertEqual(traces[4]['kind'], 'db.sqlite.sql')
60 | self.assertEqual(traces[4]['transaction'], 'MyView')
61 |
62 | self.assertEqual(len(traces), 5)
63 |
--------------------------------------------------------------------------------
/opbeat/contrib/zerorpc/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | opbeat.contrib.zerorpc
3 | ~~~~~~~~~~~~~~~~~~~~~
4 |
5 | :copyright: (c) 2012 by the Sentry Team, see AUTHORS for more details.
6 | :license: BSD, see LICENSE for more details.
7 | """
8 |
9 | import inspect
10 |
11 | from opbeat.base import Client
12 |
13 |
14 | class OpbeatMiddleware(object):
15 | """Opbeat/opbeat middleware for ZeroRPC.
16 |
17 | >>> opbeat = OpbeatMiddleware(app_id='..', secret_token='...')
18 | >>> zerorpc.Context.get_instance().register_middleware(opbeat)
19 |
20 | Exceptions detected server-side in ZeroRPC will be submitted to Opbeat (and
21 | propagated to the client as well).
22 | """
23 |
24 | def __init__(self, hide_zerorpc_frames=True, client=None, **kwargs):
25 | """Create a middleware object that can be injected in a ZeroRPC server.
26 |
27 | - hide_zerorpc_frames: modify the exception stacktrace to remove the
28 | internal zerorpc frames (True by default to make
29 | the stacktrace as readable as possible);
30 | - client: use an existing raven.Client object, otherwise one will be
31 | instantiated from the keyword arguments.
32 |
33 | """
34 | self._opbeat_client = client or Client(**kwargs)
35 | self._hide_zerorpc_frames = hide_zerorpc_frames
36 |
37 | def server_inspect_exception(self, req_event, rep_event, task_ctx, exc_info):
38 | """Called when an exception has been raised in the code run by ZeroRPC"""
39 |
40 | # Hide the zerorpc internal frames for readability, for a REQ/REP or
41 | # REQ/STREAM server the frames to hide are:
42 | # - core.ServerBase._async_task
43 | # - core.Pattern*.process_call
44 | # - core.DecoratorBase.__call__
45 | #
46 | # For a PUSH/PULL or PUB/SUB server the frame to hide is:
47 | # - core.Puller._receiver
48 | if self._hide_zerorpc_frames:
49 | traceback = exc_info[2]
50 | while traceback:
51 | zerorpc_frame = traceback.tb_frame
52 | zerorpc_frame.f_locals['__traceback_hide__'] = True
53 | frame_info = inspect.getframeinfo(zerorpc_frame)
54 | # Is there a better way than this (or looking up the filenames
55 | # or hardcoding the number of frames to skip) to know when we
56 | # are out of zerorpc?
57 | if frame_info.function == '__call__' \
58 | or frame_info.function == '_receiver':
59 | break
60 | traceback = traceback.tb_next
61 |
62 | self._opbeat_client.capture_exception(
63 | exc_info,
64 | extra=task_ctx
65 | )
66 |
--------------------------------------------------------------------------------
/opbeat/instrumentation/packages/psycopg2.py:
--------------------------------------------------------------------------------
1 | from opbeat.instrumentation.packages.dbapi2 import (ConnectionProxy,
2 | CursorProxy,
3 | DbApi2Instrumentation,
4 | extract_signature)
5 | from opbeat.traces import trace
6 | from opbeat.utils import default_ports
7 |
8 |
9 | class PGCursorProxy(CursorProxy):
10 | provider_name = 'postgresql'
11 |
12 | def _bake_sql(self, sql):
13 | # if this is a Composable object, use its `as_string` method
14 | # see http://initd.org/psycopg/docs/sql.html
15 | if hasattr(sql, 'as_string'):
16 | return sql.as_string(self.__wrapped__)
17 | return sql
18 |
19 | def extract_signature(self, sql):
20 | return extract_signature(sql)
21 |
22 |
23 | class PGConnectionProxy(ConnectionProxy):
24 | cursor_proxy = PGCursorProxy
25 |
26 |
27 | class Psycopg2Instrumentation(DbApi2Instrumentation):
28 | name = 'psycopg2'
29 |
30 | instrument_list = [
31 | ("psycopg2", "connect")
32 | ]
33 |
34 | def call(self, module, method, wrapped, instance, args, kwargs):
35 | signature = "psycopg2.connect"
36 |
37 | host = kwargs.get('host')
38 | if host:
39 | signature += " " + str(host)
40 |
41 | port = kwargs.get('port')
42 | if port:
43 | port = str(port)
44 | if int(port) != default_ports.get("postgresql"):
45 | signature += ":" + port
46 | else:
47 | # Parse connection string and extract host/port
48 | pass
49 |
50 | with trace(signature, "db.postgreql.connect"):
51 | return PGConnectionProxy(wrapped(*args, **kwargs))
52 |
53 |
54 | class Psycopg2RegisterTypeInstrumentation(DbApi2Instrumentation):
55 | name = 'psycopg2-register-type'
56 |
57 | instrument_list = [
58 | ("psycopg2.extensions", "register_type"),
59 | # specifically instrument `register_json` as it bypasses `register_type`
60 | ("psycopg2._json", "register_json"),
61 | ]
62 |
63 | def call(self, module, method, wrapped, instance, args, kwargs):
64 | if ('conn_or_curs' in kwargs and
65 | hasattr(kwargs['conn_or_curs'], "__wrapped__")):
66 | kwargs['conn_or_curs'] = kwargs['conn_or_curs'].__wrapped__
67 | # register_type takes the connection as second argument
68 | elif len(args) == 2 and hasattr(args[1], "__wrapped__"):
69 | args = (args[0], args[1].__wrapped__)
70 | # register_json takes the connection as first argument, and can have
71 | # several more arguments
72 | elif method == 'register_json':
73 | if args and hasattr(args[0], "__wrapped__"):
74 | args = (args[0].__wrapped__,) + args[1:]
75 |
76 | return wrapped(*args, **kwargs)
77 |
--------------------------------------------------------------------------------
/tests/asyncio/test_asyncio_http.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | import sys
3 | from urllib.parse import urlparse
4 |
5 | import pytest
6 |
7 | pytestmark = pytest.mark.skipif(sys.version_info < (3, 5),
8 | reason='python3.5+ requried for asyncio')
9 |
10 | @pytest.fixture
11 | def mock_client(mocker):
12 | mock_client = mocker.MagicMock()
13 | mock_client.timeout = None
14 | response = mocker.MagicMock()
15 |
16 | async def read():
17 | return mock_client.body
18 |
19 | response.read = read
20 |
21 |
22 | class fake_post:
23 | async def __aenter__(self, *args, **kwargs):
24 | response.status = mock_client.status
25 | response.headers = mock_client.headers
26 | if mock_client.timeout:
27 | await asyncio.sleep(mock_client.timeout)
28 | return response
29 |
30 | async def __aexit__(self, *args):
31 | pass
32 |
33 | def __init__(self, *args, **kwargs):
34 | mock_client.args = args
35 | mock_client.kwargs = kwargs
36 |
37 |
38 | mock_client.post = mocker.Mock(side_effect=fake_post)
39 | return mock_client
40 |
41 |
42 | @pytest.mark.asyncio
43 | async def test_send(mock_client):
44 | from opbeat.transport.asyncio import AsyncioHTTPTransport
45 | transport = AsyncioHTTPTransport(urlparse('http://localhost:9999'))
46 |
47 | mock_client.status = 202
48 | mock_client.headers = {'Location': 'http://example.com/foo'}
49 | transport.client = mock_client
50 |
51 | url = await transport.send(b'data', {'a': 'b'}, timeout=2)
52 | assert url == 'http://example.com/foo'
53 | assert mock_client.args == ('http://localhost:9999',)
54 | assert mock_client.kwargs == {'headers': {'a': 'b'},
55 | 'data': b'data'}
56 |
57 |
58 | @pytest.mark.asyncio
59 | async def test_send_not_found(mock_client):
60 | from opbeat.transport.asyncio import AsyncioHTTPTransport
61 | from opbeat.transport.base import TransportException
62 |
63 | transport = AsyncioHTTPTransport(urlparse('http://localhost:9999'))
64 |
65 | mock_client.status = 404
66 | mock_client.headers = {}
67 | mock_client.body = b'Not Found'
68 | transport.client = mock_client
69 |
70 | with pytest.raises(TransportException) as excinfo:
71 | await transport.send(b'data', {}, timeout=2)
72 | assert 'Not Found' in str(excinfo.value)
73 | assert excinfo.value.data == b'data'
74 |
75 |
76 | @pytest.mark.asyncio
77 | async def test_send_timeout(mock_client):
78 | from opbeat.transport.asyncio import AsyncioHTTPTransport
79 | from opbeat.transport.base import TransportException
80 |
81 | transport = AsyncioHTTPTransport(urlparse('http://localhost:9999'))
82 |
83 | mock_client.timeout = 0.1
84 | transport.client = mock_client
85 |
86 | with pytest.raises(TransportException) as excinfo:
87 | await transport.send(b'data', {}, timeout=0.0001)
88 | assert 'Connection to Opbeat server timed out' in str(excinfo.value)
89 |
--------------------------------------------------------------------------------
/tests/instrumentation/urllib3_tests.py:
--------------------------------------------------------------------------------
1 | import random
2 | import threading
3 |
4 | import mock
5 | import urllib3
6 |
7 | import opbeat
8 | import opbeat.instrumentation.control
9 | from opbeat.traces import trace
10 | from tests.helpers import get_tempstoreclient
11 | from tests.utils.compat import TestCase
12 |
13 | try:
14 | from http import server as SimpleHTTPServer
15 | from socketserver import TCPServer
16 | except ImportError:
17 | import SimpleHTTPServer
18 | from SocketServer import TCPServer
19 |
20 | class MyTCPServer(TCPServer):
21 | allow_reuse_address = True
22 |
23 | class InstrumentUrllib3Test(TestCase):
24 | def setUp(self):
25 | self.client = get_tempstoreclient()
26 | self.port = random.randint(50000, 60000)
27 | self.start_test_server()
28 | opbeat.instrumentation.control.instrument()
29 |
30 | def tearDown(self):
31 | if self.httpd:
32 | self.httpd.shutdown()
33 |
34 | def start_test_server(self):
35 | handler = SimpleHTTPServer.SimpleHTTPRequestHandler
36 |
37 | self.httpd = MyTCPServer(("", self.port), handler)
38 |
39 | self.httpd_thread = threading.Thread(target=self.httpd.serve_forever)
40 | self.httpd_thread.setDaemon(True)
41 | self.httpd_thread.start()
42 |
43 | @mock.patch("opbeat.traces.RequestsStore.should_collect")
44 | def test_urllib3(self, should_collect):
45 | should_collect.return_value = False
46 | self.client.begin_transaction("transaction")
47 | expected_sig = 'GET localhost:{0}'.format(self.port)
48 | with trace("test_pipeline", "test"):
49 | pool = urllib3.PoolManager(timeout=0.1)
50 |
51 | url = 'http://localhost:{0}/hello_world'.format(self.port)
52 | r = pool.request('GET', url)
53 |
54 | self.client.end_transaction("MyView")
55 |
56 | transactions, traces = self.client.instrumentation_store.get_all()
57 |
58 | expected_signatures = ['transaction', 'test_pipeline', expected_sig]
59 |
60 | self.assertEqual(set([t['signature'] for t in traces]),
61 | set(expected_signatures))
62 |
63 | # Reorder according to the kinds list so we can just test them
64 | sig_dict = dict([(t['signature'], t) for t in traces])
65 | traces = [sig_dict[k] for k in expected_signatures]
66 |
67 | self.assertEqual(len(traces), 3)
68 |
69 | self.assertEqual(traces[0]['signature'], 'transaction')
70 | self.assertEqual(traces[0]['kind'], 'transaction')
71 | self.assertEqual(traces[0]['transaction'], 'MyView')
72 |
73 | self.assertEqual(traces[1]['signature'], 'test_pipeline')
74 | self.assertEqual(traces[1]['kind'], 'test')
75 | self.assertEqual(traces[1]['transaction'], 'MyView')
76 |
77 | self.assertEqual(traces[2]['signature'], expected_sig)
78 | self.assertEqual(traces[2]['kind'], 'ext.http.urllib3')
79 | self.assertEqual(traces[2]['transaction'], 'MyView')
80 |
81 | self.assertEqual(traces[2]['extra']['url'], url)
82 |
--------------------------------------------------------------------------------
/opbeat/utils/lru.py:
--------------------------------------------------------------------------------
1 | """
2 | Backported LRU cache from Python 3.3
3 | http://code.activestate.com/recipes/578078-py26-and-py30-backport-of-python-33s-lru-cache/
4 |
5 | """
6 | from collections import namedtuple
7 | from threading import RLock
8 |
9 | _CacheInfo = namedtuple("CacheInfo", ["hits", "misses", "maxsize", "currsize"])
10 |
11 |
12 | class LRUCache(object):
13 | def __init__(self, maxsize=100):
14 | self.cache = dict()
15 | self.cache_get = self.cache.get # bound method to lookup key or return None
16 | self._len = len # localize the global len() function
17 | self.lock = RLock() # because linkedlist updates aren't threadsafe
18 | self.root = [] # root of the circular doubly linked list
19 | self.root[:] = [self.root, self.root, None] # initialize by pointing to self
20 | self.nonlocal_root = [self.root] # make updateable non-locally
21 | self.PREV, self.NEXT, self.KEY = 0, 1, 2 # names for the link fields
22 | self.maxsize = maxsize
23 |
24 | def has_key(self, key):
25 | with self.lock:
26 | link = self.cache_get(key)
27 | if link is not None:
28 | # record recent use of the key by moving it to the front of the list
29 | root, = self.nonlocal_root
30 | link_prev, link_next, key = link
31 | link_prev[self.NEXT] = link_next
32 | link_next[self.PREV] = link_prev
33 | last = root[self.PREV]
34 | last[self.NEXT] = root[self.PREV] = link
35 | link[self.PREV] = last
36 | link[self.NEXT] = root
37 |
38 | return True
39 | return False
40 |
41 | def set(self, key):
42 | with self.lock:
43 | root, = self.nonlocal_root
44 | if key in self.cache:
45 | # getting here means that this same key was added to the
46 | # cache while the lock was released. since the link
47 | # update is already done, we need only return the
48 | # computed result and update the count of misses.
49 | pass
50 | elif self._len(self.cache) >= self.maxsize:
51 | # use the old root to store the new key and result
52 | oldroot = root
53 | oldroot[self.KEY] = key
54 | # empty the oldest link and make it the new root
55 | root = self.nonlocal_root[0] = oldroot[self.NEXT]
56 | oldkey = root[self.KEY]
57 | root[self.KEY] = None
58 | # now update the cache dictionary for the new links
59 | del self.cache[oldkey]
60 | self.cache[key] = oldroot
61 | else:
62 | # put result in a new link at the front of the list
63 | last = root[self.PREV]
64 | link = [last, root, key]
65 | last[self.NEXT] = root[self.PREV] = self.cache[key] = link
66 |
--------------------------------------------------------------------------------
/opbeat/transport/http_urllib3.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | import os
3 |
4 | import certifi
5 | import urllib3
6 | from urllib3.exceptions import MaxRetryError, TimeoutError
7 |
8 | from opbeat.conf import defaults
9 | from opbeat.transport.base import TransportException
10 | from opbeat.transport.http import AsyncHTTPTransport, HTTPTransport
11 |
12 |
13 | class Urllib3Transport(HTTPTransport):
14 |
15 | scheme = ['http', 'https']
16 |
17 | def __init__(self, parsed_url):
18 | kwargs = {
19 | 'cert_reqs': 'CERT_REQUIRED',
20 | 'ca_certs': certifi.where(),
21 | 'block': True,
22 | }
23 | proxy_url = os.environ.get('HTTPS_PROXY', os.environ.get('HTTP_PROXY'))
24 | if proxy_url:
25 | self.http = urllib3.ProxyManager(proxy_url, **kwargs)
26 | else:
27 | self.http = urllib3.PoolManager(**kwargs)
28 | super(Urllib3Transport, self).__init__(parsed_url)
29 |
30 | def send(self, data, headers, timeout=None):
31 | if timeout is None:
32 | timeout = defaults.TIMEOUT
33 | response = None
34 | try:
35 | try:
36 | response = self.http.urlopen(
37 | 'POST', self._url, body=data, headers=headers, timeout=timeout
38 | )
39 | except Exception as e:
40 | print_trace = True
41 | if isinstance(e, MaxRetryError) and isinstance(e.reason, TimeoutError):
42 | message = (
43 | "Connection to Opbeat server timed out "
44 | "(url: %s, timeout: %d seconds)" % (self._url, timeout)
45 | )
46 | print_trace = False
47 | else:
48 | message = 'Unable to reach Opbeat server: %s (url: %s)' % (
49 | e, self._url
50 | )
51 | raise TransportException(message, data, print_trace=print_trace)
52 | body = response.read()
53 | if response.status >= 400:
54 | if response.status == 429: # rate-limited
55 | message = 'Temporarily rate limited: '
56 | print_trace = False
57 | else:
58 | message = 'HTTP %s: ' % response.status
59 | print_trace = True
60 | message += body.decode('utf8')
61 | raise TransportException(message, data, print_trace=print_trace)
62 | return response.getheader('Location')
63 | finally:
64 | if response:
65 | response.close()
66 |
67 |
68 | class AsyncUrllib3Transport(AsyncHTTPTransport, Urllib3Transport):
69 | scheme = ['http', 'https']
70 | async_mode = True
71 |
72 | def send_sync(self, data=None, headers=None, success_callback=None,
73 | fail_callback=None):
74 | try:
75 | url = Urllib3Transport.send(self, data, headers)
76 | if callable(success_callback):
77 | success_callback(url=url)
78 | except Exception as e:
79 | if callable(fail_callback):
80 | fail_callback(exception=e)
81 |
--------------------------------------------------------------------------------
/tests/utils/wsgi/tests.py:
--------------------------------------------------------------------------------
1 | from __future__ import absolute_import
2 |
3 | from opbeat.utils.wsgi import get_environ, get_headers, get_host
4 | from tests.utils.compat import TestCase
5 |
6 |
7 | class GetHeadersTest(TestCase):
8 | def test_tuple_as_key(self):
9 | result = dict(get_headers({
10 | ('a', 'tuple'): 'foo',
11 | }))
12 | self.assertEquals(result, {})
13 |
14 | def test_coerces_http_name(self):
15 | result = dict(get_headers({
16 | 'HTTP_ACCEPT': 'text/plain',
17 | }))
18 | self.assertIn('Accept', result)
19 | self.assertEquals(result['Accept'], 'text/plain')
20 |
21 | def test_coerces_content_type(self):
22 | result = dict(get_headers({
23 | 'CONTENT_TYPE': 'text/plain',
24 | }))
25 | self.assertIn('Content-Type', result)
26 | self.assertEquals(result['Content-Type'], 'text/plain')
27 |
28 | def test_coerces_content_length(self):
29 | result = dict(get_headers({
30 | 'CONTENT_LENGTH': '134',
31 | }))
32 | self.assertIn('Content-Length', result)
33 | self.assertEquals(result['Content-Length'], '134')
34 |
35 |
36 | class GetEnvironTest(TestCase):
37 | def test_has_remote_addr(self):
38 | result = dict(get_environ({'REMOTE_ADDR': '127.0.0.1'}))
39 | self.assertIn('REMOTE_ADDR', result)
40 | self.assertEquals(result['REMOTE_ADDR'], '127.0.0.1')
41 |
42 | def test_has_server_name(self):
43 | result = dict(get_environ({'SERVER_NAME': '127.0.0.1'}))
44 | self.assertIn('SERVER_NAME', result)
45 | self.assertEquals(result['SERVER_NAME'], '127.0.0.1')
46 |
47 | def test_has_server_port(self):
48 | result = dict(get_environ({'SERVER_PORT': 80}))
49 | self.assertIn('SERVER_PORT', result)
50 | self.assertEquals(result['SERVER_PORT'], 80)
51 |
52 | def test_hides_wsgi_input(self):
53 | result = list(get_environ({'wsgi.input': 'foo'}))
54 | self.assertNotIn('wsgi.input', result)
55 |
56 |
57 | class GetHostTest(TestCase):
58 | def test_http_x_forwarded_host(self):
59 | result = get_host({'HTTP_X_FORWARDED_HOST': 'example.com'})
60 | self.assertEquals(result, 'example.com')
61 |
62 | def test_http_host(self):
63 | result = get_host({'HTTP_HOST': 'example.com'})
64 | self.assertEquals(result, 'example.com')
65 |
66 | def test_http_strips_port(self):
67 | result = get_host({
68 | 'wsgi.url_scheme': 'http',
69 | 'SERVER_NAME': 'example.com',
70 | 'SERVER_PORT': '80',
71 | })
72 | self.assertEquals(result, 'example.com')
73 |
74 | def test_https_strips_port(self):
75 | result = get_host({
76 | 'wsgi.url_scheme': 'https',
77 | 'SERVER_NAME': 'example.com',
78 | 'SERVER_PORT': '443',
79 | })
80 | self.assertEquals(result, 'example.com')
81 |
82 | def test_http_nonstandard_port(self):
83 | result = get_host({
84 | 'wsgi.url_scheme': 'http',
85 | 'SERVER_NAME': 'example.com',
86 | 'SERVER_PORT': '81',
87 | })
88 | self.assertEquals(result, 'example.com:81')
89 |
--------------------------------------------------------------------------------
/tests/transports/test_urllib3.py:
--------------------------------------------------------------------------------
1 | import mock
2 | import pytest
3 | import urllib3.poolmanager
4 | from urllib3.exceptions import MaxRetryError, TimeoutError
5 | from urllib3_mock import Responses
6 |
7 | from opbeat.transport.base import TransportException
8 | from opbeat.transport.http_urllib3 import (AsyncUrllib3Transport,
9 | Urllib3Transport)
10 |
11 | try:
12 | import urlparse
13 | except ImportError:
14 | from urllib import parse as urlparse
15 |
16 |
17 |
18 | responses = Responses('urllib3')
19 |
20 | @responses.activate
21 | def test_send():
22 | transport = Urllib3Transport(urlparse.urlparse('http://localhost'))
23 | responses.add('POST', '/', status=202,
24 | adding_headers={'Location': 'http://example.com/foo'})
25 | url = transport.send('x', {})
26 | assert url == 'http://example.com/foo'
27 |
28 |
29 | @responses.activate
30 | def test_timeout():
31 | transport = Urllib3Transport(urlparse.urlparse('http://localhost'))
32 | responses.add('POST', '/', status=202,
33 | body=MaxRetryError(None, None, reason=TimeoutError()))
34 | with pytest.raises(TransportException) as exc_info:
35 | transport.send('x', {})
36 | assert 'timeout' in str(exc_info.value)
37 |
38 |
39 | @responses.activate
40 | def test_http_error():
41 | url, status, body = (
42 | 'http://localhost:9999', 418, 'Nothing'
43 | )
44 | transport = Urllib3Transport(urlparse.urlparse(url))
45 | responses.add('POST', '/', status=status, body=body)
46 |
47 | with pytest.raises(TransportException) as exc_info:
48 | transport.send('x', {})
49 | for val in (status, body):
50 | assert str(val) in str(exc_info.value)
51 |
52 |
53 | @responses.activate
54 | def test_generic_error():
55 | url, status, message, body = (
56 | 'http://localhost:9999', 418, "I'm a teapot", 'Nothing'
57 | )
58 | transport = Urllib3Transport(urlparse.urlparse(url))
59 | responses.add('POST', '/', status=status, body=Exception('Oopsie'))
60 | with pytest.raises(TransportException) as exc_info:
61 | transport.send('x', {})
62 | assert 'Oopsie' in str(exc_info.value)
63 |
64 |
65 | def test_http_proxy_environment_variable():
66 | with mock.patch.dict('os.environ', {'HTTP_PROXY': 'http://example.com'}):
67 | transport = Urllib3Transport(urlparse.urlparse('http://localhost:9999'))
68 | assert isinstance(transport.http, urllib3.ProxyManager)
69 |
70 |
71 | def test_https_proxy_environment_variable():
72 | with mock.patch.dict('os.environ', {'HTTPS_PROXY': 'https://example.com'}):
73 | transport = Urllib3Transport(urlparse.urlparse('http://localhost:9999'))
74 | assert isinstance(transport.http, urllib3.poolmanager.ProxyManager)
75 |
76 |
77 | def test_https_proxy_environment_variable_is_preferred():
78 | with mock.patch.dict('os.environ', {'HTTPS_PROXY': 'https://example.com',
79 | 'HTTP_PROXY': 'http://example.com'}):
80 | transport = Urllib3Transport(urlparse.urlparse('http://localhost:9999'))
81 | assert isinstance(transport.http, urllib3.poolmanager.ProxyManager)
82 | assert transport.http.proxy.scheme == 'https'
83 |
--------------------------------------------------------------------------------
/tests/asyncio/test_asyncio_client.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | import sys
3 | from urllib.parse import urlparse
4 |
5 | import mock
6 | import pytest
7 |
8 | pytestmark = pytest.mark.skipif(sys.version_info < (3, 5),
9 | reason='python3.5+ requried for asyncio')
10 |
11 |
12 | class MockTransport(mock.MagicMock):
13 |
14 | def __init__(self, url=None, *args, **kwargs):
15 | super().__init__(*args, **kwargs)
16 | self.url = url
17 |
18 | async def send(self, data, headers, timeout):
19 | from opbeat.transport.base import TransportException
20 | self.data = data
21 | if self.url == urlparse('http://error'):
22 | raise TransportException('', data, False)
23 | await asyncio.sleep(0.0001)
24 |
25 |
26 | @pytest.mark.asyncio
27 | async def test_client_success():
28 | from opbeat.contrib.asyncio import Client
29 |
30 | client = Client(
31 | servers=['http://localhost'],
32 | organization_id='organization_id',
33 | app_id='app_id',
34 | secret_token='secret',
35 | async_mode=False,
36 | transport_class='.'.join(
37 | (__name__, MockTransport.__name__)),
38 | )
39 | client.send(foo='bar')
40 | tasks = asyncio.Task.all_tasks()
41 | task = next(t for t in tasks if t is not asyncio.Task.current_task())
42 | await task
43 | assert client.state.status == 1
44 | transport = client._get_transport(urlparse('http://localhost'))
45 | assert transport.data == client.encode({'foo': 'bar'})
46 |
47 |
48 | @pytest.mark.asyncio
49 | async def test_client_failure():
50 | from opbeat.contrib.asyncio import Client
51 | from opbeat.transport.base import TransportException
52 |
53 | client = Client(
54 | servers=['http://error'],
55 | organization_id='organization_id',
56 | app_id='app_id',
57 | secret_token='secret',
58 | async_mode=False,
59 | transport_class='.'.join(
60 | (__name__, MockTransport.__name__)),
61 | )
62 | client.send(foo='bar')
63 | tasks = asyncio.Task.all_tasks()
64 | task = next(t for t in tasks if t is not asyncio.Task.current_task())
65 | with pytest.raises(TransportException):
66 | await task
67 | assert client.state.status == 0
68 |
69 |
70 | @pytest.mark.asyncio
71 | async def test_client_failure_stdlib_exception(mocker):
72 | from opbeat.contrib.asyncio import Client
73 | from opbeat.transport.base import TransportException
74 |
75 | client = Client(
76 | servers=['http://opbeat'],
77 | organization_id='organization_id',
78 | app_id='app_id',
79 | secret_token='secret',
80 | async_mode=False,
81 | transport_class='opbeat.transport.asyncio.AsyncioHTTPTransport',
82 | )
83 | mock_client = mocker.Mock()
84 | mock_client.post = mocker.Mock(side_effect=RuntimeError('oops'))
85 | transport = client._get_transport(urlparse('http://opbeat'))
86 | transport.client = mock_client
87 | client.send(foo='bar')
88 | tasks = asyncio.Task.all_tasks()
89 | task = next(t for t in tasks if t is not asyncio.Task.current_task())
90 | with pytest.raises(TransportException):
91 | await task
92 | assert client.state.status == 0
93 |
--------------------------------------------------------------------------------
/opbeat/instrumentation/packages/pymongo.py:
--------------------------------------------------------------------------------
1 | from opbeat.instrumentation.packages.base import AbstractInstrumentedModule
2 | from opbeat.traces import trace
3 |
4 |
5 | class PyMongoInstrumentation(AbstractInstrumentedModule):
6 | name = 'pymongo'
7 |
8 | instrument_list = [
9 | ("pymongo.collection", "Collection.aggregate"),
10 | ("pymongo.collection", "Collection.bulk_write"),
11 | ("pymongo.collection", "Collection.count"),
12 | ("pymongo.collection", "Collection.create_index"),
13 | ("pymongo.collection", "Collection.create_indexes"),
14 | ("pymongo.collection", "Collection.delete_many"),
15 | ("pymongo.collection", "Collection.delete_one"),
16 | ("pymongo.collection", "Collection.distinct"),
17 | ("pymongo.collection", "Collection.drop"),
18 | ("pymongo.collection", "Collection.drop_index"),
19 | ("pymongo.collection", "Collection.drop_indexes"),
20 | ("pymongo.collection", "Collection.ensure_index"),
21 | ("pymongo.collection", "Collection.find_and_modify"),
22 | ("pymongo.collection", "Collection.find_one"),
23 | ("pymongo.collection", "Collection.find_one_and_delete"),
24 | ("pymongo.collection", "Collection.find_one_and_replace"),
25 | ("pymongo.collection", "Collection.find_one_and_update"),
26 | ("pymongo.collection", "Collection.group"),
27 | ("pymongo.collection", "Collection.inline_map_reduce"),
28 | ("pymongo.collection", "Collection.insert"),
29 | ("pymongo.collection", "Collection.insert_many"),
30 | ("pymongo.collection", "Collection.insert_one"),
31 | ("pymongo.collection", "Collection.map_reduce"),
32 | ("pymongo.collection", "Collection.reindex"),
33 | ("pymongo.collection", "Collection.remove"),
34 | ("pymongo.collection", "Collection.rename"),
35 | ("pymongo.collection", "Collection.replace_one"),
36 | ("pymongo.collection", "Collection.save"),
37 | ("pymongo.collection", "Collection.update"),
38 | ("pymongo.collection", "Collection.update_many"),
39 | ("pymongo.collection", "Collection.update_one"),
40 | ]
41 |
42 | def call(self, module, method, wrapped, instance, args, kwargs):
43 | cls_name, method_name = method.split('.', 1)
44 | signature = '.'.join([instance.full_name, method_name])
45 | with trace(signature, "db.mongodb.query", leaf=True):
46 | return wrapped(*args, **kwargs)
47 |
48 |
49 | class PyMongoBulkInstrumentation(AbstractInstrumentedModule):
50 | name = 'pymongo'
51 |
52 | instrument_list = [
53 | ("pymongo.bulk", "BulkOperationBuilder.execute"),
54 | ]
55 |
56 | def call(self, module, method, wrapped, instance, args, kwargs):
57 | collection = instance._BulkOperationBuilder__bulk.collection
58 | signature = '.'.join([collection.full_name, 'bulk.execute'])
59 | with trace(signature, "db.mongodb.query"):
60 | return wrapped(*args, **kwargs)
61 |
62 |
63 | class PyMongoCursorInstrumentation(AbstractInstrumentedModule):
64 | name = 'pymongo'
65 |
66 | instrument_list = [
67 | ("pymongo.cursor", "Cursor._refresh"),
68 | ]
69 |
70 | def call(self, module, method, wrapped, instance, args, kwargs):
71 | collection = instance.collection
72 | signature = '.'.join([collection.full_name, 'cursor.refresh'])
73 | with trace(signature, "db.mongodb.query"):
74 | return wrapped(*args, **kwargs)
75 |
--------------------------------------------------------------------------------
/opbeat/utils/wsgi.py:
--------------------------------------------------------------------------------
1 | """
2 | This module implements WSGI related helpers adapted from ``werkzeug.wsgi``
3 |
4 | :copyright: (c) 2010 by the Werkzeug Team, see AUTHORS for more details.
5 | :license: BSD, see LICENSE for more details.
6 | """
7 | from opbeat.utils import six
8 |
9 | try:
10 | from urllib import quote
11 | except ImportError:
12 | from urllib.parse import quote
13 |
14 |
15 | # `get_headers` comes from `werkzeug.datastructures.EnvironHeaders`
16 | def get_headers(environ):
17 | """
18 | Returns only proper HTTP headers.
19 | """
20 | for key, value in six.iteritems(environ):
21 | key = str(key)
22 | if key.startswith('HTTP_') and key not in \
23 | ('HTTP_CONTENT_TYPE', 'HTTP_CONTENT_LENGTH'):
24 | yield key[5:].replace('_', '-').title(), value
25 | elif key in ('CONTENT_TYPE', 'CONTENT_LENGTH'):
26 | yield key.replace('_', '-').title(), value
27 |
28 |
29 | def get_environ(environ):
30 | """
31 | Returns our whitelisted environment variables.
32 | """
33 | for key in ('REMOTE_ADDR', 'SERVER_NAME', 'SERVER_PORT'):
34 | if key in environ:
35 | yield key, environ[key]
36 |
37 |
38 | # `get_host` comes from `werkzeug.wsgi`
39 | def get_host(environ):
40 | """Return the real host for the given WSGI environment. This takes care
41 | of the `X-Forwarded-Host` header.
42 |
43 | :param environ: the WSGI environment to get the host of.
44 | """
45 | scheme = environ.get('wsgi.url_scheme')
46 | if 'HTTP_X_FORWARDED_HOST' in environ:
47 | result = environ['HTTP_X_FORWARDED_HOST']
48 | elif 'HTTP_HOST' in environ:
49 | result = environ['HTTP_HOST']
50 | else:
51 | result = environ['SERVER_NAME']
52 | if (scheme, str(environ['SERVER_PORT'])) not \
53 | in (('https', '443'), ('http', '80')):
54 | result += ':' + environ['SERVER_PORT']
55 | if result.endswith(':80') and scheme == 'http':
56 | result = result[:-3]
57 | elif result.endswith(':443') and scheme == 'https':
58 | result = result[:-4]
59 | return result
60 |
61 |
62 | # `get_current_url` comes from `werkzeug.wsgi`
63 | def get_current_url(environ, root_only=False, strip_querystring=False,
64 | host_only=False):
65 | """A handy helper function that recreates the full URL for the current
66 | request or parts of it. Here an example:
67 |
68 | >>> from werkzeug import create_environ
69 | >>> env = create_environ("/?param=foo", "http://localhost/script")
70 | >>> get_current_url(env)
71 | 'http://localhost/script/?param=foo'
72 | >>> get_current_url(env, root_only=True)
73 | 'http://localhost/script/'
74 | >>> get_current_url(env, host_only=True)
75 | 'http://localhost/'
76 | >>> get_current_url(env, strip_querystring=True)
77 | 'http://localhost/script/'
78 |
79 | :param environ: the WSGI environment to get the current URL from.
80 | :param root_only: set `True` if you only want the root URL.
81 | :param strip_querystring: set to `True` if you don't want the querystring.
82 | :param host_only: set to `True` if the host URL should be returned.
83 | """
84 | tmp = [environ['wsgi.url_scheme'], '://', get_host(environ)]
85 | cat = tmp.append
86 | if host_only:
87 | return ''.join(tmp) + '/'
88 | cat(quote(environ.get('SCRIPT_NAME', '').rstrip('/')))
89 | if root_only:
90 | cat('/')
91 | else:
92 | cat(quote('/' + environ.get('PATH_INFO', '').lstrip('/')))
93 | if not strip_querystring:
94 | qs = environ.get('QUERY_STRING')
95 | if qs:
96 | cat('?' + qs)
97 | return ''.join(tmp)
98 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | sudo: false
2 | language: python
3 | python:
4 | - 2.7
5 | - 3.3
6 | - 3.4
7 | - 3.5
8 | - 3.6
9 | - nightly
10 | - pypy
11 |
12 | env:
13 | matrix:
14 | - WEBFRAMEWORK=django-1.4
15 | - WEBFRAMEWORK=django-1.5
16 | - WEBFRAMEWORK=django-1.6
17 | - WEBFRAMEWORK=django-1.7
18 | - WEBFRAMEWORK=django-1.8
19 | - WEBFRAMEWORK=django-1.9
20 | - WEBFRAMEWORK=django-1.10
21 | - WEBFRAMEWORK=django-1.11
22 | - WEBFRAMEWORK=django-2.0
23 | - WEBFRAMEWORK=django-master
24 | - WEBFRAMEWORK=flask-0.10
25 | - WEBFRAMEWORK=flask-0.11
26 | - WEBFRAMEWORK=flask-0.12
27 | global:
28 | - PIP_CACHE="$HOME/.pip_cache"'
29 | - RUN_SCRIPT="./travis/run_tests.sh"
30 | matrix:
31 | exclude:
32 | - python: 2.7
33 | env: WEBFRAMEWORK=django-2.0
34 | - python: 2.7
35 | env: WEBFRAMEWORK=django-master
36 | - python: 3.3
37 | env: WEBFRAMEWORK=django-1.4
38 | - python: 3.3
39 | env: WEBFRAMEWORK=django-1.9
40 | - python: 3.3
41 | env: WEBFRAMEWORK=django-1.10
42 | - python: 3.3
43 | env: WEBFRAMEWORK=django-1.11
44 | - python: 3.3
45 | env: WEBFRAMEWORK=django-2.0
46 | - python: 3.3
47 | env: WEBFRAMEWORK=django-master
48 | - python: 3.4
49 | env: WEBFRAMEWORK=django-1.4
50 | - python: 3.4
51 | env: WEBFRAMEWORK=django-master
52 | - python: 3.5
53 | env: WEBFRAMEWORK=django-1.4
54 | - python: 3.5
55 | env: WEBFRAMEWORK=django-1.5
56 | - python: 3.5
57 | env: WEBFRAMEWORK=django-1.6
58 | - python: 3.5
59 | env: WEBFRAMEWORK=django-1.7
60 | - python: 3.6
61 | env: WEBFRAMEWORK=django-1.4
62 | - python: 3.6
63 | env: WEBFRAMEWORK=django-1.5
64 | - python: 3.6
65 | env: WEBFRAMEWORK=django-1.6
66 | - python: 3.6
67 | env: WEBFRAMEWORK=django-1.7
68 | - python: pypy
69 | env: WEBFRAMEWORK=django-2.0
70 | - python: pypy
71 | env: WEBFRAMEWORK=django-master
72 | - python: nightly
73 | env: WEBFRAMEWORK=django-1.4
74 | - python: nightly
75 | env: WEBFRAMEWORK=django-1.5
76 | - python: nightly
77 | env: WEBFRAMEWORK=django-1.6
78 | - python: nightly
79 | env: WEBFRAMEWORK=django-1.7
80 | include:
81 | - sudo: required
82 | python: 3.4
83 | services:
84 | - docker
85 | env: DOCKER_IMAGE=quay.io/pypa/manylinux1_x86_64 RUN_SCRIPT=./travis/run_docker.sh
86 | - sudo: required
87 | python: 3.4
88 | services:
89 | - docker
90 | env: DOCKER_IMAGE=quay.io/pypa/manylinux1_i686 RUN_SCRIPT=./travis/run_docker.sh
91 | PRE_CMD=linux32
92 | allow_failures:
93 | - env: WEBFRAMEWORK=django-master
94 | - python: nightly
95 | addons:
96 | apt:
97 | sources:
98 | - mongodb-3.0-precise
99 | packages:
100 | - libevent-dev
101 | - libzmq3-dev
102 | - mongodb-org-server
103 | postgresql: '9.4'
104 | cache:
105 | directories:
106 | - "$HOME/.pip_cache"
107 | script:
108 | - bash $RUN_SCRIPT
109 | notifications:
110 | email: false
111 | slack:
112 | secure: LcTTbTj0Px0/9Bs/S/uwbhkdULlj1YVdHnU8F/kOa3bq2QdCTptqB719r6BnzHvW+QGyADvDZ25UncVXFuLuHY67ZYfmyZ/H2cj0nrRSuYdPct0avhVbT/3s50GlNWK5qkfZDuqw6szYTFrgFWJcr5dl7Zf6Vovcvd38uaYOdno=
113 | services:
114 | - redis-server
115 | - memcached
116 | - mongodb
117 | - mysql
118 | - postgresql
119 | deploy:
120 | provider: s3
121 | access_key_id: AKIAIHY7VOHA6YNCCEYQ
122 | secret_access_key:
123 | secure: kb8Ho6JjTi3yTtdppw+fk6Zka0TLrFuEZU+O/b1YP4GEWUcf/aFKwtE8hi4SvsnjHGZxrAY9jRHKjVU02eEfbUTrCGu05ej9wEVC8IhevJMJljgInHWsG1PgPtNeD+uxWADXSXddjJ0U+N3Gh3I/PO530te2V2rQ1szJ2Hq79go=
124 | bucket: wheels.opbeat.com
125 | skip_cleanup: true
126 | local_dir: wheelhouse
127 | acl: public_read
128 | on:
129 | repo: opbeat/opbeat_python
130 | tags: true
131 |
--------------------------------------------------------------------------------
/tests/instrumentation/requests_tests.py:
--------------------------------------------------------------------------------
1 | import mock
2 | import requests
3 | from requests.exceptions import InvalidURL, MissingSchema
4 |
5 | import opbeat
6 | import opbeat.instrumentation.control
7 | from opbeat.traces import trace
8 | from tests.helpers import get_tempstoreclient
9 | from tests.utils.compat import TestCase
10 |
11 |
12 | class InstrumentRequestsTest(TestCase):
13 | def setUp(self):
14 | self.client = get_tempstoreclient()
15 | opbeat.instrumentation.control.instrument()
16 |
17 | @mock.patch("requests.adapters.HTTPAdapter.send")
18 | def test_requests_instrumentation(self, mock_send):
19 | mock_send.return_value = mock.Mock(
20 | url='http://example.com',
21 | history=[],
22 | headers={'location': ''},
23 | )
24 | self.client.begin_transaction("transaction.test")
25 | with trace("test_pipeline", "test"):
26 | # NOTE: The `allow_redirects` argument has to be set to `False`,
27 | # because mocking is done a level deeper, and the mocked response
28 | # from the `HTTPAdapter` is about to be used to make further
29 | # requests to resolve redirects, which doesn't make sense for this
30 | # test case.
31 | requests.get('http://example.com', allow_redirects=False)
32 | self.client.end_transaction("MyView")
33 |
34 | _, traces = self.client.instrumentation_store.get_all()
35 | self.assertIn('GET example.com', map(lambda x: x['signature'], traces))
36 |
37 | @mock.patch("requests.adapters.HTTPAdapter.send")
38 | def test_requests_instrumentation_via_session(self, mock_send):
39 | mock_send.return_value = mock.Mock(
40 | url='http://example.com',
41 | history=[],
42 | headers={'location': ''},
43 | )
44 | self.client.begin_transaction("transaction.test")
45 | with trace("test_pipeline", "test"):
46 | s = requests.Session()
47 | s.get('http://example.com', allow_redirects=False)
48 | self.client.end_transaction("MyView")
49 |
50 | _, traces = self.client.instrumentation_store.get_all()
51 | self.assertIn('GET example.com', map(lambda x: x['signature'], traces))
52 |
53 | @mock.patch("requests.adapters.HTTPAdapter.send")
54 | def test_requests_instrumentation_via_prepared_request(self, mock_send):
55 | mock_send.return_value = mock.Mock(
56 | url='http://example.com',
57 | history=[],
58 | headers={'location': ''},
59 | )
60 | self.client.begin_transaction("transaction.test")
61 | with trace("test_pipeline", "test"):
62 | r = requests.Request('get', 'http://example.com')
63 | pr = r.prepare()
64 | s = requests.Session()
65 | s.send(pr, allow_redirects=False)
66 | self.client.end_transaction("MyView")
67 |
68 | _, traces = self.client.instrumentation_store.get_all()
69 | self.assertIn('GET example.com', map(lambda x: x['signature'], traces))
70 |
71 | def test_requests_instrumentation_malformed_none(self):
72 | self.client.begin_transaction("transaction.test")
73 | with trace("test_pipeline", "test"):
74 | self.assertRaises(MissingSchema, requests.get, None)
75 |
76 | def test_requests_instrumentation_malformed_schema(self):
77 | self.client.begin_transaction("transaction.test")
78 | with trace("test_pipeline", "test"):
79 | self.assertRaises(MissingSchema, requests.get, '')
80 |
81 | def test_requests_instrumentation_malformed_path(self):
82 | self.client.begin_transaction("transaction.test")
83 | with trace("test_pipeline", "test"):
84 | self.assertRaises(InvalidURL, requests.get, 'http://')
85 |
--------------------------------------------------------------------------------
/opbeat/handlers/logbook.py:
--------------------------------------------------------------------------------
1 | """
2 | opbeat.handlers.logbook
3 | ~~~~~~~~~~~~~~~~~~~~~~
4 |
5 | :copyright: (c) 2011-2012 Opbeat
6 |
7 | Large portions are
8 | :copyright: (c) 2010 by the Sentry Team, see AUTHORS for more details.
9 | :license: BSD, see LICENSE for more details.
10 | """
11 | from __future__ import absolute_import
12 |
13 | import sys
14 | import traceback
15 |
16 | import logbook
17 |
18 | from opbeat.base import Client
19 | from opbeat.utils import six
20 | from opbeat.utils.encoding import to_string
21 |
22 | LOOKBOOK_LEVELS = {
23 | logbook.DEBUG: 'debug',
24 | logbook.INFO: 'info',
25 | logbook.NOTICE: 'info',
26 | logbook.WARNING: 'warning',
27 | logbook.ERROR: 'error',
28 | logbook.CRITICAL: 'fatal',
29 | }
30 |
31 |
32 | class OpbeatHandler(logbook.Handler):
33 | def __init__(self, *args, **kwargs):
34 | if len(args) == 1:
35 | arg = args[0]
36 | # if isinstance(arg, six.string_types):
37 | # self.client = kwargs.pop('client_cls', Client)(dsn=arg)
38 | if isinstance(arg, Client):
39 | self.client = arg
40 | else:
41 | raise ValueError(
42 | 'The first argument to %s must be a Client instance, '
43 | 'got %r instead.' % (
44 | self.__class__.__name__,
45 | arg,
46 | ))
47 | args = []
48 | else:
49 | try:
50 | self.client = kwargs.pop('client')
51 | except KeyError:
52 | raise TypeError(
53 | 'Expected keyword argument for OpbeatHandler: client')
54 | super(OpbeatHandler, self).__init__(*args, **kwargs)
55 |
56 | def emit(self, record):
57 | self.format(record)
58 |
59 | # Avoid typical config issues by overriding loggers behavior
60 | if record.channel.startswith('opbeat.errors'):
61 | six.print_(to_string(record.message), file=sys.stderr)
62 | return
63 |
64 | try:
65 | return self._emit(record)
66 | except Exception:
67 | six.print_(sys.stderr,
68 | "Top level Opbeat exception caught - "
69 | "failed creating log record",
70 | file=sys.stderr)
71 | six.print_(sys.stderr, to_string(record.msg), file=sys.stderr)
72 | six.print_(sys.stderr, to_string(traceback.format_exc()),
73 | file=sys.stderr)
74 |
75 | try:
76 | self.client.capture('Exception')
77 | except Exception:
78 | pass
79 |
80 | def _emit(self, record):
81 | data = {
82 | 'level': LOOKBOOK_LEVELS[record.level],
83 | 'logger': record.channel,
84 | }
85 |
86 | # If there's no exception being processed,
87 | # exc_info may be a 3-tuple of None
88 | # http://docs.python.org/library/sys.html#sys.exc_info
89 | if record.exc_info is True or (
90 | record.exc_info and all(record.exc_info)):
91 | handler = self.client.get_handler('opbeat.events.Exception')
92 |
93 | data.update(handler.capture(exc_info=record.exc_info))
94 |
95 | return self.client.capture('Message',
96 | param_message=
97 | {
98 | 'message': record.msg,
99 | 'params': record.args
100 | },
101 | data=data,
102 | extra=record.extra,
103 | )
104 |
--------------------------------------------------------------------------------
/docs/config/logging.rst:
--------------------------------------------------------------------------------
1 | Configuring ``logging``
2 | =======================
3 |
4 | .. csv-table::
5 | :class: page-info
6 |
7 | "Page updated: 23rd July 2013", ""
8 |
9 | Opbeat supports the ability to directly tie into the :mod:`logging` module. To
10 | use it simply add :class:`OpbeatHandler` to your logger.
11 |
12 | First you'll need to configure a handler
13 |
14 | .. code::
15 | :class: language-python
16 |
17 | from opbeat.handlers.logging import OpbeatHandler
18 |
19 | # Manually specify a client
20 | client = Client(...)
21 | handler = OpbeatHandler(client)
22 |
23 | .. You can also automatically configure the default client with a DSN::
24 |
25 | .. # Configure the default client
26 | .. handler = OpbeatHandler('http://public:secret@example.com/1')
27 |
28 | Finally, call the :func:`setup_logging` helper function
29 |
30 | .. code::
31 | :class: language-python
32 |
33 | from opbeat.conf import setup_logging
34 |
35 | setup_logging(handler)
36 |
37 | Usage
38 | ~~~~~
39 |
40 | A recommended pattern in logging is to simply reference the modules name for
41 | each logger, so for example, you might at the top of your module define the
42 | following
43 |
44 | .. code::
45 | :class: language-python
46 |
47 | import logging
48 | logger = logging.getLogger(__name__)
49 |
50 | You can also use the ``exc_info`` and ``extra={'stack': True}`` arguments on
51 | your ``log`` methods. This will store the appropriate information and allow
52 | Opbeat to render it based on that information
53 |
54 | .. code::
55 | :class: language-python
56 |
57 | logger.error('There was some crazy error', exc_info=True, extra={
58 | 'culprit': 'my.view.name',
59 | })
60 |
61 | You may also pass additional information to be stored as meta information with
62 | the event. As long as the key name is not reserved and not private (_foo) it
63 | will be displayed on the Opbeat dashboard. To do this, pass it as ``data``
64 | within your ``extra`` clause
65 |
66 | .. code::
67 | :class: language-python
68 |
69 | logger.error('There was some crazy error', exc_info=True, extra={
70 | # Optionally you can pass additional arguments to specify request info
71 | 'culprit': 'my.view.name',
72 |
73 | 'data': {
74 | # You may specify any values here and Opbeat will log and output them
75 | 'username': request.user.username,
76 | }
77 | })
78 |
79 | .. container:: note
80 |
81 | The ``url`` and ``view`` keys are used internally by Opbeat within the extra data.
82 |
83 | .. container:: note
84 |
85 | Any key (in ``data``) prefixed with ``_`` will not automatically output on the Opbeat details view.
86 |
87 | |
88 |
89 | Opbeat will intelligently group messages if you use proper string formatting. For example, the following messages would
90 | be seen as the same message within Opbeat
91 |
92 | .. code::
93 | :class: language-python
94 |
95 | logger.error('There was some %s error', 'crazy')
96 | logger.error('There was some %s error', 'fun')
97 | logger.error('There was some %s error', 1)
98 |
99 | The :mod:`logging` integration also allows easy capture of
100 | stack frames (and their locals) as if you were logging an exception. This can
101 | be done automatically with the ``AUTO_LOG_STACKS`` setting, as well as
102 | by passing the ``stack`` boolean to ``extra``
103 |
104 | .. code::
105 | :class: language-python
106 |
107 | logger.error('There was an error', extra={
108 | 'stack': True,
109 | })
110 |
111 | .. .. container:: note
112 |
113 | .. Other languages that provide a logging package that is comparable to the
114 | .. python :mod:`logging` package may define an Opbeat handler. Check the
115 | .. `Extending Opbeat
116 | .. `_
117 | .. documentation.
118 |
--------------------------------------------------------------------------------
/opbeat/transport/http.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | import logging
3 | import socket
4 |
5 | from opbeat.conf import defaults
6 | from opbeat.contrib.async_worker import AsyncWorker
7 | from opbeat.transport.base import AsyncTransport, Transport, TransportException
8 | from opbeat.utils.compat import HTTPError
9 |
10 | try:
11 | from urllib2 import Request, urlopen
12 | except ImportError:
13 | from urllib.request import Request, urlopen
14 |
15 |
16 | logger = logging.getLogger('opbeat')
17 |
18 |
19 | class HTTPTransport(Transport):
20 |
21 | scheme = ['http', 'https']
22 |
23 | def __init__(self, parsed_url):
24 | self.check_scheme(parsed_url)
25 |
26 | self._parsed_url = parsed_url
27 | self._url = parsed_url.geturl()
28 |
29 | def send(self, data, headers, timeout=None):
30 | """
31 | Sends a request to a remote webserver using HTTP POST.
32 |
33 | Returns the shortcut URL of the recorded error on Opbeat
34 | """
35 | req = Request(self._url, headers=headers)
36 | if timeout is None:
37 | timeout = defaults.TIMEOUT
38 | response = None
39 | try:
40 | try:
41 | response = urlopen(req, data, timeout)
42 | except TypeError:
43 | response = urlopen(req, data)
44 | except Exception as e:
45 | print_trace = True
46 | if isinstance(e, socket.timeout):
47 | message = (
48 | "Connection to Opbeat server timed out "
49 | "(url: %s, timeout: %d seconds)" % (self._url, timeout)
50 | )
51 | elif isinstance(e, HTTPError):
52 | body = e.read()
53 | if e.code == 429: # rate-limited
54 | message = 'Temporarily rate limited: '
55 | print_trace = False
56 | else:
57 | message = 'Unable to reach Opbeat server: '
58 | message += '%s (url: %s, body: %s)' % (e, self._url, body)
59 | else:
60 | message = 'Unable to reach Opbeat server: %s (url: %s)' % (
61 | e, self._url
62 | )
63 | raise TransportException(message, data, print_trace=print_trace)
64 | finally:
65 | if response:
66 | response.close()
67 |
68 | return response.info().get('Location')
69 |
70 |
71 | class AsyncHTTPTransport(AsyncTransport, HTTPTransport):
72 | scheme = ['http', 'https']
73 | async_mode = True
74 |
75 | def __init__(self, parsed_url):
76 | super(AsyncHTTPTransport, self).__init__(parsed_url)
77 | if self._url.startswith('async+'):
78 | self._url = self._url[6:]
79 | self._worker = None
80 |
81 | @property
82 | def worker(self):
83 | if not self._worker or not self._worker.is_alive():
84 | self._worker = AsyncWorker()
85 | return self._worker
86 |
87 | def send_sync(self, data=None, headers=None, success_callback=None,
88 | fail_callback=None):
89 | try:
90 | url = HTTPTransport.send(self, data, headers)
91 | if callable(success_callback):
92 | success_callback(url=url)
93 | except Exception as e:
94 | if callable(fail_callback):
95 | fail_callback(exception=e)
96 |
97 | def send_async(self, data, headers, success_callback=None,
98 | fail_callback=None):
99 | kwargs = {
100 | 'data': data,
101 | 'headers': headers,
102 | 'success_callback': success_callback,
103 | 'fail_callback': fail_callback,
104 | }
105 | self.worker.queue(self.send_sync, kwargs)
106 |
107 | def close(self):
108 | if self._worker:
109 | self._worker.main_thread_terminated()
110 |
--------------------------------------------------------------------------------
/opbeat/processors.py:
--------------------------------------------------------------------------------
1 | """
2 | opbeat.core.processors
3 | ~~~~~~~~~~~~~~~~~~~~~
4 |
5 | :copyright: (c) 2011-2012 Opbeat
6 |
7 | Large portions are
8 | :copyright: (c) 2010 by the Sentry Team, see AUTHORS for more details.
9 | :license: BSD, see LICENSE for more details.
10 | """
11 |
12 | import re
13 |
14 | from opbeat.utils import six, varmap
15 | from opbeat.utils.encoding import force_text
16 |
17 |
18 | class Processor(object):
19 | def __init__(self, client):
20 | self.client = client
21 |
22 | def get_data(self, data, **kwargs):
23 | return
24 |
25 | def process(self, data, **kwargs):
26 | resp = self.get_data(data, **kwargs)
27 | if resp:
28 | data = resp
29 | return data
30 |
31 |
32 | class RemovePostDataProcessor(Processor):
33 | """
34 | Removes HTTP post data.
35 | """
36 | def process(self, data, **kwargs):
37 | if 'http' in data:
38 | data['http'].pop('data', None)
39 |
40 | return data
41 |
42 |
43 | class RemoveStackLocalsProcessor(Processor):
44 | """
45 | Removes local context variables from stacktraces.
46 | """
47 | def process(self, data, **kwargs):
48 | if 'stacktrace' in data:
49 | for frame in data['stacktrace'].get('frames', []):
50 | frame.pop('vars', None)
51 |
52 | return data
53 |
54 |
55 | class SanitizePasswordsProcessor(Processor):
56 | """
57 | Asterisk out passwords from password fields in frames, http,
58 | and basic extra data.
59 | """
60 | MASK = '*' * 8
61 | FIELDS = frozenset([
62 | 'password',
63 | 'secret',
64 | 'passwd',
65 | 'token',
66 | 'api_key',
67 | 'access_token',
68 | 'sessionid',
69 | ])
70 | VALUES_RE = re.compile(r'^\d{16}$')
71 |
72 | def sanitize(self, key, value):
73 | if value is None:
74 | return
75 |
76 | if isinstance(value, six.string_types) and self.VALUES_RE.match(value):
77 | return self.MASK
78 |
79 | if not key: # key can be a NoneType
80 | return value
81 |
82 | key = key.lower()
83 | for field in self.FIELDS:
84 | if field in key:
85 | # store mask as a fixed length for security
86 | return self.MASK
87 | return value
88 |
89 | def filter_stacktrace(self, data):
90 | if 'frames' not in data:
91 | return
92 | for frame in data['frames']:
93 | if 'vars' not in frame:
94 | continue
95 | frame['vars'] = varmap(self.sanitize, frame['vars'])
96 |
97 | def filter_http(self, data):
98 | for n in ('data', 'cookies', 'headers', 'env', 'query_string'):
99 | if n not in data:
100 | continue
101 |
102 | if isinstance(data[n], (six.binary_type,) + six.string_types):
103 | text_data = force_text(data[n], errors='replace')
104 | if '=' in text_data:
105 | # at this point we've assumed it's a standard HTTP query
106 | querybits = []
107 | for bit in text_data.split('&'):
108 | chunk = bit.split('=')
109 | if len(chunk) == 2:
110 | querybits.append((chunk[0], self.sanitize(*chunk)))
111 | else:
112 | querybits.append(chunk)
113 |
114 | data[n] = '&'.join('='.join(k) for k in querybits)
115 | continue
116 | data[n] = varmap(self.sanitize, data[n])
117 |
118 | def process(self, data, **kwargs):
119 | if 'stacktrace' in data:
120 | self.filter_stacktrace(data['stacktrace'])
121 |
122 | if 'http' in data:
123 | self.filter_http(data['http'])
124 |
125 | return data
126 |
--------------------------------------------------------------------------------
/opbeat/contrib/django/utils.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | from django.template.base import Node
4 |
5 | from opbeat.utils.stacks import get_frame_info
6 |
7 | try:
8 | from django.template.base import Template
9 | except ImportError:
10 | class Template(object):
11 | pass
12 |
13 |
14 |
15 | def linebreak_iter(template_source):
16 | yield 0
17 | p = template_source.find('\n')
18 | while p >= 0:
19 | yield p + 1
20 | p = template_source.find('\n', p + 1)
21 | yield len(template_source) + 1
22 |
23 |
24 | def get_data_from_template_source(source):
25 | origin, (start, end) = source
26 | template_source = origin.reload()
27 |
28 | lineno = None
29 | upto = 0
30 | source_lines = []
31 | for num, next in enumerate(linebreak_iter(template_source)):
32 | if start >= upto and end <= next:
33 | lineno = num
34 | source_lines.append(template_source[upto:next])
35 | upto = next
36 |
37 | if not source_lines or lineno is None:
38 | return {}
39 |
40 | pre_context = source_lines[max(lineno - 3, 0):lineno]
41 | post_context = source_lines[(lineno + 1):(lineno + 4)]
42 | context_line = source_lines[lineno]
43 |
44 | return {
45 | 'template': {
46 | 'filename': origin.loadname,
47 | 'abs_path': origin.name,
48 | 'pre_context': pre_context,
49 | 'context_line': context_line,
50 | 'lineno': lineno,
51 | 'post_context': post_context,
52 | },
53 | 'culprit': origin.loadname,
54 | }
55 |
56 |
57 | def get_data_from_template_debug(template_debug):
58 | pre_context = []
59 | post_context = []
60 | context_line = None
61 | for lineno, line in template_debug['source_lines']:
62 | if lineno < template_debug['line']:
63 | pre_context.append(line)
64 | elif lineno > template_debug['line']:
65 | post_context.append(line)
66 | else:
67 | context_line = line
68 | return {
69 | 'template': {
70 | 'filename': os.path.basename(template_debug['name']),
71 | 'abs_path': template_debug['name'],
72 | 'pre_context': pre_context,
73 | 'context_line': context_line,
74 | 'lineno': template_debug['line'],
75 | 'post_context': post_context,
76 | },
77 | 'culprit': os.path.basename(template_debug['name']),
78 | }
79 |
80 |
81 | def iterate_with_template_sources(frames, extended=True):
82 | template = None
83 | for frame, lineno in frames:
84 | f_code = getattr(frame, 'f_code', None)
85 | if f_code:
86 | function = frame.f_code.co_name
87 | if function == 'render':
88 | renderer = getattr(frame, 'f_locals', {}).get('self')
89 | if renderer and isinstance(renderer, Node):
90 | if getattr(renderer, "token", None) is not None:
91 | if hasattr(renderer, "source"):
92 | # up to Django 1.8
93 | yield {
94 | 'lineno': renderer.token.lineno,
95 | 'filename': renderer.source[0].name
96 | }
97 | else:
98 | template = {'lineno': renderer.token.lineno}
99 | # Django 1.9 doesn't have the origin on the Node instance,
100 | # so we have to get it a bit further down the stack from the
101 | # Template instance
102 | elif renderer and isinstance(renderer, Template):
103 | if template and getattr(renderer, 'origin', None):
104 | template['filename'] = renderer.origin.name
105 | yield template
106 | template = None
107 |
108 | yield get_frame_info(frame, lineno, extended)
109 |
--------------------------------------------------------------------------------
/conftest.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | import sys
3 | from os.path import abspath, dirname, join
4 |
5 | where_am_i = dirname(abspath(__file__))
6 |
7 | BASE_TEMPLATE_DIR = join(where_am_i, 'tests', 'contrib', 'django', 'testapp',
8 | 'templates')
9 |
10 | sys.path.insert(0, where_am_i)
11 |
12 | # don't run tests of dependencies that land in "build" and "src"
13 | collect_ignore = ['build', 'src']
14 |
15 |
16 | try:
17 | from psycopg2cffi import compat
18 | compat.register()
19 | except ImportError:
20 | pass
21 |
22 |
23 | def pytest_configure(config):
24 | try:
25 | from django.conf import settings
26 | except ImportError:
27 | settings = None
28 | if settings is not None and not settings.configured:
29 | import django
30 |
31 | if django.VERSION >= (2, 0):
32 | middleware_settings_name = 'MIDDLEWARE'
33 | else:
34 | middleware_settings_name = 'MIDDLEWARE_CLASSES'
35 |
36 | # django-celery does not work well with Django 1.8+
37 | if django.VERSION < (1, 8):
38 | djcelery = ['djcelery']
39 | else:
40 | djcelery = []
41 | settings_dict = dict(
42 | DATABASES={
43 | 'default': {
44 | 'ENGINE': 'django.db.backends.sqlite3',
45 | 'NAME': 'opbeat_tests.db',
46 | 'TEST_NAME': 'opbeat_tests.db',
47 | 'TEST': {
48 | 'NAME': 'opbeat_tests.db',
49 | }
50 | },
51 | },
52 | TEST_DATABASE_NAME='opbeat_tests.db',
53 | INSTALLED_APPS=[
54 | 'django.contrib.auth',
55 | 'django.contrib.admin',
56 | 'django.contrib.sessions',
57 | 'django.contrib.sites',
58 | 'django.contrib.redirects',
59 |
60 | 'django.contrib.contenttypes',
61 |
62 | 'opbeat.contrib.django',
63 | 'tests.contrib.django.testapp',
64 | ] + djcelery,
65 | ROOT_URLCONF='tests.contrib.django.testapp.urls',
66 | DEBUG=False,
67 | SITE_ID=1,
68 | BROKER_HOST="localhost",
69 | BROKER_PORT=5672,
70 | BROKER_USER="guest",
71 | BROKER_PASSWORD="guest",
72 | BROKER_VHOST="/",
73 | CELERY_ALWAYS_EAGER=True,
74 | TEMPLATE_DEBUG=False,
75 | TEMPLATE_DIRS=[BASE_TEMPLATE_DIR],
76 | ALLOWED_HOSTS=['*'],
77 | MIDDLEWARE_CLASSES=[
78 | 'django.contrib.sessions.middleware.SessionMiddleware',
79 | 'django.contrib.auth.middleware.AuthenticationMiddleware',
80 | 'django.contrib.messages.middleware.MessageMiddleware',
81 | ],
82 | TEMPLATES=[
83 | {
84 | 'BACKEND': 'django.template.backends.django.DjangoTemplates',
85 | 'DIRS': [BASE_TEMPLATE_DIR],
86 | 'OPTIONS': {
87 | 'context_processors': [
88 | 'django.contrib.auth.context_processors.auth',
89 | ],
90 | 'loaders': [
91 | 'django.template.loaders.filesystem.Loader',
92 | ],
93 | 'debug': False,
94 | },
95 | },
96 | ]
97 |
98 | )
99 | settings_dict[middleware_settings_name] = [
100 | 'django.contrib.sessions.middleware.SessionMiddleware',
101 | 'django.contrib.auth.middleware.AuthenticationMiddleware',
102 | 'django.contrib.messages.middleware.MessageMiddleware',
103 | ]
104 | settings.configure(**settings_dict)
105 | if hasattr(django, 'setup'):
106 | django.setup()
107 | if django.VERSION < (1, 8):
108 | import djcelery
109 | djcelery.setup_loader()
110 |
--------------------------------------------------------------------------------
/opbeat/utils/wrapt/arguments.py:
--------------------------------------------------------------------------------
1 | # This is a copy of the inspect.getcallargs() function from Python 2.7
2 | # so we can provide it for use under Python 2.6. As the code in this
3 | # file derives from the Python distribution, it falls under the version
4 | # of the PSF license used for Python 2.7.
5 |
6 | from inspect import getargspec, ismethod
7 |
8 | def getcallargs(func, *positional, **named):
9 | """Get the mapping of arguments to values.
10 |
11 | A dict is returned, with keys the function argument names (including the
12 | names of the * and ** arguments, if any), and values the respective bound
13 | values from 'positional' and 'named'."""
14 | args, varargs, varkw, defaults = getargspec(func)
15 | f_name = func.__name__
16 | arg2value = {}
17 |
18 | # The following closures are basically because of tuple parameter unpacking.
19 | assigned_tuple_params = []
20 | def assign(arg, value):
21 | if isinstance(arg, str):
22 | arg2value[arg] = value
23 | else:
24 | assigned_tuple_params.append(arg)
25 | value = iter(value)
26 | for i, subarg in enumerate(arg):
27 | try:
28 | subvalue = next(value)
29 | except StopIteration:
30 | raise ValueError('need more than %d %s to unpack' %
31 | (i, 'values' if i > 1 else 'value'))
32 | assign(subarg,subvalue)
33 | try:
34 | next(value)
35 | except StopIteration:
36 | pass
37 | else:
38 | raise ValueError('too many values to unpack')
39 | def is_assigned(arg):
40 | if isinstance(arg,str):
41 | return arg in arg2value
42 | return arg in assigned_tuple_params
43 | if ismethod(func) and func.im_self is not None:
44 | # implicit 'self' (or 'cls' for classmethods) argument
45 | positional = (func.im_self,) + positional
46 | num_pos = len(positional)
47 | num_total = num_pos + len(named)
48 | num_args = len(args)
49 | num_defaults = len(defaults) if defaults else 0
50 | for arg, value in zip(args, positional):
51 | assign(arg, value)
52 | if varargs:
53 | if num_pos > num_args:
54 | assign(varargs, positional[-(num_pos-num_args):])
55 | else:
56 | assign(varargs, ())
57 | elif 0 < num_args < num_pos:
58 | raise TypeError('%s() takes %s %d %s (%d given)' % (
59 | f_name, 'at most' if defaults else 'exactly', num_args,
60 | 'arguments' if num_args > 1 else 'argument', num_total))
61 | elif num_args == 0 and num_total:
62 | if varkw:
63 | if num_pos:
64 | # XXX: We should use num_pos, but Python also uses num_total:
65 | raise TypeError('%s() takes exactly 0 arguments '
66 | '(%d given)' % (f_name, num_total))
67 | else:
68 | raise TypeError('%s() takes no arguments (%d given)' %
69 | (f_name, num_total))
70 | for arg in args:
71 | if isinstance(arg, str) and arg in named:
72 | if is_assigned(arg):
73 | raise TypeError("%s() got multiple values for keyword "
74 | "argument '%s'" % (f_name, arg))
75 | else:
76 | assign(arg, named.pop(arg))
77 | if defaults: # fill in any missing values with the defaults
78 | for arg, value in zip(args[-num_defaults:], defaults):
79 | if not is_assigned(arg):
80 | assign(arg, value)
81 | if varkw:
82 | assign(varkw, named)
83 | elif named:
84 | unexpected = next(iter(named))
85 | if isinstance(unexpected, unicode):
86 | unexpected = unexpected.encode(sys.getdefaultencoding(), 'replace')
87 | raise TypeError("%s() got an unexpected keyword argument '%s'" %
88 | (f_name, unexpected))
89 | unassigned = num_args - len([arg for arg in args if is_assigned(arg)])
90 | if unassigned:
91 | num_required = num_args - num_defaults
92 | raise TypeError('%s() takes %s %d %s (%d given)' % (
93 | f_name, 'at least' if defaults else 'exactly', num_required,
94 | 'arguments' if num_required > 1 else 'argument', num_total))
95 | return arg2value
96 |
--------------------------------------------------------------------------------
/tests/instrumentation/mysql_tests.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from opbeat.instrumentation.packages.mysql import extract_signature
3 | from tests.utils.compat import TestCase
4 |
5 |
6 | class ExtractSignatureTest(TestCase):
7 | def test_insert(self):
8 | sql = """INSERT INTO `mytable` (id, name) VALUE ('2323', 'Ron')"""
9 | actual = extract_signature(sql)
10 |
11 | self.assertEqual("INSERT INTO mytable", actual)
12 |
13 | def test_update(self):
14 | sql = """UPDATE `mytable` set name='Ron' WHERE id = 2323"""
15 | actual = extract_signature(sql)
16 |
17 | self.assertEqual("UPDATE mytable", actual)
18 |
19 | def test_delete(self):
20 | sql = """DELETE FROM `mytable` WHERE id = 2323"""
21 | actual = extract_signature(sql)
22 |
23 | self.assertEqual("DELETE FROM mytable", actual)
24 |
25 | def test_select_simple(self):
26 | sql = """SELECT `id`, `name` FROM `mytable` WHERE id = 2323"""
27 | actual = extract_signature(sql)
28 |
29 | self.assertEqual("SELECT FROM mytable", actual)
30 |
31 | def test_select_with_entity_quotes(self):
32 | sql = """SELECT `id`, `name` FROM `mytable` WHERE id = 2323"""
33 | actual = extract_signature(sql)
34 |
35 | self.assertEqual("SELECT FROM mytable", actual)
36 |
37 | def test_select_with_difficult_values(self):
38 | sql = """SELECT id, 'some \\'name' + " from Denmark" FROM `mytable` WHERE id = 2323"""
39 | actual = extract_signature(sql)
40 |
41 | self.assertEqual("SELECT FROM mytable", actual)
42 |
43 | def test_select_with_difficult_table_name(self):
44 | sql = "SELECT id FROM `myta\n-æøåble` WHERE id = 2323"""
45 | actual = extract_signature(sql)
46 |
47 | self.assertEqual("SELECT FROM myta\n-æøåble", actual)
48 |
49 | def test_select_subselect(self):
50 | sql = """SELECT id, name FROM (
51 | SELECT id, "not a FROM ''value" FROM mytable WHERE id = 2323
52 | ) LIMIT 20"""
53 | actual = extract_signature(sql)
54 |
55 | self.assertEqual("SELECT FROM mytable", actual)
56 |
57 | def test_select_subselect_with_alias(self):
58 | sql = """
59 | SELECT count(*)
60 | FROM (
61 | SELECT count(id) AS some_alias, some_column
62 | FROM mytable
63 | GROUP BY some_colun
64 | HAVING count(id) > 1
65 | ) AS foo
66 | """
67 | actual = extract_signature(sql)
68 |
69 | self.assertEqual("SELECT FROM mytable", actual)
70 |
71 | def test_select_with_multiple_tables(self):
72 | sql = """SELECT count(table2.id)
73 | FROM table1, table2, table2
74 | WHERE table2.id = table1.table2_id
75 | """
76 | actual = extract_signature(sql)
77 | self.assertEqual("SELECT FROM table1", actual)
78 |
79 | def test_select_with_invalid_literal(self):
80 | sql = "SELECT \"neverending literal FROM (SELECT * FROM ..."""
81 | actual = extract_signature(sql)
82 |
83 | self.assertEqual("SELECT FROM", actual)
84 |
85 | def test_savepoint(self):
86 | sql = """SAVEPOINT x_asd1234"""
87 | actual = extract_signature(sql)
88 |
89 | self.assertEqual("SAVEPOINT", actual)
90 |
91 | def test_begin(self):
92 | sql = """BEGIN"""
93 | actual = extract_signature(sql)
94 |
95 | self.assertEqual("BEGIN", actual)
96 |
97 | def test_create_index_with_name(self):
98 | sql = """CREATE INDEX myindex ON mytable"""
99 | actual = extract_signature(sql)
100 |
101 | self.assertEqual("CREATE INDEX", actual)
102 |
103 | def test_create_index_without_name(self):
104 | sql = """CREATE INDEX ON mytable"""
105 | actual = extract_signature(sql)
106 |
107 | self.assertEqual("CREATE INDEX", actual)
108 |
109 | def test_drop_table(self):
110 | sql = """DROP TABLE mytable"""
111 | actual = extract_signature(sql)
112 |
113 | self.assertEqual("DROP TABLE", actual)
114 |
115 | def test_multi_statement_sql(self):
116 | sql = """CREATE TABLE mytable; SELECT * FROM mytable; DROP TABLE mytable"""
117 | actual = extract_signature(sql)
118 |
119 | self.assertEqual("CREATE TABLE", actual)
120 |
--------------------------------------------------------------------------------
/docs/make.bat:
--------------------------------------------------------------------------------
1 | @ECHO OFF
2 |
3 | REM Command file for Sphinx documentation
4 |
5 | if "%SPHINXBUILD%" == "" (
6 | set SPHINXBUILD=sphinx-build
7 | )
8 | set BUILDDIR=_build
9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% .
10 | if NOT "%PAPER%" == "" (
11 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS%
12 | )
13 |
14 | if "%1" == "" goto help
15 |
16 | if "%1" == "help" (
17 | :help
18 | echo.Please use `make ^` where ^ is one of
19 | echo. html to make standalone HTML files
20 | echo. dirhtml to make HTML files named index.html in directories
21 | echo. singlehtml to make a single large HTML file
22 | echo. pickle to make pickle files
23 | echo. json to make JSON files
24 | echo. htmlhelp to make HTML files and a HTML help project
25 | echo. qthelp to make HTML files and a qthelp project
26 | echo. devhelp to make HTML files and a Devhelp project
27 | echo. epub to make an epub
28 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter
29 | echo. text to make text files
30 | echo. man to make manual pages
31 | echo. changes to make an overview over all changed/added/deprecated items
32 | echo. linkcheck to check all external links for integrity
33 | echo. doctest to run all doctests embedded in the documentation if enabled
34 | goto end
35 | )
36 |
37 | if "%1" == "clean" (
38 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i
39 | del /q /s %BUILDDIR%\*
40 | goto end
41 | )
42 |
43 | if "%1" == "html" (
44 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html
45 | echo.
46 | echo.Build finished. The HTML pages are in %BUILDDIR%/html.
47 | goto end
48 | )
49 |
50 | if "%1" == "dirhtml" (
51 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml
52 | echo.
53 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml.
54 | goto end
55 | )
56 |
57 | if "%1" == "singlehtml" (
58 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml
59 | echo.
60 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml.
61 | goto end
62 | )
63 |
64 | if "%1" == "pickle" (
65 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle
66 | echo.
67 | echo.Build finished; now you can process the pickle files.
68 | goto end
69 | )
70 |
71 | if "%1" == "json" (
72 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json
73 | echo.
74 | echo.Build finished; now you can process the JSON files.
75 | goto end
76 | )
77 |
78 | if "%1" == "htmlhelp" (
79 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp
80 | echo.
81 | echo.Build finished; now you can run HTML Help Workshop with the ^
82 | .hhp project file in %BUILDDIR%/htmlhelp.
83 | goto end
84 | )
85 |
86 | if "%1" == "qthelp" (
87 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp
88 | echo.
89 | echo.Build finished; now you can run "qcollectiongenerator" with the ^
90 | .qhcp project file in %BUILDDIR%/qthelp, like this:
91 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\Sentry.qhcp
92 | echo.To view the help file:
93 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\Sentry.ghc
94 | goto end
95 | )
96 |
97 | if "%1" == "devhelp" (
98 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp
99 | echo.
100 | echo.Build finished.
101 | goto end
102 | )
103 |
104 | if "%1" == "epub" (
105 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub
106 | echo.
107 | echo.Build finished. The epub file is in %BUILDDIR%/epub.
108 | goto end
109 | )
110 |
111 | if "%1" == "latex" (
112 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
113 | echo.
114 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex.
115 | goto end
116 | )
117 |
118 | if "%1" == "text" (
119 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text
120 | echo.
121 | echo.Build finished. The text files are in %BUILDDIR%/text.
122 | goto end
123 | )
124 |
125 | if "%1" == "man" (
126 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man
127 | echo.
128 | echo.Build finished. The manual pages are in %BUILDDIR%/man.
129 | goto end
130 | )
131 |
132 | if "%1" == "changes" (
133 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes
134 | echo.
135 | echo.The overview file is in %BUILDDIR%/changes.
136 | goto end
137 | )
138 |
139 | if "%1" == "linkcheck" (
140 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck
141 | echo.
142 | echo.Link check complete; look for any errors in the above output ^
143 | or in %BUILDDIR%/linkcheck/output.txt.
144 | goto end
145 | )
146 |
147 | if "%1" == "doctest" (
148 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest
149 | echo.
150 | echo.Testing of doctests in the sources finished, look at the ^
151 | results in %BUILDDIR%/doctest/output.txt.
152 | goto end
153 | )
154 |
155 | :end
156 |
--------------------------------------------------------------------------------
/docs/Makefile:
--------------------------------------------------------------------------------
1 | # Makefile for Sphinx documentation
2 | #
3 |
4 | # You can set these variables from the command line.
5 | SPHINXOPTS =
6 | SPHINXBUILD = sphinx-build
7 | PAPER =
8 | BUILDDIR = ./
9 |
10 | # Internal variables.
11 | PAPEROPT_a4 = -D latex_paper_size=a4
12 | PAPEROPT_letter = -D latex_paper_size=letter
13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
14 |
15 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest
16 |
17 | help:
18 | @echo "Please use \`make ' where is one of"
19 | @echo " html to make standalone HTML files"
20 | @echo " dirhtml to make HTML files named index.html in directories"
21 | @echo " singlehtml to make a single large HTML file"
22 | @echo " pickle to make pickle files"
23 | @echo " json to make JSON files"
24 | @echo " htmlhelp to make HTML files and a HTML help project"
25 | @echo " qthelp to make HTML files and a qthelp project"
26 | @echo " devhelp to make HTML files and a Devhelp project"
27 | @echo " epub to make an epub"
28 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
29 | @echo " latexpdf to make LaTeX files and run them through pdflatex"
30 | @echo " text to make text files"
31 | @echo " man to make manual pages"
32 | @echo " changes to make an overview of all changed/added/deprecated items"
33 | @echo " linkcheck to check all external links for integrity"
34 | @echo " doctest to run all doctests embedded in the documentation (if enabled)"
35 |
36 | clean:
37 | -rm -rf $(BUILDDIR)/*
38 |
39 | html:
40 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
41 | @echo
42 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
43 |
44 | dirhtml:
45 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
46 | @echo
47 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
48 |
49 | singlehtml:
50 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
51 | @echo
52 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
53 |
54 | pickle:
55 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
56 | @echo
57 | @echo "Build finished; now you can process the pickle files."
58 |
59 | json:
60 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
61 | @echo
62 | @echo "Build finished; now you can process the JSON files."
63 |
64 | htmlhelp:
65 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
66 | @echo
67 | @echo "Build finished; now you can run HTML Help Workshop with the" \
68 | ".hhp project file in $(BUILDDIR)/htmlhelp."
69 |
70 | qthelp:
71 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
72 | @echo
73 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \
74 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:"
75 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Sentry.qhcp"
76 | @echo "To view the help file:"
77 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Sentry.qhc"
78 |
79 | devhelp:
80 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
81 | @echo
82 | @echo "Build finished."
83 | @echo "To view the help file:"
84 | @echo "# mkdir -p $$HOME/.local/share/devhelp/Sentry"
85 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Sentry"
86 | @echo "# devhelp"
87 |
88 | epub:
89 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
90 | @echo
91 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub."
92 |
93 | latex:
94 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
95 | @echo
96 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
97 | @echo "Run \`make' in that directory to run these through (pdf)latex" \
98 | "(use \`make latexpdf' here to do that automatically)."
99 |
100 | latexpdf:
101 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
102 | @echo "Running LaTeX files through pdflatex..."
103 | make -C $(BUILDDIR)/latex all-pdf
104 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
105 |
106 | text:
107 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
108 | @echo
109 | @echo "Build finished. The text files are in $(BUILDDIR)/text."
110 |
111 | man:
112 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
113 | @echo
114 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man."
115 |
116 | changes:
117 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
118 | @echo
119 | @echo "The overview file is in $(BUILDDIR)/changes."
120 |
121 | linkcheck:
122 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
123 | @echo
124 | @echo "Link check complete; look for any errors in the above output " \
125 | "or in $(BUILDDIR)/linkcheck/output.txt."
126 |
127 | doctest:
128 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
129 | @echo "Testing of doctests in the sources finished, look at the " \
130 | "results in $(BUILDDIR)/doctest/output.txt."
131 |
--------------------------------------------------------------------------------
/opbeat/handlers/logging.py:
--------------------------------------------------------------------------------
1 | """
2 | opbeat.handlers.logging
3 | ~~~~~~~~~~~~~~~~~~~~~~
4 |
5 | :copyright: (c) 2011-2012 Opbeat
6 |
7 | Large portions are
8 | :copyright: (c) 2010 by the Sentry Team, see AUTHORS for more details.
9 | :license: BSD, see LICENSE for more details.
10 | """
11 |
12 | from __future__ import absolute_import
13 |
14 | import datetime
15 | import logging
16 | import sys
17 | import traceback
18 |
19 | from opbeat.base import Client
20 | from opbeat.utils import six
21 | from opbeat.utils.encoding import to_string
22 | from opbeat.utils.stacks import iter_stack_frames
23 |
24 |
25 | class OpbeatHandler(logging.Handler):
26 | def __init__(self, *args, **kwargs):
27 | client = kwargs.pop('client_cls', Client)
28 | if len(args) == 1:
29 | arg = args[0]
30 | args = args[1:]
31 | if isinstance(arg, Client):
32 | self.client = arg
33 | else:
34 | raise ValueError(
35 | 'The first argument to %s must be a Client instance, '
36 | 'got %r instead.' % (
37 | self.__class__.__name__,
38 | arg,
39 | ))
40 | elif 'client' in kwargs:
41 | self.client = kwargs.pop('client')
42 | else:
43 | self.client = client(*args, **kwargs)
44 |
45 | logging.Handler.__init__(self, level=kwargs.get('level', logging.NOTSET))
46 |
47 | def emit(self, record):
48 | self.format(record)
49 |
50 | # Avoid typical config issues by overriding loggers behavior
51 | if record.name.startswith('opbeat.errors'):
52 | six.print_(to_string(record.message), file=sys.stderr)
53 | return
54 |
55 | try:
56 | return self._emit(record)
57 | except Exception:
58 | six.print_(
59 | "Top level Opbeat exception caught - "
60 | "failed creating log record",
61 | sys.stderr)
62 | six.print_(to_string(record.msg), sys.stderr)
63 | six.print_(to_string(traceback.format_exc()), sys.stderr)
64 |
65 | try:
66 | self.client.capture('Exception')
67 | except Exception:
68 | pass
69 |
70 | def _emit(self, record, **kwargs):
71 | data = {}
72 |
73 | for k, v in six.iteritems(record.__dict__):
74 | if '.' not in k and k not in ('culprit',):
75 | continue
76 | data[k] = v
77 |
78 | stack = getattr(record, 'stack', None)
79 | if stack is True:
80 | stack = iter_stack_frames()
81 |
82 | if stack:
83 | frames = []
84 | started = False
85 | last_mod = ''
86 | for item in stack:
87 | if isinstance(item, (list, tuple)):
88 | frame, lineno = item
89 | else:
90 | frame, lineno = item, item.f_lineno
91 |
92 | if not started:
93 | f_globals = getattr(frame, 'f_globals', {})
94 | module_name = f_globals.get('__name__', '')
95 | if last_mod.startswith(
96 | 'logging') and not module_name.startswith(
97 | 'logging'):
98 | started = True
99 | else:
100 | last_mod = module_name
101 | continue
102 | frames.append((frame, lineno))
103 | stack = frames
104 |
105 | extra = getattr(record, 'data', {})
106 | # Add in all of the data from the record that we aren't already capturing
107 | for k in record.__dict__.keys():
108 | if k in (
109 | 'stack', 'name', 'args', 'msg', 'levelno', 'exc_text',
110 | 'exc_info', 'data', 'created', 'levelname', 'msecs',
111 | 'relativeCreated'):
112 | continue
113 | if k.startswith('_'):
114 | continue
115 | extra[k] = record.__dict__[k]
116 |
117 | date = datetime.datetime.utcfromtimestamp(record.created)
118 |
119 | # If there's no exception being processed,
120 | # exc_info may be a 3-tuple of None
121 | # http://docs.python.org/library/sys.html#sys.exc_info
122 | if record.exc_info and all(record.exc_info):
123 | handler = self.client.get_handler('opbeat.events.Exception')
124 |
125 | data.update(handler.capture(exc_info=record.exc_info))
126 | # data['checksum'] = handler.get_hash(data)
127 |
128 | data['level'] = record.levelno
129 | data['logger'] = record.name
130 |
131 | return self.client.capture('Message',
132 | param_message={'message': record.msg,
133 | 'params': record.args},
134 | stack=stack, data=data, extra=extra,
135 | date=date, **kwargs)
136 |
--------------------------------------------------------------------------------
/opbeat/events.py:
--------------------------------------------------------------------------------
1 | """
2 | opbeat.events
3 | ~~~~~~~~~~~~
4 |
5 | :copyright: (c) 2011-2012 Opbeat
6 |
7 | Large portions are
8 | :copyright: (c) 2010 by the Sentry Team, see AUTHORS for more details.
9 | :license: BSD, see LICENSE for more details.
10 | """
11 |
12 | import logging
13 | import sys
14 |
15 | from opbeat.utils import varmap
16 | from opbeat.utils.encoding import shorten, to_unicode
17 | from opbeat.utils.stacks import (get_culprit, get_stack_info,
18 | iter_traceback_frames)
19 |
20 | __all__ = ('BaseEvent', 'Exception', 'Message', 'Query')
21 |
22 |
23 | class BaseEvent(object):
24 | def __init__(self, client):
25 | self.client = client
26 | self.logger = logging.getLogger(__name__)
27 |
28 | def to_string(self, data):
29 | raise NotImplementedError
30 |
31 | def capture(self, **kwargs):
32 | return {}
33 |
34 |
35 | class Exception(BaseEvent):
36 | """
37 | Exceptions store the following metadata:
38 |
39 | - value: 'My exception value'
40 | - type: 'ClassName'
41 | - module '__builtin__' (i.e. __builtin__.TypeError)
42 | - frames: a list of serialized frames (see _get_traceback_frames)
43 | """
44 |
45 | def to_string(self, data):
46 | exc = data['exception']
47 | if exc['value']:
48 | return '%s: %s' % (exc['type'], exc['value'])
49 | return exc['type']
50 |
51 | def get_hash(self, data):
52 | exc = data['exception']
53 | output = [exc['type']]
54 | for frame in data['stacktrace']['frames']:
55 | output.append(frame['module'])
56 | output.append(frame['function'])
57 | return output
58 |
59 | def capture(self, exc_info=None, **kwargs):
60 | new_exc_info = False
61 | if not exc_info or exc_info is True:
62 | new_exc_info = True
63 | exc_info = sys.exc_info()
64 |
65 | if not exc_info:
66 | raise ValueError('No exception found')
67 |
68 | try:
69 | exc_type, exc_value, exc_traceback = exc_info
70 |
71 | frames = varmap(
72 | lambda k, v: shorten(v,
73 | string_length=self.client.string_max_length,
74 | list_length=self.client.list_max_length
75 | ),
76 | get_stack_info((iter_traceback_frames(exc_traceback)))
77 | )
78 |
79 | culprit = get_culprit(frames, self.client.include_paths,
80 | self.client.exclude_paths)
81 |
82 | if hasattr(exc_type, '__module__'):
83 | exc_module = exc_type.__module__
84 | exc_type = exc_type.__name__
85 | else:
86 | exc_module = None
87 | exc_type = exc_type.__name__
88 | finally:
89 | if new_exc_info:
90 | try:
91 | del exc_info
92 | del exc_traceback
93 | except Exception as e:
94 | self.logger.exception(e)
95 |
96 | return {
97 | 'level': logging.ERROR,
98 | 'culprit': culprit,
99 | 'exception': {
100 | 'value': to_unicode(exc_value),
101 | 'type': str(exc_type),
102 | 'module': str(exc_module),
103 | },
104 | 'stacktrace': {
105 | 'frames': frames
106 | },
107 | }
108 |
109 |
110 | class Message(BaseEvent):
111 | """
112 | Messages store the following metadata:
113 |
114 | - message: 'My message from %s about %s'
115 | - params: ('foo', 'bar')
116 | """
117 |
118 | def to_string(self, data):
119 | msg = data['param_message']
120 | if msg.get('params'):
121 | return msg['message'] % msg['params']
122 | return msg['message']
123 |
124 | def get_hash(self, data):
125 | msg = data['param_message']
126 | return [msg['message']]
127 |
128 | def capture(self, param_message=None, message=None, **kwargs):
129 | if message:
130 | param_message = {'message': message}
131 |
132 | params = param_message.get('params', ())
133 | data = {
134 | 'param_message': {
135 | 'message': param_message['message'],
136 | 'params': params,
137 | }
138 | }
139 | return data
140 |
141 |
142 | class Query(BaseEvent):
143 | """
144 | Messages store the following metadata:
145 |
146 | - query: 'SELECT * FROM table'
147 | - engine: 'postgesql_psycopg2'
148 | """
149 |
150 | def to_string(self, data):
151 | sql = data['query']
152 | return sql['query']
153 |
154 | def get_hash(self, data):
155 | sql = data['query']
156 | return [sql['query'], sql['engine']]
157 |
158 | def capture(self, query, engine, **kwargs):
159 | return {
160 | 'query': {
161 | 'query': query,
162 | 'engine': engine,
163 | }
164 | }
165 |
--------------------------------------------------------------------------------
/tests/handlers/logbook/logbook_tests.py:
--------------------------------------------------------------------------------
1 | import logbook
2 |
3 | from opbeat.handlers.logbook import OpbeatHandler
4 | from tests.helpers import get_tempstoreclient
5 | from tests.utils.compat import TestCase
6 |
7 |
8 | class LogbookHandlerTest(TestCase):
9 | def setUp(self):
10 | self.logger = logbook.Logger(__name__)
11 |
12 | def test_logger(self):
13 | client = get_tempstoreclient(include_paths=['tests', 'opbeat'])
14 | handler = OpbeatHandler(client)
15 | logger = self.logger
16 |
17 | with handler.applicationbound():
18 | logger.error('This is a test error')
19 |
20 | self.assertEquals(len(client.events), 1)
21 | event = client.events.pop(0)
22 | self.assertEquals(event['logger'], __name__)
23 | self.assertEquals(event['level'], "error")
24 | self.assertEquals(event['message'], 'This is a test error')
25 | self.assertFalse('stacktrace' in event)
26 | self.assertFalse('exception' in event)
27 | self.assertTrue('param_message' in event)
28 | msg = event['param_message']
29 | self.assertEquals(msg['message'], 'This is a test error')
30 | self.assertEquals(msg['params'], ())
31 |
32 | logger.warning('This is a test warning')
33 | self.assertEquals(len(client.events), 1)
34 | event = client.events.pop(0)
35 | self.assertEquals(event['logger'], __name__)
36 | self.assertEquals(event['level'], 'warning')
37 | self.assertEquals(event['message'], 'This is a test warning')
38 | self.assertFalse('stacktrace' in event)
39 | self.assertFalse('exception' in event)
40 | self.assertTrue('param_message' in event)
41 | msg = event['param_message']
42 | self.assertEquals(msg['message'], 'This is a test warning')
43 | self.assertEquals(msg['params'], ())
44 |
45 | logger.info('This is a test info with a url', extra=dict(
46 | url='http://example.com',
47 | ))
48 | self.assertEquals(len(client.events), 1)
49 | event = client.events.pop(0)
50 | self.assertEquals(event['extra']['url'], 'http://example.com')
51 | self.assertFalse('stacktrace' in event)
52 | self.assertFalse('exception' in event)
53 | self.assertTrue('param_message' in event)
54 | msg = event['param_message']
55 | self.assertEquals(msg['message'], 'This is a test info with a url')
56 | self.assertEquals(msg['params'], ())
57 |
58 | try:
59 | raise ValueError('This is a test ValueError')
60 | except ValueError:
61 | logger.info('This is a test info with an exception', exc_info=True)
62 |
63 | self.assertEquals(len(client.events), 1)
64 | event = client.events.pop(0)
65 |
66 | self.assertEquals(event['message'], 'This is a test info with an exception')
67 | self.assertTrue('stacktrace' in event)
68 | self.assertTrue('exception' in event)
69 | exc = event['exception']
70 | self.assertEquals(exc['type'], 'ValueError')
71 | self.assertEquals(exc['value'], 'This is a test ValueError')
72 | self.assertTrue('param_message' in event)
73 | msg = event['param_message']
74 | self.assertEquals(msg['message'], 'This is a test info with an exception')
75 | self.assertEquals(msg['params'], ())
76 |
77 | # test args
78 | logger.info('This is a test of %s', 'args')
79 | self.assertEquals(len(client.events), 1)
80 | event = client.events.pop(0)
81 | self.assertEquals(event['message'], 'This is a test of args')
82 | self.assertFalse('stacktrace' in event)
83 | self.assertFalse('exception' in event)
84 | self.assertTrue('param_message' in event)
85 | msg = event['param_message']
86 | self.assertEquals(msg['message'], 'This is a test of %s')
87 | self.assertEquals(msg['params'], ('args',))
88 |
89 | def test_client_arg(self):
90 | client = get_tempstoreclient(include_paths=['tests'])
91 | handler = OpbeatHandler(client)
92 | self.assertEquals(handler.client, client)
93 |
94 | def test_client_kwarg(self):
95 | client = get_tempstoreclient(include_paths=['tests'])
96 | handler = OpbeatHandler(client=client)
97 | self.assertEquals(handler.client, client)
98 |
99 | # def test_first_arg_as_dsn(self):
100 | # handler = OpbeatHandler('http://public:secret@example.com/1')
101 | # self.assertTrue(isinstance(handler.client, Client))
102 |
103 | # def test_custom_client_class(self):
104 | # handler = OpbeatHandler('http://public:secret@example.com/1', client_cls=TempStoreClient)
105 | # self.assertTrue(type(handler.client), TempStoreClient)
106 |
107 | def test_invalid_first_arg_type(self):
108 | self.assertRaises(ValueError, OpbeatHandler, object)
109 |
110 | def test_missing_client_arg(self):
111 | self.assertRaises(TypeError, OpbeatHandler)
112 |
--------------------------------------------------------------------------------
/docs/config/other.rst:
--------------------------------------------------------------------------------
1 | Configuring Other stacks
2 | =======================================
3 |
4 | .. csv-table::
5 | :class: page-info
6 |
7 | "Page updated: 23rd July 2013", ""
8 |
9 | Introduction
10 | ----------------------
11 |
12 | This document describes configuration options available to Opbeat.
13 | For available framework integrations and modules, see the sidebar on the left.
14 |
15 | .. note::
16 |
17 | Some integrations allow specifying these in a standard configuration, otherwise they are generally passed upon instantiation of the Opbeat client.
18 |
19 |
20 | Configuring the Client
21 | ----------------------
22 |
23 | Settings are specified as part of the intialization of the client.
24 |
25 | .. code::
26 | :class: language-python
27 |
28 | from opbeat import Client
29 |
30 | # Read configuration from the environment
31 | client = Client()
32 |
33 | # Configure a client manually
34 | client = Client(
35 | organization_id='',
36 | app_id='',
37 | secret_token='',
38 | )
39 |
40 | Client Arguments
41 | ----------------
42 |
43 |
44 | The following are valid arguments which may be passed to the Opbeat client:
45 |
46 | organization id
47 | ~~~~~~~~~~~~~~~~
48 |
49 | Set this to your Opbeat organization ID.
50 |
51 | .. code::
52 | :class: language-python
53 |
54 | organization_id = ''
55 |
56 | app id
57 | ~~~~~~~~~~~~~~
58 |
59 | Set this to your Opbeat app ID.
60 |
61 | .. code::
62 | :class: language-python
63 |
64 | app_id = ''
65 |
66 | secret_token
67 | ~~~~~~~~~~~~~~~~~~
68 |
69 | Set this to the secret key of the app.
70 | You can find this information on the settings page of your app
71 | at https://opbeat.com
72 |
73 | .. code::
74 | :class: language-python
75 |
76 | secret_token = ''
77 |
78 | hostname
79 | ~~~~~~~~~~~~~~
80 |
81 | This will override the ``hostname`` value for this installation. Defaults to ``socket.gethostname()``.
82 |
83 | .. code::
84 | :class: language-python
85 |
86 | hostname = 'opbeat_rocks_' + socket.gethostname()
87 |
88 | exclude_paths
89 | ~~~~~~~~~~~~~
90 |
91 | Extending this allow you to ignore module prefixes when we attempt to discover which function an error comes from (typically a view)
92 |
93 | .. code::
94 | :class: language-python
95 |
96 | exclude_paths = [
97 | 'django',
98 | 'opbeat',
99 | 'lxml.objectify',
100 | ]
101 |
102 | include_paths
103 | ~~~~~~~~~~~~~
104 |
105 | For example, in Django this defaults to your list of ``INSTALLED_APPS``, and is used for drilling down where an exception is located
106 |
107 | .. code::
108 | :class: language-python
109 |
110 | include_paths = [
111 | 'django',
112 | 'opbeat',
113 | 'lxml.objectify',
114 | ]
115 |
116 | list_max_length
117 | ~~~~~~~~~~~~~~~
118 |
119 | The maximum number of items a list-like container should store.
120 |
121 | If an iterable is longer than the specified length, the left-most elements up to length will be kept.
122 |
123 | .. note:: This affects sets as well, which are unordered.
124 |
125 | .. code::
126 | :class: language-python
127 |
128 | list_max_length = 50
129 |
130 | string_max_length
131 | ~~~~~~~~~~~~~~~~~
132 |
133 | The maximum characters of a string that should be stored.
134 |
135 | If a string is longer than the given length, it will be truncated down to the specified size.
136 |
137 | .. code::
138 | :class: language-python
139 |
140 | list_max_length = 200
141 |
142 | auto_log_stacks
143 | ~~~~~~~~~~~~~~~
144 |
145 | Should opbeat automatically log frame stacks (including locals) all calls as it would for exceptions.
146 |
147 | .. code::
148 | :class: language-python
149 |
150 | auto_log_stacks = True
151 |
152 | timeout
153 | ~~~~~~~
154 |
155 | If supported, the timeout value for sending messages to remote.
156 |
157 | .. code::
158 | :class: language-python
159 |
160 | timeout = 5
161 |
162 | processors
163 | ~~~~~~~~~~
164 |
165 | A list of processors to apply to events before sending them to the Opbeat server. Useful for sending
166 | additional global state data or sanitizing data that you want to keep off of the server.
167 |
168 | .. code::
169 | :class: language-python
170 |
171 | processors = (
172 | 'opbeat.processors.SanitizePasswordsProcessor',
173 | )
174 |
175 | Sanitizing Data
176 | ---------------
177 |
178 | Several processors are included with opbeat to assist in data sanitiziation. These are configured with the
179 | ``processors`` value.
180 |
181 | .. data:: opbeat.processors.SanitizePasswordsProcessor
182 |
183 | Removes all keys which resemble ``password`` or ``secret`` within stacktrace contexts, and HTTP
184 | bits (such as cookies, POST data, the querystring, and environment).
185 |
186 | .. data:: opbeat.processors.RemoveStackLocalsProcessor
187 |
188 | Removes all stacktrace context variables. This will cripple the functionality of Opbeat, as you'll only
189 | get raw tracebacks, but it will ensure no local scoped information is available to the server.
190 |
191 | .. data:: opbeat.processors.RemovePostDataProcessor
192 |
193 | Removes the ``body`` of all HTTP data.
194 |
195 |
196 | Usage
197 | -----
198 |
199 | .. autoclass:: opbeat.Client
200 | :members:
201 |
--------------------------------------------------------------------------------