diff --git a/.codeclimate.yml b/.codeclimate.yml new file mode 100644 index 00000000..4d91d7f8 --- /dev/null +++ b/.codeclimate.yml @@ -0,0 +1,17 @@ +--- +engines: + duplication: + enabled: true + config: + languages: + - python + fixme: + enabled: true + pep8: + enabled: true + radon: + enabled: true +ratings: + paths: + - "**.py" +exclude_paths: [] diff --git a/.flake8 b/.flake8 new file mode 100644 index 00000000..f8cb73ee --- /dev/null +++ b/.flake8 @@ -0,0 +1,4 @@ +[flake8] +max-line-length = 100 +max-complexity = 10 +exclude = test/* diff --git a/.gitignore b/.gitignore index b35cc152..d0e568ed 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,7 @@ rollbar.egg-info/ *.egg .eggs/ .idea/ +*~ +Pipfile +Pipfile.lock +.pytest_cache/ diff --git a/.travis.yml b/.travis.yml index 5a94c1ae..a45c5483 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,39 +2,105 @@ sudo: false language: python matrix: include: - - python: "2.6" + - python: "2.7" env: FLASK_VERSION=0.9 - python: "2.7" env: FLASK_VERSION=0.10.1 + - python: "2.7" + env: FLASK_VERSION=0.11.1 + - python: "2.7" + env: FLASK_VERSION=0.12.2 + - python: "3.3" + env: FLASK_VERSION=0.10.1 - python: "3.3" + env: FLASK_VERSION=0.11.1 + - python: "3.3" + env: FLASK_VERSION=0.12.2 + - python: "3.4" env: FLASK_VERSION=0.10.1 - python: "3.4" + env: FLASK_VERSION=0.11.1 + - python: "3.4" + env: FLASK_VERSION=0.12.2 + - python: "3.5" env: FLASK_VERSION=0.10.1 - python: "3.5" + env: FLASK_VERSION=0.11.1 + - python: "3.4" + env: FLASK_VERSION=0.12.2 + - python: "3.6" env: FLASK_VERSION=0.10.1 + - python: "3.6" + env: FLASK_VERSION=0.11.1 + - python: "3.6" + env: FLASK_VERSION=0.12.2 - python: "2.7" - env: TWISTED_VERSION=15.4 + env: TWISTED_VERSION=15.5.0 + - python: "2.7" + env: TWISTED_VERSION=16.1.1 + - python: "2.7" + env: TWISTED_VERSION=16.2.0 + - python: "2.7" + env: TWISTED_VERSION=16.3.0 + - python: "2.7" + env: TWISTED_VERSION=16.4.0 + - python: "2.7" + env: TWISTED_VERSION=16.5.0 + - python: "2.7" + env: TWISTED_VERSION=16.6.0 + - python: "2.7" + env: TWISTED_VERSION=17.1.0 - - python: "2.6" - env: DJANGO_VERSION=1.4 - python: "2.7" - env: DJANGO_VERSION=1.4 - - python: "3.2" - env: DJANGO_VERSION=1.7 + env: DJANGO_VERSION=1.6.11 + - python: "2.7" + env: DJANGO_VERSION=1.7.11 + - python: "2.7" + env: DJANGO_VERSION=1.8.18 + - python: "2.7" + env: DJANGO_VERSION=1.9.13 + - python: "2.7" + env: DJANGO_VERSION=1.10.7 + - python: "2.7" + env: DJANGO_VERSION=1.11.1 + + - python: "3.3" + env: DJANGO_VERSION=1.6.11 - python: "3.3" - env: DJANGO_VERSION=1.8 + env: DJANGO_VERSION=1.8.18 + + - python: "3.4" + env: DJANGO_VERSION=1.7.11 + - python: "3.4" + env: DJANGO_VERSION=1.8.18 + - python: "3.4" + env: DJANGO_VERSION=1.9.13 - python: "3.4" - env: DJANGO_VERSION=1.8 + env: DJANGO_VERSION=1.10.7 + - python: "3.4" + env: DJANGO_VERSION=1.11.1 + + - python: "3.5" + env: DJANGO_VERSION=1.8.18 + - python: "3.5" + env: DJANGO_VERSION=1.9.13 + - python: "3.5" + env: DJANGO_VERSION=1.10.7 - python: "3.5" - env: DJANGO_VERSION=1.8 + env: DJANGO_VERSION=1.11.1 + + - python: "3.6" + env: DJANGO_VERSION=1.11.1 + + - python: "3.6" + env: PYRAMID_VERSION=1.9.2 install: - if [ -v FLASK_VERSION ]; then pip install Flask==$FLASK_VERSION; fi - - if [ -v TWISTED_VERSION ]; then pip install Twisted==$TWISTED_VERSION service_identity pyOpenSSL; fi + - if [ -v TWISTED_VERSION ]; then pip install Twisted==$TWISTED_VERSION treq; fi - if [ -v DJANGO_VERSION ]; then pip install Django==$DJANGO_VERSION; fi + - if [ -v PYRAMID_VERSION ]; then pip install pyramid==$PYRAMID_VERSION; fi script: - python setup.py test - - diff --git a/CHANGELOG.md b/CHANGELOG.md index e2c232e2..5521d1b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,146 @@ # Change Log +The change log has moved to this repo's [GitHub Releases Page](https://github.com/rollbar/pyrollbar/releases). + +**0.14.0** + +- Create the configuration options, `capture_username` and `capture_email`. Prior to this release, + if we gather person data automatically, we would try to capture the id, email, and username. + Starting with this release by default we will only capture the id. If you set `capture_username` + to `True` then we will also attempt to capture the username. Similarly for `capture_email` with + the email. (See [#262](https://github.com/rollbar/pyrollbar/pull/262)) +- Create the configuration option `capture_ip`. This can take one of three values: `True`, + `'anonymize'`, or `False`. This controls how we handle IP addresses that are captured from + requests. If `True`, then we will send the full IP address. This is the current behaviour and the + default. If set to the string `'anonymize'` which is also available as the constant `ANONYMIZE` on + the `rollbar` module, we will mask out the least significant bits of the IP address. If set to + `False`, then we will not capture the IP address. (See [#262](https://github.com/rollbar/pyrollbar/pull/262)) +- Fix `request.files_keys` for Flask [#263](https://github.com/rollbar/pyrollbar/pull/263) +- If you call `init` multiple times we will update the settings at each call. Prior to + this release we emitted a warning and did not update settings. [#259](https://github.com/rollbar/pyrollbar/pull/259) +- Better Tornado support [#256](https://github.com/rollbar/pyrollbar/pull/256) + +**0.13.18** + +- See Release Notes + +**0.13.17** + +- Fix deprecation warning related to Logging.warn +- Fix bug where non-copyable objects could cause an exception if they end up trying to get passed to + one of the logging methods. +- Fix bug where both `trace` and `trace_chain` could appear in the final payload, which is not + allowed by the API. + +**0.13.16** + +- Fix PyPI documentation + +**0.13.15** + +- Fix shortener issue for Python 3 + +**0.13.14** + +- Fix bug that caused some payload objects to be turned into the wrong type when +shortening is applied. This would lead to API rejections. See [#200](https://github.com/rollbar/pyrollbar/pull/200) +- Add `suppress_reinit_warning` option if you want to allow calling init twice. See [#198](https://github.com/rollbar/pyrollbar/pull/198) +- Pass through keyword arguments from the logging handler to the underling Rollbar init call. See + [#203](https://github.com/rollbar/pyrollbar/pull/203) + +**0.13.13** + +- Add support for AWS Lambda. See [#191](https://github.com/rollbar/pyrollbar/pull/191) + +**0.13.12** + +- Remove the Django request body from the payload as it can contain sensitive data. See [#174](https://github.com/rollbar/pyrollbar/pull/174) +- Allow users to shorten arbitrary parts of the payload. See [#173](https://github.com/rollbar/pyrollbar/pull/173) +- Fix a Django deprecation warning. See [#165](https://github.com/rollbar/pyrollbar/pull/165) + +**0.13.11** + +- Handle environments where `sys.argv` does not exist. See [#131](https://github.com/rollbar/pyrollbar/pull/131) + +**0.13.10** + +- Gather request method from WebOb requests. See [#152](https://github.com/rollbar/pyrollbar/pull/152) + +**0.13.9** + +- Change `_check_config()` to deal with agent handler. See [#147](https://github.com/rollbar/pyrollbar/pull/147) +- Fix settings values not being booleans in Pyramid. See [#150](https://github.com/rollbar/pyrollbar/pull/150) + +**0.13.8** + +- Fix regression from 0.13.7. See [#141](https://github.com/rollbar/pyrollbar/pull/141) + +**0.13.7** + +- Update Django middleware to support Django 1.10+. See [#138](https://github.com/rollbar/pyrollbar/pull/138) + +**0.13.6** + +- Fixed a referenced before assignment in the failsafe. See [#136](https://github.com/rollbar/pyrollbar/pull/136) + +**0.13.5** + +- Fixed record message formatting issues breaking the log handler's history. See [#135](https://github.com/rollbar/pyrollbar/pull/135) + +**0.13.4** + +- Fixed failsafe handling for payloads that are too large. See [#133](https://github.com/rollbar/pyrollbar/pull/133) + +**0.13.3** + +- Improved handling of Enums. See [#121](https://github.com/rollbar/pyrollbar/pull/121) + +**0.13.2** + +- Improved handling of Nan and (Negative)Infinity. See [#117](https://github.com/rollbar/pyrollbar/pull/117) +- RollbarHandler now ignores log records from Rollbar. See [#118](https://github.com/rollbar/pyrollbar/pull/118) + +**0.13.1** + +- Failsafe handling for payloads that are too large. See [#116](https://github.com/rollbar/pyrollbar/pull/116) + - Failsafe Behavior + - Log an error containing the original payload and the UUID from it + - Send a new payload to Rollbar with the custom attribute containing the UUID and host from the original payload + +**0.13.0** + +- Frame payload refactor and varargs scrubbing. See [#113](https://github.com/rollbar/pyrollbar/pull/113) + - Frame Payload Changes + - remove args and kwargs + - add argspec as the list of argument names to the function call + - add varargspec as the name of the list containing the arbitrary unnamed positional arguments to the function call if any exist + - add keywordspec as the name of the object containing the arbitrary keyword arguments to the function call if any exist + - Other Changes: + - Arguments with default values are no longer removed from args and placed into kwargs + - varargs are now scrubbable and scrubbed by default +- Switched to using a Session object to perform HTTPS requests to optimize for keepalive connections. See [#114](https://github.com/rollbar/pyrollbar/pull/114) + +**0.12.1** + +- Keep blank values from request query strings when scrubbing URLs. See [#110](https://github.com/rollbar/pyrollbar/pull/110) + +**0.12.0** + +- Fix and update Twisted support. See [#109](https://github.com/rollbar/pyrollbar/pull/109) + - **Breaking Changes**: [treq](https://github.com/twisted/treq) is now required for using Twisted with pyrollbar. + +**0.11.6** + +- Improve object handling for SQLAlchemy. See [#108](https://github.com/rollbar/pyrollbar/pull/108) + +**0.11.5** + +- Fixed a bug when custom `__repr__()` calls resulted in an exception being thrown. See [#102](https://github.com/rollbar/pyrollbar/pull/102) + **0.11.4** -- revert changes from 0.11.3 since they ended-up having the unintended side effect by that exceptions messages weren't processing as expected. -- update settings in init first so that custom scrub_fields entries are handled correctly +- Revert changes from 0.11.3 since they ended-up having the unintended side effect by that exceptions messages weren't processing as expected. +- Update settings in init first so that custom scrub_fields entries are handled correctly **0.11.3** diff --git a/README.md b/README.md index 474ad9e9..0aaece7b 100644 --- a/README.md +++ b/README.md @@ -1,436 +1,25 @@ -# Rollbar notifier for Python [![Build Status](https://travis-ci.org/rollbar/pyrollbar.png?branch=v0.11.0)](https://travis-ci.org/rollbar/pyrollbar) +# Pyrollbar +[![Build Status](https://api.travis-ci.org/rollbar/pyrollbar.png?branch=v0.14.2)](https://travis-ci.org/rollbar/pyrollbar) - Python notifier for reporting exceptions, errors, and log messages to [Rollbar](https://rollbar.com). - +# Setup Instructions -## Quick start +1. [Sign up for a Rollbar account](https://rollbar.com/signup) +2. Follow the [Quick Start](https://docs.rollbar.com/docs/python#section-quick-start) instructions in our [Python SDK docs](https://docs.rollbar.com/docs/python) to install pyrollbar and configure it for your platform. -Install using pip: +# Usage and Reference -```bash -pip install rollbar -``` - -```python -import rollbar -rollbar.init('POST_SERVER_ITEM_ACCESS_TOKEN', 'production') # access_token, environment - -try: - main_app_loop() -except IOError: - rollbar.report_message('Got an IOError in the main loop', 'warning') -except: - # catch-all - rollbar.report_exc_info() -``` - -## Requirements - -- Python 2.6, 2.7, 3.2, 3.3, 3.4, or 3.5 -- requests 0.12+ -- A Rollbar account - -## Configuration - -### Django - -In your ``settings.py``, add ``'rollbar.contrib.django.middleware.RollbarNotifierMiddleware'`` as the last item in ``MIDDLEWARE_CLASSES``: - -```python -MIDDLEWARE_CLASSES = ( - # ... other middleware classes ... - 'rollbar.contrib.django.middleware.RollbarNotifierMiddleware', -) -``` - -Add these configuration variables in ``settings.py``: - -```python -ROLLBAR = { - 'access_token': 'POST_SERVER_ITEM_ACCESS_TOKEN', - 'environment': 'development' if DEBUG else 'production', - 'branch': 'master', - 'root': '/absolute/path/to/code/root', -} -``` - - -Be sure to replace ```POST_SERVER_ITEM_ACCESS_TOKEN``` with your project's ```post_server_item``` access token, which you can find in the Rollbar.com interface. - -Check out the [Django example](https://github.com/rollbar/pyrollbar/tree/master/rollbar/examples/django). - -### Pyramid - -In your ``ini`` file (e.g. ``production.ini``), add ``rollbar.contrib.pyramid`` to the end of your ``pyramid.includes``: - -```ini -[app:main] -pyramid.includes = - pyramid_debugtoolbar - rollbar.contrib.pyramid -``` - -And add these rollbar configuration variables: - -```ini -[app:main] -rollbar.access_token = POST_SERVER_ITEM_ACCESS_TOKEN -rollbar.environment = production -rollbar.branch = master -rollbar.root = %(here)s -``` - -Be sure to replace ```POST_SERVER_ITEM_ACCESS_TOKEN``` with your project's ```post_server_item``` access token, which you can find in the Rollbar.com interface. - -The above will configure Rollbar to catch and report all exceptions that occur inside your Pyramid app. However, in order to catch exceptions in middlewares or in Pyramid itself, you will also need to wrap your app inside a ```pipeline``` with Rollbar as a ```filter```. - -To do this, first change your ```ini``` file to use a ```pipeline```. Change this: - -```ini -[app:main] -#... -``` - -To: - -```ini -[pipeline:main] -pipeline = - rollbar - YOUR_APP_NAME - -[app:YOUR_APP_NAME] -pyramid.includes = - pyramid_debugtoolbar - rollbar.contrib.pyramid - -rollbar.access_token = POST_SERVER_ITEM_ACCESS_TOKEN -rollbar.environment = production -rollbar.branch = master -rollbar.root = %(here)s - -[filter:rollbar] -use = egg:rollbar#pyramid -access_token = POST_SERVER_ITEM_ACCESS_TOKEN -environment = production -branch = master -root = %(here)s -``` - -Note that the access_token, environment, and other Rollbar config params do need to be present in both the ```app``` section and the ```filter``` section. - - -### Flask - -Check out [rollbar-flask-example](https://github.com/rollbar/rollbar-flask-example). - - -### Bottle - -Import the plugin and install! -Can be installed globally or on a per route basis. - -```python -import bottle -from rollbar.contrib.bottle import RollbarBottleReporter - -rbr = RollbarBottleReporter(access_token='POST_SERVER_ITEM_ACCESS_TOKEN', environment='production') #setup rollbar - -bottle.install(rbr) #install globally - -@bottle.get('/') -def raise_error(): - ''' - When navigating to /, we'll get a regular 500 page from bottle, - as well as have the error below listed on Rollbar. - ''' - raise Exception('Hello, Rollbar!') - -if __name__ == '__main__': - bottle.run(host='localhost', port=8080) -``` - - -Be sure to replace ```POST_SERVER_ITEM_ACCESS_TOKEN``` with your project's ```post_server_item``` access token, which you can find in the Rollbar.com interface. - - -### Twisted - -Check out the [Twisted example](https://github.com/rollbar/pyrollbar/tree/master/rollbar/examples/twisted). - - -### Other - -For generic Python or a non-Django/non-Pyramid framework just initialize the Rollbar library with your access token and environment. - -```python -rollbar.init('POST_SERVER_ITEM_ACCESS_TOKEN', environment='production', **other_config_params) -``` - -Other options can be passed as keyword arguments. See the reference below for all options. - -### Command-line usage - -pyrollbar comes with a command-line tool that can be used with other UNIX utilities to create an ad-hoc monitoring solution. - -e.g. Report all 5xx haproxy requests as ```warning``` - -```bash -tail -f /var/log/haproxy.log | awk '{print $11,$0}' | grep '^5' | awk '{$1="";print "warning",$0}' | rollbar -t POST_SERVER_ITEM_ACCESS_TOKEN -e production -v -``` - -e.g. Test an access token - -```bash -rollbar -t POST_SERVER_ITEM_ACCESS_TOKEN -e test debug testing access token -``` - -#### Reference - -``` -$ rollbar --help -Usage: rollbar [options] - -Options: - --version show program's version number and exit - -h, --help show this help message and exit - -t ACCESS_TOKEN, --access_token=ACCESS_TOKEN - You project's access token from rollbar.com. - -e ENVIRONMENT, --environment=ENVIRONMENT - The environment to report errors and messages to. - -u ENDPOINT_URL, --url=ENDPOINT_URL - The Rollbar API endpoint url to send data to. - -m HANDLER, --handler=HANDLER - The method in which to report errors. - -v, --verbose Print verbose output. -``` - -## Usage - -The Django, Pyramid, Flask, and Bottle integrations will automatically report uncaught exceptions to Rollbar. - -### Exceptions - -To report a caught exception to Rollbar, use ```rollbar.report_exc_info()```: - -```python -try: - do_something() -except: - rollbar.report_exc_info(sys.exc_info()) - # or if you have a webob-like request object, pass that as well: - # rollbar.report_exc_info(sys.exc_info(), request) -``` - -### Logging - -You can also send any other log messages you want, using ```rollbar.report_message()```: - -```python -try: - do_something() -except IOError: - rollbar.report_message('Got an IOError while trying to do_something()', 'warning') - # report_message() also accepts a request object: - #rollbar.report_message('message here', 'warning', request) -``` - -### Examples - -Here's a full example, integrating into a simple Gevent app. - -```python -""" -Sample Gevent application with Rollbar integration. -""" -import sys -import logging - -from gevent.pywsgi import WSGIServer -import rollbar -import webob - -# configure logging so that rollbar's log messages will appear -logging.basicConfig() - -def application(environ, start_response): - request = webob.Request(environ) - status = '200 OK' - headers = [('Content-Type', 'text/html')] - start_response(status, headers) - - yield '

Hello world

' - - # extra fields we'd like to send along to rollbar (optional) - extra_data = {'datacenter': 'us1', 'app' : {'version': '1.1'}} - - try: - # will raise a NameError about 'bar' not being defined - foo = bar - except: - # report full exception info - rollbar.report_exc_info(sys.exc_info(), request, extra_data=extra_data) - - # and/or, just send a string message with a level - rollbar.report_message("Here's a message", 'info', request, extra_data=extra_data) - - yield '

Caught an exception

' - -# initialize rollbar with an access token and environment name -rollbar.init('POST_SERVER_ITEM_ACCESS_TOKEN', 'development') - -# now start the wsgi server -WSGIServer(('', 8000), application).serve_forever() -``` - -## Configuration reference - -
-
access_token
-
Access token from your Rollbar project -
-
agent.log_file
-
If ```handler``` is ```agent```, the path to the log file. Filename must end in ```.rollbar``` -
-
branch
-
Name of the checked-out branch. - -Default: ```master``` - -
-
code_version
-
A string describing the current code revision/version (i.e. a git sha). Max 40 characters. Default `None`
-
enabled
-
Controls whether or not Rollbar will report any data - -Default: ```True``` - -
-
endpoint
-
URL items are posted to. - -Default: ```https://api.rollbar.com/api/1/item/``` - -
-
environment
-
Environment name. Any string up to 255 chars is OK. For best results, use "production" for your production environment. -
-
exception_level_filters
-
List of tuples in the form ```(class, level)``` where ```class``` is an Exception class you want to always filter to the respective ```level```. Any subclasses of the given ```class``` will also be matched. - -Valid levels: ```'critical'```, ```'error'```, ```'warning'```, ```'info'```, ```'debug'``` and ```'ignored'```. - -Use ```'ignored'``` if you want an Exception (sub)class to never be reported to Rollbar. - -Any exceptions not found in this configuration setting will default to ```'error'```. - -Django ```settings.py``` example (and Django default): - -```python -from django.http import Http404 - -ROLLBAR = { - ... - 'exception_level_filters': [ - (Http404, 'warning') - ] -} -``` - -In a Pyramid ``ini`` file, define each tuple as an individual whitespace delimited line, for example: - -``` -rollbar.exception_level_filters = - pyramid.exceptions.ConfigurationError critical - #... -``` - -
-
handler
-
The method for reporting rollbar items to api.rollbar.com +For complete usage instructions and configuration reference, see our [Python SDK docs](https://docs.rollbar.com/docs/python). -One of: - -- blocking -- runs in main thread -- thread -- spawns a new thread -- agent -- writes messages to a log file for consumption by rollbar-agent -- tornado -- uses the Tornado async library to send the payload -- gae -- uses the Google AppEngineFetch library to send the payload -- twisted -- uses the Twisted event-driven networking library to send the payload - -Default: ```thread``` - -
-
locals
-
Configuration for collecting local variables. A dictionary: -
-
enabled
-
If `True`, variable values will be collected for stack traces. Default `True`.
-
safe_repr
-
If `True`, non-built-in objects will be serialized into just their class name. If `False` `repr(obj)` - will be used for serialization. Default `True`.
-
sizes
-
Dictionary of configuration describing the max size to repr() for each type. -
-
maxdict
-
Default 10
-
maxarray
-
Default 10
-
maxlist
-
Default 10
-
maxtuple
-
Default 10
-
maxset
-
Default 10
-
maxfrozenset
-
Default 10
-
maxdeque
-
Default 10 -
maxstring
-
Default 100
-
maxlong
-
Default 40
-
maxother
-
Default 100
-
-
-
whitelisted_types
-
A list of `type` objects, (e.g. `type(my_class_instance)` or `MyClass`) that will be serialized using - `repr()`. Default `[]`
-
-
-
root
-
Absolute path to the root of your application, not including the final ```/```. -
-
scrub_fields
-
List of sensitive field names to scrub out of request params and locals. Values will be replaced with asterisks. If overriding, make sure to list all fields you want to scrub, not just fields you want to add to the default. Param names are converted to lowercase before comparing against the scrub list. - -Default: ```['pw', 'passwd', 'password', 'secret', 'confirm_password', 'confirmPassword', 'password_confirmation', 'passwordConfirmation', 'access_token', 'auth', 'authentication']``` - -
-
timeout
-
Timeout for any HTTP requests made to the Rollbar API (in seconds). - -Default: ```3``` - -
-
allow_logging_basic_config
-
When True, ```logging.basicConfig()``` will be called to set up the logging system. Set to False to skip this call. If using Flask, you'll want to set to ```False```. If using Pyramid or Django, ```True``` should be fine. - -Default: ```True``` - -
-
+# Release History & Changelog +See our [Releases](https://github.com/rollbar/pyrollbar/releases) page for a list of all releases, including changes. ## Help / Support If you run into any issues, please email us at [support@rollbar.com](mailto:support@rollbar.com) -You can also find us in IRC: [#rollbar on chat.freenode.net](irc://chat.freenode.net/rollbar) - For bug reports, please [open an issue on GitHub](https://github.com/rollbar/pyrollbar/issues/new). diff --git a/README.rst b/README.rst new file mode 100644 index 00000000..ade1728b --- /dev/null +++ b/README.rst @@ -0,0 +1,41 @@ +Pyrollbar |Build Status| +==================== + +Python notifier for reporting exceptions, errors, and log messages to `Rollbar `__. + +Setup Instructions +------------------- +1. `Sign up for a Rollbar account `__. +2. Follow the `Quick Start `__ instructions in our `Python SDK docs `__ to install pyrollbar and configure it for your platform. + +Usage and Reference +------------------- + +For complete usage instructions and configuration reference, see our `Python SDK docs `__. + +Release History & Changelog +---------------------------- + +See our `Releases `__ page for a list of all releases, including changes. + +Help / Support +--------------- + +If you run into any issues, please email us at `support@rollbar.com `__ + +For bug reports, please `open an issue on GitHub `__. + + +Contributing +------------- + +1. Fork it +2. Create your feature branch (``git checkout -b my-new-feature``). +3. Commit your changes (``git commit -am 'Added some feature'``) +4. Push to the branch (``git push origin my-new-feature``) +5. Create new Pull Request + +Tests are in ``rollbar/test``. To run the tests: ``python setup.py test`` + +.. |Build Status| image:: https://api.travis-ci.org/rollbar/pyrollbar.png?branch=v0.14.2 + :target: https://travis-ci.org/rollbar/pyrollbar diff --git a/THANKS.md b/THANKS.md index 1b796819..9958a9ea 100644 --- a/THANKS.md +++ b/THANKS.md @@ -14,3 +14,5 @@ Huge thanks to the following contributors (by github username). For the most up- - [rhcarvalho](https://github.com/rhcarvalho) - [tschieggm](https://github.com/tschieggm) - [zvirusz](https://github.com/zvirusz) +- [benkuhn](https://github.com/benkuhn) +- [pmourlanne](https://github.com/pmourlanne) diff --git a/default.nix b/default.nix new file mode 100644 index 00000000..253f06dc --- /dev/null +++ b/default.nix @@ -0,0 +1,19 @@ +{ + pkgs ? import {}, + python ? pkgs.python36, +}: + +with pkgs; +with python.pkgs; + +buildPythonPackage rec { + name = "pyrollbar"; + src = builtins.filterSource (path: type: + type != "unknown" && + baseNameOf path != ".git" && + baseNameOf path != "result" && + !(pkgs.lib.hasSuffix ".nix" path) + ) ./.; + propagatedBuildInputs = [requests six]; +} + diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 5781632e..00000000 --- a/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -requests>=0.12.1 -six==1.9.0 diff --git a/rollbar/__init__.py b/rollbar/__init__.py index 1118d929..39c00aec 100644 --- a/rollbar/__init__.py +++ b/rollbar/__init__.py @@ -4,9 +4,8 @@ from __future__ import absolute_import from __future__ import unicode_literals -__version__ = '0.11.5+pix' - import copy +import functools import inspect import json import logging @@ -18,17 +17,23 @@ import traceback import types import uuid - import wsgiref.util -import requests +import requests import six -from rollbar.lib import dict_merge, map, parse_qs, text, urljoin, urlparse, iteritems +from rollbar.lib import events, filters, dict_merge, parse_qs, text, transport, urljoin, iteritems +__version__ = '0.14.5+pix' +__log_name__ = 'rollbar' +log = logging.getLogger(__log_name__) -log = logging.getLogger(__name__) - +try: + # 2.x + import Queue as queue +except ImportError: + # 3.x + import queue # import request objects from various frameworks, if available try: @@ -56,7 +61,7 @@ del ImproperlyConfigured try: - from werkzeug.wrappers import Request as WerkzeugRequest + from werkzeug.wrappers import BaseRequest as WerkzeugRequest except (ImportError, SyntaxError): WerkzeugRequest = None @@ -75,77 +80,30 @@ except ImportError: BottleRequest = None +try: + from sanic.request import Request as SanicRequest +except ImportError: + SanicRequest = None + try: from google.appengine.api.urlfetch import fetch as AppEngineFetch except ImportError: AppEngineFetch = None + def passthrough_decorator(func): def wrap(*args, **kwargs): return func(*args, **kwargs) return wrap try: - from tornado.gen import coroutine as tornado_coroutine from tornado.httpclient import AsyncHTTPClient as TornadoAsyncHTTPClient except ImportError: - tornado_coroutine = passthrough_decorator TornadoAsyncHTTPClient = None try: - from twisted.internet import reactor - from twisted.internet.defer import inlineCallbacks, Deferred, returnValue, succeed - from twisted.internet.protocol import Protocol + import treq from twisted.python import log as twisted_log - from twisted.web.client import Agent as TwistedHTTPClient - from twisted.web.http_headers import Headers as TwistedHeaders - from twisted.web.iweb import IBodyProducer - - from zope.interface import implementer - - - try: - # Verify we can make HTTPS requests with Twisted. - # From http://twistedmatrix.com/documents/12.0.0/core/howto/ssl.html - from OpenSSL import SSL - except ImportError: - log.exception('Rollbar requires SSL to work with Twisted') - raise - - - @implementer(IBodyProducer) - class StringProducer(object): - - def __init__(self, body): - self.body = body - self.length = len(body) - - def startProducing(self, consumer): - consumer.write(self.body) - return succeed(None) - - def pauseProducing(self): - pass - - def stopProducing(self): - pass - - - class ResponseAccumulator(Protocol): - def __init__(self, length, finished): - self.remaining = length - self.finished = finished - self.response = '' - - def dataReceived(self, bytes): - if self.remaining: - chunk = bytes[:self.remaining] - self.response += chunk - self.remaining -= len(chunk) - - def connectionLost(self, reason): - self.finished.callback(self.response) - def log_handler(event): """ @@ -165,15 +123,17 @@ def log_handler(event): except: log.exception('Error while reporting to Rollbar') - # Add Rollbar as a log handler which will report uncaught errors twisted_log.addObserver(log_handler) except ImportError: - TwistedHTTPClient = None - inlineCallbacks = passthrough_decorator - StringProducer = None + treq = None + +try: + from falcon import Request as FalconRequest +except ImportError: + FalconRequest = None def get_request(): @@ -235,6 +195,7 @@ def _get_pylons_request(): VERSION = __version__ DEFAULT_ENDPOINT = 'https://api.rollbar.com/api/1/' DEFAULT_TIMEOUT = 3 +ANONYMIZE = 'anonymize' DEFAULT_LOCALS_SIZES = { 'maxlevel': 5, @@ -287,32 +248,44 @@ def _get_pylons_request(): 'locals': { 'enabled': True, 'safe_repr': True, + 'scrub_varargs': True, 'sizes': DEFAULT_LOCALS_SIZES, 'whitelisted_types': [] }, - 'verify_https': True + 'verify_https': True, + 'shortener_keys': [], + 'suppress_reinit_warning': False, + 'capture_email': False, + 'capture_username': False, + 'capture_ip': True, + 'log_all_rate_limited_items': True, + 'http_proxy': None, + 'http_proxy_user': None, + 'http_proxy_password': None, } +_CURRENT_LAMBDA_CONTEXT = None +_LAST_RESPONSE_STATUS = None + # Set in init() _transforms = [] _serialize_transform = None _initialized = False -# Do not call repr() on these types while gathering local variables -blacklisted_local_types = [] - +from rollbar.lib.transforms.scrub_redact import REDACT_REF from rollbar.lib import transforms from rollbar.lib.transforms.scrub import ScrubTransform from rollbar.lib.transforms.scruburl import ScrubUrlTransform +from rollbar.lib.transforms.scrub_redact import ScrubRedactTransform from rollbar.lib.transforms.serializable import SerializableTransform from rollbar.lib.transforms.shortener import ShortenerTransform ## public api -def init(access_token, environment='production', **kw): +def init(access_token, environment='production', scrub_fields=None, url_fields=None, **kw): """ Saves configuration variables in this module's SETTINGS. @@ -324,21 +297,26 @@ def init(access_token, environment='production', **kw): 'staging', 'yourname' **kw: provided keyword arguments will override keys in SETTINGS. """ - global SETTINGS, agent_log, _initialized, _transforms, _serialize_transform + global SETTINGS, agent_log, _initialized, _transforms, _serialize_transform, _threads + if scrub_fields is not None: + SETTINGS['scrub_fields'] = list(scrub_fields) + if url_fields is not None: + SETTINGS['url_fields'] = list(url_fields) + + # Merge the extra config settings into SETTINGS + SETTINGS = dict_merge(SETTINGS, kw) if _initialized: # NOTE: Temp solution to not being able to re-init. # New versions of pyrollbar will support re-initialization # via the (not-yet-implemented) configure() method. - log.warn('Rollbar already initialized. Ignoring re-init.') + if not SETTINGS.get('suppress_reinit_warning'): + log.warning('Rollbar already initialized. Ignoring re-init.') return SETTINGS['access_token'] = access_token SETTINGS['environment'] = environment - # Merge the extra config settings into SETTINGS - SETTINGS = dict_merge(SETTINGS, kw) - if SETTINGS.get('allow_logging_basic_config'): logging.basicConfig() @@ -354,13 +332,18 @@ def init(access_token, environment='production', **kw): _serialize_transform = SerializableTransform(safe_repr=SETTINGS['locals']['safe_repr'], whitelist_types=SETTINGS['locals']['whitelisted_types']) _transforms = [ + ScrubRedactTransform(), _serialize_transform, ScrubTransform(suffixes=[(field,) for field in SETTINGS['scrub_fields']], redact_char='*'), ScrubUrlTransform(suffixes=[(field,) for field in SETTINGS['url_fields']], params_to_scrub=SETTINGS['scrub_fields']) ] - # A list of key prefixes to apply our shortener transform to + # A list of key prefixes to apply our shortener transform to. The request + # being included in the body key is old behavior and is being retained for + # backwards compatibility. shortener_keys = [ + ('request', 'POST'), + ('request', 'json'), ('body', 'request', 'POST'), ('body', 'request', 'json'), ] @@ -371,20 +354,44 @@ def init(access_token, environment='production', **kw): shortener_keys.append(('body', 'trace', 'frames', '*', 'kwargs', '*')) shortener_keys.append(('body', 'trace', 'frames', '*', 'locals', '*')) + shortener_keys.extend(SETTINGS['shortener_keys']) + shortener = ShortenerTransform(safe_repr=SETTINGS['locals']['safe_repr'], keys=shortener_keys, **SETTINGS['locals']['sizes']) _transforms.append(shortener) + _threads = queue.Queue() + events.reset() + filters.add_builtin_filters(SETTINGS) _initialized = True +def lambda_function(f): + """ + Decorator for making error handling on AWS Lambda easier + """ + @functools.wraps(f) + def wrapper(event, context): + global _CURRENT_LAMBDA_CONTEXT + _CURRENT_LAMBDA_CONTEXT = context + try: + result = f(event, context) + return wait(lambda: result) + except: + cls, exc, trace = sys.exc_info() + report_exc_info((cls, exc, trace.tb_next)) + wait() + raise + return wrapper + + def report_exc_info(exc_info=None, request=None, extra_data=None, payload_data=None, level=None, **kw): """ Reports an exception to Rollbar, using exc_info (from calling sys.exc_info()) exc_info: optional, should be the result of calling sys.exc_info(). If omitted, sys.exc_info() will be called here. - request: optional, a WebOb or Werkzeug-based request object. + request: optional, a WebOb, Werkzeug-based or Sanic request object. extra_data: optional, will be included in the 'custom' section of the payload payload_data: optional, dict that will override values in the final payload (e.g. 'level' or 'fingerprint') @@ -425,7 +432,7 @@ def report_message(message, level='error', request=None, extra_data=None, payloa def send_payload(payload, access_token): """ - Sends a payload object, (the result of calling _build_payload()). + Sends a payload object, (the result of calling _build_payload() + _serialize_payload()). Uses the configured handler from SETTINGS['handler'] Available handlers: @@ -433,30 +440,39 @@ def send_payload(payload, access_token): - 'thread': starts a single-use thread that will call _send_payload(). returns immediately. - 'agent': writes to a log file to be processed by rollbar-agent - 'tornado': calls _send_payload_tornado() (which makes an async HTTP request using tornado's AsyncHTTPClient) + - 'gae': calls _send_payload_appengine() (which makes a blocking call to Google App Engine) + - 'twisted': calls _send_payload_twisted() (which makes an async HTTP reqeust using Twisted and Treq) """ + payload = events.on_payload(payload) + if payload is False: + return + + payload_str = _serialize_payload(payload) + handler = SETTINGS.get('handler') if handler == 'blocking': - _send_payload(payload, access_token) + _send_payload(payload_str, access_token) elif handler == 'agent': - agent_log.error(payload, access_token) + agent_log.error(payload_str) elif handler == 'tornado': if TornadoAsyncHTTPClient is None: log.error('Unable to find tornado') return - _send_payload_tornado(payload, access_token) + _send_payload_tornado(payload_str, access_token) elif handler == 'gae': if AppEngineFetch is None: log.error('Unable to find AppEngine URLFetch module') return - _send_payload_appengine(payload, access_token) + _send_payload_appengine(payload_str, access_token) elif handler == 'twisted': - if TwistedHTTPClient is None: - log.error('Unable to find twisted') + if treq is None: + log.error('Unable to find Treq') return - _send_payload_twisted(payload, access_token) + _send_payload_twisted(payload_str, access_token) else: # default to 'thread' - thread = threading.Thread(target=_send_payload, args=(payload, access_token)) + thread = threading.Thread(target=_send_payload, args=(payload_str, access_token)) + _threads.put(thread) thread.start() @@ -487,6 +503,12 @@ def search_items(title, return_fields=None, access_token=None, endpoint=None, ** **search_fields) +def wait(f=None): + _threads.join() + if f is not None: + return f() + + class ApiException(Exception): """ This exception will be raised if there was a problem decoding the @@ -567,6 +589,7 @@ def _resolve_exception_class(idx, filter): cls = None return cls, level + def _filtered_level(exception): for i, filter in enumerate(SETTINGS['exception_level_filters']): cls, level = _resolve_exception_class(i, filter) @@ -587,7 +610,7 @@ def _create_agent_log(): log_file = SETTINGS['agent.log_file'] if not log_file.endswith('.rollbar'): log.error("Provided agent log file does not end with .rollbar, which it must. " - "Using default instead.") + "Using default instead.") log_file = DEFAULTS['agent.log_file'] retval = logging.getLogger('rollbar_agent') @@ -603,49 +626,59 @@ def _report_exc_info(exc_info, request, extra_data, payload_data, level=None): """ Called by report_exc_info() wrapper """ - # check if exception is marked ignored - cls, exc, trace = exc_info - if getattr(exc, '_rollbar_ignore', False) or _is_ignored(exc): - return if not _check_config(): return - data = _build_base_data(request) + filtered_level = _filtered_level(exc_info[1]) + if level is None: + level = filtered_level - filtered_level = _filtered_level(exc) - if filtered_level: - data['level'] = filtered_level + filtered_exc_info = events.on_exception_info(exc_info, + request=request, + extra_data=extra_data, + payload_data=payload_data, + level=level) - # explicitly override the level with provided level - if level: + if filtered_exc_info is False: + return + + cls, exc, trace = filtered_exc_info + + data = _build_base_data(request) + if level is not None: data['level'] = level - # exception info - # most recent call last - raw_frames = traceback.extract_tb(trace) - frames = [{'filename': f[0], 'lineno': f[1], 'method': f[2], 'code': f[3]} for f in raw_frames] + # walk the trace chain to collect cause and context exceptions + trace_chain = _walk_trace_chain(cls, exc, trace) - data['body'] = { - 'trace': { - 'frames': frames, - 'exception': { - 'class': cls.__name__, - 'message': text(exc), - } + extra_trace_data = None + if len(trace_chain) > 1: + data['body'] = { + 'trace_chain': trace_chain + } + if payload_data and ('body' in payload_data) and ('trace' in payload_data['body']): + extra_trace_data = payload_data['body']['trace'] + del payload_data['body']['trace'] + else: + data['body'] = { + 'trace': trace_chain[0] } - } if extra_data: extra_data = extra_data - if isinstance(extra_data, dict): - data['custom'] = extra_data - else: - data['custom'] = {'value': extra_data} - - _add_locals_data(data, exc_info) + if not isinstance(extra_data, dict): + extra_data = {'value': extra_data} + if extra_trace_data: + extra_data = dict_merge(extra_data, extra_trace_data) + data['custom'] = extra_data + if extra_trace_data and not extra_data: + data['custom'] = extra_trace_data + + request = _get_actual_request(request) _add_request_data(data, request) _add_person_data(data, request) + _add_lambda_context_data(data) data['server'] = _build_server_data() if payload_data: @@ -657,6 +690,37 @@ def _report_exc_info(exc_info, request, extra_data, payload_data, level=None): return data['uuid'] +def _walk_trace_chain(cls, exc, trace): + trace_chain = [_trace_data(cls, exc, trace)] + + while True: + exc = getattr(exc, '__cause__', None) or getattr(exc, '__context__', None) + if not exc: + break + trace_chain.append(_trace_data(type(exc), exc, getattr(exc, '__traceback__', None))) + + return trace_chain + + +def _trace_data(cls, exc, trace): + # exception info + # most recent call last + raw_frames = traceback.extract_tb(trace) + frames = [{'filename': f[0], 'lineno': f[1], 'method': f[2], 'code': f[3]} for f in raw_frames] + + trace_data = { + 'frames': frames, + 'exception': { + 'class': getattr(cls, '__name__', cls.__class__.__name__), + 'message': text(exc), + } + } + + _add_locals_data(trace_data, (cls, exc, trace)) + + return trace_data + + def _report_message(message, level, request, extra_data, payload_data): """ Called by report_message() wrapper @@ -664,12 +728,21 @@ def _report_message(message, level, request, extra_data, payload_data): if not _check_config(): return + filtered_message = events.on_message(message, + request=request, + extra_data=extra_data, + payload_data=payload_data, + level=level) + + if filtered_message is False: + return + data = _build_base_data(request, level=level) # message data['body'] = { 'message': { - 'body': message + 'body': filtered_message } } @@ -677,8 +750,10 @@ def _report_message(message, level, request, extra_data, payload_data): extra_data = extra_data data['body']['message'].update(extra_data) + request = _get_actual_request(request) _add_request_data(data, request) _add_person_data(data, request) + _add_lambda_context_data(data) data['server'] = _build_server_data() if payload_data: @@ -695,6 +770,10 @@ def _check_config(): log.info("pyrollbar: Not reporting because rollbar is disabled.") return False + # skip access token check for the agent handler + if SETTINGS.get('handler') == 'agent': + return True + # make sure we have an access_token if not SETTINGS.get('access_token'): log.warning("pyrollbar: No access_token provided. Please configure by calling rollbar.init() with your access token.") @@ -726,9 +805,13 @@ def _add_person_data(data, request): try: person_data = _build_person_data(request) except Exception as e: - log.exception("Exception while building person data for Rollbar paylooad: %r", e) + log.exception("Exception while building person data for Rollbar payload: %r", e) else: if person_data: + if not SETTINGS['capture_username'] and 'username' in person_data: + person_data['username'] = None + if not SETTINGS['capture_email'] and 'email' in person_data: + person_data['email'] = None data['person'] = person_data @@ -770,9 +853,11 @@ def _build_person_data(request): # id is required, so only include username/email if we have an id if retval.get('id'): + username = getattr(user, 'username', None) + email = getattr(user, 'email', None) retval.update({ - 'username': getattr(user, 'username', None), - 'email': getattr(user, 'email', None) + 'username': username, + 'email': email }) return retval @@ -810,11 +895,11 @@ def _flatten_nested_lists(l): return ret -def _add_locals_data(data, exc_info): +def _add_locals_data(trace_data, exc_info): if not SETTINGS['locals']['enabled']: return - frames = data['body']['trace']['frames'] + frames = trace_data['frames'] cur_tb = exc_info[2] frame_num = 0 @@ -832,84 +917,96 @@ def _add_locals_data(data, exc_info): frame_num += 1 continue - # Create placeholders for args/kwargs/locals - args = [] - kw = {} + # Create placeholders for argspec/varargspec/keywordspec/locals + argspec = None + varargspec = None + keywordspec = None _locals = {} try: arginfo = inspect.getargvalues(tb_frame) - local_vars = arginfo.locals - argspec = None - - func = _get_func_from_frame(tb_frame) - if func: - if inspect.isfunction(func) or inspect.ismethod(func): - argspec = inspect.getargspec(func) - elif inspect.isclass(func): - init_func = getattr(func, '__init__', None) - if init_func: - argspec = inspect.getargspec(init_func) - - # Get all of the named args - # - # args can be a nested list of args in the case where there - # are anonymous tuple args provided. - # e.g. in Python 2 you can: - # def func((x, (a, b), z)): - # return x + a + b + z - # - # func((1, (1, 2), 3)) - named_args = _flatten_nested_lists(arginfo.args) - - # Fill in all of the named args - for named_arg in named_args: - if named_arg in local_vars: - args.append(local_vars[named_arg]) - - # Add any varargs - if arginfo.varargs is not None: - args.extend(local_vars[arginfo.varargs]) - - # Fill in all of the kwargs - if arginfo.keywords is not None: - kw.update(local_vars[arginfo.keywords]) - - if argspec and argspec.defaults: - # Put any of the args that have defaults into kwargs - num_defaults = len(argspec.defaults) - if num_defaults: - # The last len(argspec.defaults) args in arginfo.args should be added - # to kwargs and removed from args - kw.update(dict(zip(arginfo.args[-num_defaults:], args[-num_defaults:]))) - args = args[:-num_defaults] # Optionally fill in locals for this frame - if local_vars and _check_add_locals(cur_frame, frame_num, num_frames): - _locals.update(local_vars.items()) - - args = args - kw = kw - _locals = _locals - - except Exception as e: + if arginfo.locals and _check_add_locals(cur_frame, frame_num, num_frames): + # Get all of the named args + # + # args can be a nested list of args in the case where there + # are anonymous tuple args provided. + # e.g. in Python 2 you can: + # def func((x, (a, b), z)): + # return x + a + b + z + # + # func((1, (1, 2), 3)) + argspec = _flatten_nested_lists(arginfo.args) + + if arginfo.varargs is not None: + varargspec = arginfo.varargs + if SETTINGS['locals']['scrub_varargs']: + temp_varargs = list(arginfo.locals[varargspec]) + for i, arg in enumerate(temp_varargs): + temp_varargs[i] = REDACT_REF + + arginfo.locals[varargspec] = tuple(temp_varargs) + + if arginfo.keywords is not None: + keywordspec = arginfo.keywords + + _locals.update(arginfo.locals.items()) + + except Exception: log.exception('Error while extracting arguments from frame. Ignoring.') # Finally, serialize each arg/kwarg/local separately so that we only report # CircularReferences for each variable, instead of for the entire payload # as would be the case if we serialized that payload in one-shot. - if args: - cur_frame['args'] = map(_serialize_frame_data, args) - if kw: - cur_frame['kwargs'] = dict((k, _serialize_frame_data(v)) for k, v in iteritems(kw)) + if argspec: + cur_frame['argspec'] = argspec + if varargspec: + cur_frame['varargspec'] = varargspec + if keywordspec: + cur_frame['keywordspec'] = keywordspec if _locals: - cur_frame['locals'] = dict((k, _serialize_frame_data(v)) for k, v in iteritems(_locals)) + try: + cur_frame['locals'] = dict((k, _serialize_frame_data(v)) for k, v in iteritems(_locals)) + except Exception: + log.exception('Error while serializing frame data.') frame_num += 1 def _serialize_frame_data(data): - return transforms.transform(data, (_serialize_transform,)) + for transform in (ScrubRedactTransform(), _serialize_transform): + data = transforms.transform(data, transform) + + return data + + +def _add_lambda_context_data(data): + """ + Attempts to add information from the lambda context if it exists + """ + global _CURRENT_LAMBDA_CONTEXT + context = _CURRENT_LAMBDA_CONTEXT + if context is None: + return + try: + lambda_data = { + 'lambda': { + 'remaining_time_in_millis': context.get_remaining_time_in_millis(), + 'function_name': context.function_name, + 'function_version': context.function_version, + 'arn': context.invoked_function_arn, + 'request_id': context.aws_request_id, + } + } + if 'custom' in data: + data['custom'] = dict_merge(data['custom'], lambda_data) + else: + data['custom'] = lambda_data + except Exception as e: + log.exception("Exception while adding lambda context data: %r", e) + finally: + _CURRENT_LAMBDA_CONTEXT = None def _add_request_data(data, request): @@ -922,6 +1019,7 @@ def _add_request_data(data, request): log.exception("Exception while building request_data for Rollbar payload: %r", e) else: if request_data: + _filter_ip(request_data, SETTINGS['capture_ip']) data['request'] = request_data @@ -935,6 +1033,16 @@ def _check_add_locals(frame, frame_num, total_frames): ('root' in SETTINGS and (frame.get('filename') or '').lower().startswith((SETTINGS['root'] or '').lower())))) +def _get_actual_request(request): + if WerkzeugLocalProxy and isinstance(request, WerkzeugLocalProxy): + try: + actual_request = request._get_current_object() + except RuntimeError: + return None + return actual_request + return request + + def _build_request_data(request): """ Returns a dictionary containing data from the request. @@ -957,13 +1065,6 @@ def _build_request_data(request): if WerkzeugRequest and isinstance(request, WerkzeugRequest): return _build_werkzeug_request_data(request) - if WerkzeugLocalProxy and isinstance(request, WerkzeugLocalProxy): - try: - actual_request = request._get_current_object() - except RuntimeError: - return None - return _build_werkzeug_request_data(actual_request) - # tornado if TornadoRequest and isinstance(request, TornadoRequest): return _build_tornado_request_data(request) @@ -972,6 +1073,14 @@ def _build_request_data(request): if BottleRequest and isinstance(request, BottleRequest): return _build_bottle_request_data(request) + # Sanic + if SanicRequest and isinstance(request, SanicRequest): + return _build_sanic_request_data(request) + + # falcon + if FalconRequest and isinstance(request, FalconRequest): + return _build_falcon_request_data(request) + # Plain wsgi (should be last) if isinstance(request, dict) and 'wsgi.version' in request: return _build_wsgi_request_data(request) @@ -985,6 +1094,7 @@ def _build_webob_request_data(request): 'GET': dict(request.GET), 'user_ip': _extract_user_ip(request), 'headers': dict(request.headers), + 'method': request.method, } try: @@ -1018,19 +1128,14 @@ def _extract_wsgi_headers(items): def _build_django_request_data(request): request_data = { - 'url': request.build_absolute_uri(), + 'url': request.get_raw_uri(), 'method': request.method, 'GET': dict(request.GET), 'POST': dict(request.POST), - 'user_ip': _wsgi_extract_user_ip(request.environ), + 'user_ip': _wsgi_extract_user_ip(request.META), } - try: - request_data['body'] = request.body - except: - pass - - request_data['headers'] = _extract_wsgi_headers(request.environ.items()) + request_data['headers'] = _extract_wsgi_headers(request.META.items()) return request_data @@ -1043,7 +1148,7 @@ def _build_werkzeug_request_data(request): 'user_ip': _extract_user_ip(request), 'headers': dict(request.headers), 'method': request.method, - 'files_keys': request.files.keys(), + 'files_keys': list(request.files.keys()), } try: @@ -1089,6 +1194,39 @@ def _build_bottle_request_data(request): return request_data +def _build_sanic_request_data(request): + request_data = { + 'url': request.url, + 'user_ip': request.remote_addr, + 'headers': request.headers, + 'method': request.method, + 'GET': dict(request.args) + } + + if request.json: + try: + request_data['body'] = request.json + except: + pass + else: + request_data['POST'] = request.form + + return request_data + + +def _build_falcon_request_data(request): + request_data = { + 'url': request.url, + 'user_ip': _wsgi_extract_user_ip(request.env), + 'headers': dict(request.headers), + 'method': request.method, + 'GET': dict(request.params), + 'context': dict(request.context), + } + + return request_data + + def _build_wsgi_request_data(request): request_data = { 'url': wsgiref.util.request_uri(request), @@ -1116,6 +1254,34 @@ def _build_wsgi_request_data(request): return request_data +def _filter_ip(request_data, capture_ip): + if 'user_ip' not in request_data or capture_ip == True: + return + + current_ip = request_data['user_ip'] + if not current_ip: + return + + new_ip = current_ip + if not capture_ip: + new_ip = None + elif capture_ip == ANONYMIZE: + try: + if '.' in current_ip: + new_ip = '.'.join(current_ip.split('.')[0:3]) + '.0' + elif ':' in current_ip: + parts = current_ip.split(':') + if len(parts) > 2: + terminal = '0000:0000:0000:0000:0000' + new_ip = ':'.join(parts[0:3] + [terminal]) + else: + new_ip = None + except: + new_ip = None + + request_data['user_ip'] = new_ip + + def _build_server_data(): """ Returns a dictionary containing information about the server environment. @@ -1123,10 +1289,14 @@ def _build_server_data(): # server environment server_data = { 'host': socket.gethostname(), - 'argv': sys.argv, 'pid': os.getpid() } + # argv does not always exist in embedded python environments + argv = getattr(sys, 'argv', None) + if argv: + server_data['argv'] = argv + for key in ['branch', 'root']: if SETTINGS.get(key): server_data[key] = SETTINGS[key] @@ -1135,7 +1305,10 @@ def _build_server_data(): def _transform(obj, key=None): - return transforms.transform(obj, _transforms, key=key) + for transform in _transforms: + obj = transforms.transform(obj, transform, key=key) + + return obj def _build_payload(data): @@ -1151,24 +1324,33 @@ def _build_payload(data): 'data': data } + return payload + + +def _serialize_payload(payload): return json.dumps(payload) -def _send_payload(payload, access_token): +def _send_payload(payload_str, access_token): try: - _post_api('item/', payload, access_token=access_token) + _post_api('item/', payload_str, access_token=access_token) except Exception as e: log.exception('Exception while posting item %r', e) + try: + _threads.get_nowait() + _threads.task_done() + except queue.Empty: + pass -def _send_payload_appengine(payload, access_token): +def _send_payload_appengine(payload_str, access_token): try: - _post_api_appengine('item/', payload, access_token=access_token) + _post_api_appengine('item/', payload_str, access_token=access_token) except Exception as e: log.exception('Exception while posting item %r', e) -def _post_api_appengine(path, payload, access_token=None): +def _post_api_appengine(path, payload_str, access_token=None): headers = {'Content-Type': 'application/json'} if access_token is not None: @@ -1177,121 +1359,181 @@ def _post_api_appengine(path, payload, access_token=None): url = urljoin(SETTINGS['endpoint'], path) resp = AppEngineFetch(url, method="POST", - payload=payload, + payload=payload_str, headers=headers, allow_truncated=False, deadline=SETTINGS.get('timeout', DEFAULT_TIMEOUT), validate_certificate=SETTINGS.get('verify_https', True)) - return _parse_response(path, SETTINGS['access_token'], payload, resp) + return _parse_response(path, SETTINGS['access_token'], payload_str, resp) -def _post_api(path, payload, access_token=None): +def _post_api(path, payload_str, access_token=None): headers = {'Content-Type': 'application/json'} if access_token is not None: headers['X-Rollbar-Access-Token'] = access_token url = urljoin(SETTINGS['endpoint'], path) - resp = requests.post(url, - data=payload, - headers=headers, - timeout=SETTINGS.get('timeout', DEFAULT_TIMEOUT), - verify=SETTINGS.get('verify_https', True)) + resp = transport.post(url, + data=payload_str, + headers=headers, + timeout=SETTINGS.get('timeout', DEFAULT_TIMEOUT), + verify=SETTINGS.get('verify_https', True), + proxy=SETTINGS.get('http_proxy'), + proxy_user=SETTINGS.get('http_proxy_user'), + proxy_password=SETTINGS.get('http_proxy_password')) - return _parse_response(path, SETTINGS['access_token'], payload, resp) + return _parse_response(path, SETTINGS['access_token'], payload_str, resp) def _get_api(path, access_token=None, endpoint=None, **params): access_token = access_token or SETTINGS['access_token'] url = urljoin(endpoint or SETTINGS['endpoint'], path) params['access_token'] = access_token - resp = requests.get(url, params=params, verify=SETTINGS.get('verify_https', True)) + resp = transport.get(url, + params=params, + verify=SETTINGS.get('verify_https', True), + proxy=SETTINGS.get('http_proxy'), + proxy_user=SETTINGS.get('http_proxy_user'), + proxy_password=SETTINGS.get('http_proxy_password')) return _parse_response(path, access_token, params, resp, endpoint=endpoint) -def _send_payload_tornado(payload, access_token): +def _send_payload_tornado(payload_str, access_token): try: - _post_api_tornado('item/', payload, access_token=access_token) + _post_api_tornado('item/', payload_str, access_token=access_token) except Exception as e: log.exception('Exception while posting item %r', e) -@tornado_coroutine -def _post_api_tornado(path, payload, access_token=None): +def _post_api_tornado(path, payload_str, access_token=None): headers = {'Content-Type': 'application/json'} if access_token is not None: headers['X-Rollbar-Access-Token'] = access_token + else: + access_token = SETTINGS['access_token'] url = urljoin(SETTINGS['endpoint'], path) - resp = yield TornadoAsyncHTTPClient().fetch( - url, body=payload, method='POST', connect_timeout=SETTINGS.get('timeout', DEFAULT_TIMEOUT), - request_timeout=SETTINGS.get('timeout', DEFAULT_TIMEOUT) - ) - - r = requests.Response() - r._content = resp.body - r.status_code = resp.code - r.headers.update(resp.headers) + def post_tornado_cb(resp): + r = requests.Response() + r._content = resp.body + r.status_code = resp.code + r.headers.update(resp.headers) + try: + _parse_response(path, access_token, payload_str, r) + except Exception as e: + log.exception('Exception while posting item %r', e) - _parse_response(path, SETTINGS['access_token'], payload, r) + TornadoAsyncHTTPClient().fetch(url, + callback=post_tornado_cb, + raise_error=False, + body=payload_str, + method='POST', + connect_timeout=SETTINGS.get('timeout', DEFAULT_TIMEOUT), + request_timeout=SETTINGS.get('timeout', DEFAULT_TIMEOUT)) -def _send_payload_twisted(payload, access_token): +def _send_payload_twisted(payload_str, access_token): try: - _post_api_twisted('item/', payload, access_token=access_token) + _post_api_twisted('item/', payload_str, access_token=access_token) except Exception as e: log.exception('Exception while posting item %r', e) -@inlineCallbacks -def _post_api_twisted(path, payload, access_token=None): - headers = {'Content-Type': ['application/json']} +def _post_api_twisted(path, payload_str, access_token=None): + def post_data_cb(data, resp): + resp._content = data + _parse_response(path, SETTINGS['access_token'], payload_str, resp) + + def post_cb(resp): + r = requests.Response() + r.status_code = resp.code + r.headers.update(resp.headers.getAllRawHeaders()) + return treq.content(resp).addCallback(post_data_cb, r) + headers = {'Content-Type': ['application/json']} if access_token is not None: headers['X-Rollbar-Access-Token'] = [access_token] url = urljoin(SETTINGS['endpoint'], path) + d = treq.post(url, payload_str, headers=headers, + timeout=SETTINGS.get('timeout', DEFAULT_TIMEOUT)) + d.addCallback(post_cb) + + +def _send_failsafe(message, uuid, host): + body_message = ('Failsafe from pyrollbar: {0}. Original payload may be found ' + 'in your server logs by searching for the UUID.').format(message) + + data = { + 'level': 'error', + 'environment': SETTINGS['environment'], + 'body': { + 'message': { + 'body': body_message + } + }, + 'notifier': SETTINGS['notifier'], + 'custom': { + 'orig_uuid': uuid, + 'orig_host': host + }, + 'failsafe': True, + 'internal': True, + } - agent = TwistedHTTPClient(reactor, connectTimeout=SETTINGS.get('timeout', DEFAULT_TIMEOUT)) - resp = yield agent.request( - 'POST', - url, - TwistedHeaders(headers), - StringProducer(payload)) + payload = _build_payload(data) - r = requests.Response() - r.status_code = resp.code - r.headers.update(resp.headers.getAllRawHeaders()) - bodyDeferred = Deferred() - resp.deliverBody(ResponseAccumulator(resp.length, bodyDeferred)) - body = yield bodyDeferred - r._content = body - _parse_response(path, SETTINGS['access_token'], payload, r) - yield returnValue(None) + try: + send_payload(payload, SETTINGS['access_token']) + except Exception: + log.exception('Rollbar: Error sending failsafe.') def _parse_response(path, access_token, params, resp, endpoint=None): if isinstance(resp, requests.Response): try: data = resp.text - except Exception as e: + except Exception: data = resp.content log.error('resp.text is undefined, resp.content is %r', resp.content) else: data = resp.content + global _LAST_RESPONSE_STATUS + last_response_was_429 = _LAST_RESPONSE_STATUS == 429 + _LAST_RESPONSE_STATUS = resp.status_code + if resp.status_code == 429: - log.warning("Rollbar: over rate limit, data was dropped. Payload was: %r", params) + if SETTINGS['log_all_rate_limited_items'] or not last_response_was_429: + log.warning("Rollbar: over rate limit, data was dropped. Payload was: %r", params) return - elif resp.status_code == 413: - log.warning("Rollbar: request entity too large. Payload was: %r", params) + elif resp.status_code == 502: + log.exception('Rollbar api returned a 502') return + elif resp.status_code == 413: + uuid = None + host = None + + try: + payload = json.loads(params) + uuid = payload['data']['uuid'] + host = payload['data']['server']['host'] + log.error("Rollbar: request entity too large for UUID %r\n. Payload:\n%r", uuid, payload) + except (TypeError, ValueError): + log.exception('Unable to decode JSON for failsafe.') + except KeyError: + log.exception('Unable to find payload parameters for failsafe.') + + _send_failsafe('payload too large', uuid, host) + # TODO: Should we return here? elif resp.status_code != 200: log.warning("Got unexpected status code from Rollbar api: %s\nResponse:\n%s", - resp.status_code, data) + resp.status_code, data) + # TODO: Should we also return here? try: json_data = json.loads(data) diff --git a/rollbar/cli.py b/rollbar/cli.py index 755f0d0c..44e8bda6 100644 --- a/rollbar/cli.py +++ b/rollbar/cli.py @@ -33,7 +33,8 @@ def main(): metavar='ACCESS_TOKEN') parser.add_option('-e', '--environment', dest='environment', - help="The environment to report errors and messages to.", + help="The environment to report errors and messages to. \ + Can be any string; suggestions: 'production', 'development', 'staging'", metavar='ENVIRONMENT') parser.add_option('-u', '--url', dest='endpoint_url', diff --git a/rollbar/contrib/django/__init__.py b/rollbar/contrib/django/__init__.py index f4c481dc..e69de29b 100644 --- a/rollbar/contrib/django/__init__.py +++ b/rollbar/contrib/django/__init__.py @@ -1,5 +0,0 @@ -from django.db.models import query -from rollbar import blacklisted_local_types - -# QuerySet objects will potentially execute SQL if you call repr() on them -blacklisted_local_types.extend([query.QuerySet]) diff --git a/rollbar/contrib/django/middleware.py b/rollbar/contrib/django/middleware.py index 3e4892cf..13bb3d23 100644 --- a/rollbar/contrib/django/middleware.py +++ b/rollbar/contrib/django/middleware.py @@ -1,9 +1,76 @@ """ django-rollbar middleware -To install, add the following in your settings.py: -1. add 'rollbar.contrib.django.middleware.RollbarNotifierMiddleware' to MIDDLEWARE_CLASSES -2. add a section like this: +There are two options for installing the Rollbar middleware. Both options +require modifying your settings.py file. + +The first option is to use +'rollbar.contrib.django.middleware.RollbarNotifierMiddleware' which will +report all exceptions to Rollbar including 404s. This middlware should be +placed as the last item in your middleware list which is: + * MIDDLEWARE_CLASSES in Django 1.9 and earlier + * MIDDLEWARE in Django 1.10 and up + +The other option is two use the two separate middlewares: + * 'rollbar.contrib.django.middleware.RollbarNotifierMiddlewareExcluding404' + * 'rollbar.contrib.django.middleware.RollbarNotifierMiddlewareOnly404' +The Excluding404 middleware should be placed as the last item in your middleware +list, and the Only404 middleware should be placed as the first item in your +middleware list. This allows 404s to be processed by your other middlewares +before sendind an item to Rollbar. Therefore if you handle the 404 differently +in a way that returns a response early you won't end up with a Rollbar item. + +Regardless of which method you use, you also should add a section to settings.py +like this: + +ROLLBAR = { + 'access_token': 'tokengoeshere', +} + +This can be used for passing configuration options to Rollbar. Additionally, +you can use the key 'ignorable_404_urls' to set an iterable of regular expression +patterns to use to determine whether a 404 exception should be ignored based +on the full url path for the request. For example, + +import re +ROLLBAR = { + 'access_token': 'YOUR_TOKEN', + 'ignorable_404_urls': ( + re.compile('/index\.php'), + re.compile('/foobar'), + ), +} + +To get more control of middleware and enrich it with custom data +you can subclass any of the middleware classes described above +and optionally override the methods: + def get_extra_data(self, request, exc): + ''' May be defined. Must return a dict or None. Use it to put some custom extra data on rollbar event. ''' + return + + def get_payload_data(self, request, exc): + ''' May be defined. Must return a dict or None. Use it to put some custom payload data on rollbar event. ''' + return +You would then insert your custom subclass into your middleware +configuration in the same place as the base class as described above. +For example: + +1. create a 'middleware.py' file on your project (name is up to you) +2. import the rollbar default middleware: 'from rollbar.contrib.django.middleware import RollbarNotifierMiddleware' +3. create your own middleware like this: +class CustomRollbarNotifierMiddleware(RollbarNotifierMiddleware): + def get_extra_data(self, request, exc): + ''' May be defined. Must return a dict or None. Use it to put some custom extra data on rollbar event. ''' + return + + def get_payload_data(self, request, exc): + ''' May be defined. Must return a dict or None. Use it to put some custom payload data on rollbar event. ''' + return + +4. add 'path.to.your.CustomRollbarNotifierMiddleware' in your settings.py to + a. MIDDLEWARE_CLASSES in Django 1.9 and earlier + b. MIDDLEWARE in Django 1.10 and up +5. add a section like this in your settings.py: ROLLBAR = { 'access_token': 'tokengoeshere', } @@ -17,9 +84,19 @@ import rollbar from django.core.exceptions import MiddlewareNotUsed -from django.core.urlresolvers import resolve from django.conf import settings from django.http import Http404 +from six import reraise + +try: + from django.urls import resolve +except ImportError: + from django.core.urlresolvers import resolve + +try: + from django.utils.deprecation import MiddlewareMixin +except ImportError: + from rollbar.contrib.django.utils import MiddlewareMixin log = logging.getLogger(__name__) @@ -39,10 +116,10 @@ def _patch_debugview(rollbar_web_base): from django.views import debug except ImportError: return - + if rollbar_web_base.endswith('/'): rollbar_web_base = rollbar_web_base[:-1] - + # modify the TECHNICAL_500_TEMPLATE new_data = """ {% if view_in_rollbar_url %} @@ -50,13 +127,34 @@ def _patch_debugview(rollbar_web_base): {% endif %} """ - if new_data in debug.TECHNICAL_500_TEMPLATE: - return - insert_before = '' replacement = new_data + insert_before - debug.TECHNICAL_500_TEMPLATE = debug.TECHNICAL_500_TEMPLATE.replace(insert_before, - replacement, 1) + + if hasattr(debug, 'TECHNICAL_500_TEMPLATE'): + if new_data in debug.TECHNICAL_500_TEMPLATE: + return + debug.TECHNICAL_500_TEMPLATE = debug.TECHNICAL_500_TEMPLATE.replace(insert_before, replacement, 1) + else: + # patch ExceptionReporter.get_traceback_html if this version of Django is using + # the file system templates rather than the ones in code + # This code comes from: + # https://github.com/django/django/blob/d79cf1e9e2887aa12567c8f27e384195253cb847/django/views/debug.py#L329,L334 + # There are theoretical issues with the code below, for example t.render could throw because + # t might be None, but this is the code from Django + from pathlib import Path + from django.template import Context + def new_get_traceback_html(exception_reporter): + """Return HTML version of debug 500 HTTP error page.""" + with Path(debug.CURRENT_DIR, 'templates', 'technical_500.html').open() as fh: + template_string = fh.read() + template_string = template_string.replace(insert_before, replacement, 1) + t = debug.DEBUG_ENGINE.from_string(template_string) + c = Context(exception_reporter.get_traceback_data(), use_l10n=False) + return t.render(c) + debug.ExceptionReporter.get_traceback_html = new_get_traceback_html + + if hasattr(debug.ExceptionReporter, '__rollbar__patched'): + return # patch ExceptionReporter.get_traceback_data old_get_traceback_data = debug.ExceptionReporter.get_traceback_data @@ -71,26 +169,38 @@ def new_get_traceback_data(exception_reporter): log.exception("Exception while adding view-in-rollbar link to technical_500_template.") return data debug.ExceptionReporter.get_traceback_data = new_get_traceback_data + debug.ExceptionReporter.__rollbar__patched = True + -class RollbarNotifierMiddleware(object): - def __init__(self): +def _should_ignore_404(url): + url_patterns = getattr(settings, 'ROLLBAR', {}).get('ignorable_404_urls', ()) + return any(p.search(url) for p in url_patterns) + + +class RollbarNotifierMiddleware(MiddlewareMixin): + def __init__(self, get_response=None): + super(RollbarNotifierMiddleware, self).__init__(get_response) + self.settings = getattr(settings, 'ROLLBAR', {}) if not self.settings.get('access_token'): raise MiddlewareNotUsed if not self._get_setting('enabled'): raise MiddlewareNotUsed - + self._ensure_log_handler() - + kw = self.settings.copy() access_token = kw.pop('access_token') environment = kw.pop('environment', 'development' if settings.DEBUG else 'production') kw.setdefault('exception_level_filters', DEFAULTS['exception_level_filters']) - + + # ignorable_404_urls is only relevant for this middleware not as an argument to init + kw.pop('ignorable_404_urls', None) + rollbar.init(access_token, environment, **kw) - + def hook(request, data): try: # try django 1.5 method for getting url_name @@ -106,20 +216,22 @@ def hook(request, data): data['context'] = url_name data['framework'] = 'django' - - if request: + + if request and hasattr(request, 'META'): request.META['rollbar.uuid'] = data['uuid'] - + rollbar.BASE_DATA_HOOK = hook - + # monkeypatch debug module if self._get_setting('patch_debugview'): try: _patch_debugview(self._get_setting('web_base')) except Exception as e: - log.error("Rollbar - unable to monkeypatch debugview to add 'View in Rollbar' link." + log.error( + "Rollbar - unable to monkeypatch debugview to add 'View in Rollbar' link." " To disable, set `ROLLBAR['patch_debugview'] = False` in settings.py." - " Exception was: %r", e) + " Exception was: %r", e + ) def _ensure_log_handler(self): """ @@ -132,7 +244,7 @@ def _ensure_log_handler(self): '%(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s') handler.setFormatter(formatter) log.addHandler(handler) - + def _get_setting(self, name, default=None): try: return self.settings[name] @@ -144,8 +256,59 @@ def _get_setting(self, name, default=None): return default_val return default + def get_extra_data(self, request, exc): + return + + def get_payload_data(self, request, exc): + return + def process_response(self, request, response): return response def process_exception(self, request, exc): - rollbar.report_exc_info(sys.exc_info(), request) + if isinstance(exc, Http404) and _should_ignore_404(request.get_full_path()): + return + rollbar.report_exc_info( + sys.exc_info(), + request, + extra_data=self.get_extra_data(request, exc), + payload_data=self.get_payload_data(request, exc), + ) + + +class RollbarNotifierMiddlewareOnly404(MiddlewareMixin): + def get_extra_data(self, request, exc): + return + + def get_payload_data(self, request, exc): + return + + def process_response(self, request, response): + if response.status_code != 404: + return response + + if _should_ignore_404(request.get_full_path()): + return response + + try: + if hasattr(request, '_rollbar_notifier_original_http404_exc_info'): + exc_type, exc_value, exc_traceback = request._rollbar_notifier_original_http404_exc_info + reraise(exc_type, exc_value, exc_traceback) + else: + raise Http404() + except Exception as exc: + rollbar.report_exc_info( + sys.exc_info(), + request, + extra_data=self.get_extra_data(request, exc), + payload_data=self.get_payload_data(request, exc), + ) + return response + + +class RollbarNotifierMiddlewareExcluding404(RollbarNotifierMiddleware): + def process_exception(self, request, exc): + if isinstance(exc, Http404): + request._rollbar_notifier_original_http404_exc_info = sys.exc_info() + else: + super(RollbarNotifierMiddlewareExcluding404, self).process_exception(request, exc) diff --git a/rollbar/contrib/django/utils.py b/rollbar/contrib/django/utils.py new file mode 100644 index 00000000..6cb71ce8 --- /dev/null +++ b/rollbar/contrib/django/utils.py @@ -0,0 +1,3 @@ +class MiddlewareMixin(object): + def __init__(self, get_response=None): + super(MiddlewareMixin, self).__init__() diff --git a/rollbar/contrib/django_rest_framework/__init__.py b/rollbar/contrib/django_rest_framework/__init__.py new file mode 100755 index 00000000..46a6c9a9 --- /dev/null +++ b/rollbar/contrib/django_rest_framework/__init__.py @@ -0,0 +1,22 @@ +try: + from django.core.exceptions import ImproperlyConfigured +except ImportError: + RestFrameworkExceptionHandler = None +else: + try: + from rest_framework.views import exception_handler as RestFrameworkExceptionHandler + except (ImportError, ImproperlyConfigured): + RestFrameworkExceptionHandler = None + + del ImproperlyConfigured + + +def post_exception_handler(exc, context): + # This is to be used with the Django REST Framework (DRF) as its + # global exception handler. It replaces the POST data of the Django + # request with the parsed data from the DRF. This is necessary + # because we cannot read the request data/stream more than once. + # This will allow us to see the parsed POST params in the rollbar + # exception log. + context['request']._request.POST = context['request'].data + return RestFrameworkExceptionHandler(exc, context) diff --git a/rollbar/contrib/pyramid/__init__.py b/rollbar/contrib/pyramid/__init__.py index 4b27fb51..174f738f 100644 --- a/rollbar/contrib/pyramid/__init__.py +++ b/rollbar/contrib/pyramid/__init__.py @@ -13,11 +13,25 @@ import rollbar DEFAULT_WEB_BASE = 'https://rollbar.com' +BOOLEAN_SETTINGS = [ + 'rollbar.enabled', 'rollbar.allow_logging_basic_config', + 'rollbar.verify_https' +] log = logging.getLogger(__name__) -def handle_error(settings, request): - rollbar.report_exc_info(sys.exc_info(), request) + +EXCEPTION_BLACKLIST = (WSGIHTTPException,) +EXCEPTION_WHITELIST = tuple() + + +def handle_error(request, exception, exc_info): + if( + isinstance(exception, EXCEPTION_BLACKLIST) and + not isinstance(exception, EXCEPTION_WHITELIST) + ): + return + rollbar.report_exc_info(exc_info, request) def parse_settings(settings): @@ -25,38 +39,35 @@ def parse_settings(settings): out = {} for k, v in settings.items(): if k.startswith(prefix): + if k in BOOLEAN_SETTINGS: + v = asbool(v) out[k[len(prefix):]] = v + return out def rollbar_tween_factory(pyramid_handler, registry): settings = parse_settings(registry.settings) - whitelist = () - blacklist = (WSGIHTTPException,) - def rollbar_tween(request): # for testing out the integration try: if (settings.get('allow_test', 'true') == 'true' and - request.GET.get('pyramid_rollbar_test') == 'true'): + request.GET.get('pyramid_rollbar_test') == 'true'): try: raise Exception("pyramid_rollbar test exception") - except: - handle_error(settings, request) + except Exception as exc: + handle_error(request, exc, sys.exc_info()) except: log.exception("Error in pyramid_rollbar_test block") try: response = pyramid_handler(request) - except whitelist: - handle_error(settings, request) - raise - except blacklist: - raise - except: - handle_error(settings, request) + except Exception as exc: + handle_error(request, exc, sys.exc_info()) raise + if request.exception is not None: + handle_error(request, request.exception, request.exc_info) return response return rollbar_tween @@ -90,9 +101,11 @@ def insert_rollbar_console(request, html): # patch tbtools.Traceback.render_full old_render_full = tbtools.Traceback.render_full + def new_render_full(self, request, *args, **kw): html = old_render_full(self, request, *args, **kw) return insert_rollbar_console(request, html) + tbtools.Traceback.render_full = new_render_full @@ -102,7 +115,7 @@ def includeme(config): """ settings = config.registry.settings - config.add_tween('rollbar.contrib.pyramid.rollbar_tween_factory', under=EXCVIEW) + config.add_tween('rollbar.contrib.pyramid.rollbar_tween_factory', over=EXCVIEW) # run patch_debugtoolbar, unless they disabled it if asbool(settings.get('rollbar.patch_debugtoolbar', True)): @@ -163,7 +176,7 @@ def __init__(self, settings, app): def __call__(self, environ, start_resp): try: return self.app(environ, start_resp) - except Exception as e: + except Exception as exc: from pyramid.request import Request - handle_error(self.settings, Request(environ)) + handle_error(Request(environ), exc, sys.exc_info()) raise diff --git a/rollbar/examples/twisted/simpleserv.py b/rollbar/examples/twisted/simpleserv.py index 488a5f29..2ba1d283 100644 --- a/rollbar/examples/twisted/simpleserv.py +++ b/rollbar/examples/twisted/simpleserv.py @@ -24,7 +24,7 @@ def foo(): class Echo(protocol.Protocol): """This is just about the simplest possible protocol""" - + def dataReceived(self, data): "As soon as any data is received, write it back." @@ -39,7 +39,7 @@ def main(): """This runs the protocol on port 8000""" factory = protocol.ServerFactory() factory.protocol = Echo - reactor.listenTCP(8000,factory) + reactor.listenTCP(8000, factory) reactor.run() diff --git a/rollbar/lib/__init__.py b/rollbar/lib/__init__.py index a138c8af..9a3f6649 100644 --- a/rollbar/lib/__init__.py +++ b/rollbar/lib/__init__.py @@ -1,7 +1,9 @@ import base64 +import collections import copy import os import sys +from array import array import six from six.moves import urllib @@ -11,7 +13,9 @@ binary_type = six.binary_type integer_types = six.integer_types +number_types = integer_types + (float, ) string_types = six.string_types +sequence_types = (collections.Mapping, list, tuple, set, frozenset, array, collections.deque) urlparse = urllib.parse.urlparse urlsplit = urllib.parse.urlsplit @@ -44,12 +48,11 @@ def text(val): return repr(val) - _map = map + def map(*args): return _map(*args) - def force_lower(val): return str(val).lower() @@ -57,12 +60,11 @@ def force_lower(val): def text(val): return str(val) - _map = map + def map(*args): return list(_map(*args)) - def force_lower(val): try: return val.lower() @@ -171,7 +173,10 @@ def dict_merge(a, b): if k in result and isinstance(result[k], dict): result[k] = dict_merge(result[k], v) else: - result[k] = copy.deepcopy(v) + try: + result[k] = copy.deepcopy(v) + except: + result[k] = '' % (v,) return result @@ -181,6 +186,17 @@ def circular_reference_label(data, ref_key=None): return '' % (type(data).__name__, ref) +def float_nan_label(data): + return '' + + +def float_infinity_label(data): + if data > 1: + return '' + else: + return '' + + def unencodable_object_label(data): return '' % (type(data).__name__, base64.b64encode(data).decode('ascii')) @@ -189,4 +205,3 @@ def unencodable_object_label(data): def undecodable_object_label(data): return '' % (type(data).__name__, base64.b64encode(data).decode('ascii')) - diff --git a/rollbar/lib/events.py b/rollbar/lib/events.py new file mode 100644 index 00000000..fd568707 --- /dev/null +++ b/rollbar/lib/events.py @@ -0,0 +1,99 @@ +EXCEPTION_INFO = 'exception_info' +MESSAGE = 'message' +PAYLOAD = 'payload' + +_event_handlers = { + EXCEPTION_INFO: [], + MESSAGE: [], + PAYLOAD: [] +} + + +def _check_type(typ): + if typ not in _event_handlers: + raise ValueError('Unknown type: %s. Must be one of %s' % (typ, _event_handlers.keys())) + + +def _add_handler(typ, handler_fn, pos): + _check_type(typ) + + pos = pos if pos is not None else -1 + handlers = _event_handlers[typ] + + try: + handlers.index(handler_fn) + except ValueError: + handlers.insert(pos, handler_fn) + + +def _remove_handler(typ, handler_fn): + _check_type(typ) + + handlers = _event_handlers[typ] + + try: + index = handlers.index(handler_fn) + handlers.pop(index) + except ValueError: + pass + + +def _on_event(typ, target, **kw): + _check_type(typ) + + ref = target + for handler in _event_handlers[typ]: + result = handler(ref, **kw) + if result is False: + return False + + ref = result + + return ref + + +# Add/remove event handlers + +def add_exception_info_handler(handler_fn, pos=None): + _add_handler(EXCEPTION_INFO, handler_fn, pos) + + +def remove_exception_info_handler(handler_fn): + _remove_handler(EXCEPTION_INFO, handler_fn) + + +def add_message_handler(handler_fn, pos=None): + _add_handler(MESSAGE, handler_fn, pos) + + +def remove_message_handler(handler_fn): + _remove_handler(MESSAGE, handler_fn) + + +def add_payload_handler(handler_fn, pos=None): + _add_handler(PAYLOAD, handler_fn, pos) + + +def remove_payload_handler(handler_fn): + _remove_handler(PAYLOAD, handler_fn) + + +# Event handler processing + +def on_exception_info(exc_info, **kw): + return _on_event(EXCEPTION_INFO, exc_info, **kw) + + +def on_message(message, **kw): + return _on_event(MESSAGE, message, **kw) + + +def on_payload(payload, **kw): + return _on_event(PAYLOAD, payload, **kw) + + +# Misc + +def reset(): + for handlers in _event_handlers.values(): + del handlers[:] diff --git a/rollbar/lib/filters/__init__.py b/rollbar/lib/filters/__init__.py new file mode 100644 index 00000000..f054e70e --- /dev/null +++ b/rollbar/lib/filters/__init__.py @@ -0,0 +1,11 @@ +from rollbar.lib import events +from rollbar.lib.filters.basic import filter_rollbar_ignored_exceptions, filter_by_level + + +def add_builtin_filters(settings): + # exc_info filters + events.add_exception_info_handler(filter_rollbar_ignored_exceptions) + events.add_exception_info_handler(filter_by_level) + + # message filters + events.add_message_handler(filter_by_level) diff --git a/rollbar/lib/filters/basic.py b/rollbar/lib/filters/basic.py new file mode 100644 index 00000000..f9833cad --- /dev/null +++ b/rollbar/lib/filters/basic.py @@ -0,0 +1,13 @@ +def filter_rollbar_ignored_exceptions(exc_info, **kw): + _, exc, _ = exc_info + if getattr(exc, '_rollbar_ignore', False): + return False + + return exc_info + + +def filter_by_level(target, **kw): + if 'level' in kw and kw['level'] == 'ignored': + return False + + return target diff --git a/rollbar/lib/transforms/__init__.py b/rollbar/lib/transforms/__init__.py index facf73ba..3b995055 100644 --- a/rollbar/lib/transforms/__init__.py +++ b/rollbar/lib/transforms/__init__.py @@ -1,6 +1,6 @@ -import collections - -from rollbar.lib import python_major_version, binary_type, string_types, integer_types, traverse +from rollbar.lib import ( + python_major_version, binary_type, string_types, integer_types, + number_types, traverse) _ALLOWED_CIRCULAR_REFERENCE_TYPES = [binary_type, bool, type(None)] @@ -9,10 +9,10 @@ else: _ALLOWED_CIRCULAR_REFERENCE_TYPES.append(string_types) -if isinstance(integer_types, tuple): - _ALLOWED_CIRCULAR_REFERENCE_TYPES.extend(integer_types) +if isinstance(number_types, tuple): + _ALLOWED_CIRCULAR_REFERENCE_TYPES.extend(number_types) else: - _ALLOWED_CIRCULAR_REFERENCE_TYPES.append(integer_types) + _ALLOWED_CIRCULAR_REFERENCE_TYPES.append(number_types) _ALLOWED_CIRCULAR_REFERENCE_TYPES = tuple(_ALLOWED_CIRCULAR_REFERENCE_TYPES) @@ -58,48 +58,53 @@ def transform_custom(self, o, key=None): return self.default(o, key=key) - -def transform(obj, transforms, key=None): +def transform(obj, transform, key=None): key = key or () - def do_transforms(type_name, val, key=None, **kw): - for transform in transforms: - fn = getattr(transform, 'transform_%s' % type_name, transform.transform_custom) - val = fn(val, key=key, **kw) + def do_transform(type_name, val, key=None, **kw): + fn = getattr(transform, 'transform_%s' % type_name, transform.transform_custom) + val = fn(val, key=key, **kw) return val if python_major_version() < 3: def string_handler(s, key=None): if isinstance(s, str): - return do_transforms('py2_str', s, key=key) + return do_transform('py2_str', s, key=key) elif isinstance(s, unicode): - return do_transforms('unicode', s, key=key) + return do_transform('unicode', s, key=key) else: def string_handler(s, key=None): if isinstance(s, bytes): - return do_transforms('py3_bytes', s, key=key) + return do_transform('py3_bytes', s, key=key) elif isinstance(s, str): - return do_transforms('unicode', s, key=key) + return do_transform('unicode', s, key=key) def default_handler(o, key=None): - if isinstance(o, integer_types + (float,)): - return do_transforms('number', o, key=key) - if isinstance(o, bool): - return do_transforms('boolean', o, key=key) + return do_transform('boolean', o, key=key) + + # There is a quirk in the current version (1.1.6) of the enum + # backport enum34 which causes it to not have the same + # behavior as Python 3.4+. One way to identify IntEnums is that + # they are instances of numbers but not number types. + if isinstance(o, number_types): + if type(o) not in number_types: + return do_transform('custom', o, key=key) + else: + return do_transform('number', o, key=key) - return do_transforms('custom', o, key=key) + return do_transform('custom', o, key=key) handlers = { 'string_handler': string_handler, - 'tuple_handler': lambda o, key=None: do_transforms('tuple', o, key=key), - 'namedtuple_handler': lambda o, key=None: do_transforms('namedtuple', o, key=key), - 'list_handler': lambda o, key=None: do_transforms('list', o, key=key), - 'set_handler': lambda o, key=None: do_transforms('set', o, key=key), - 'mapping_handler': lambda o, key=None: do_transforms('dict', o, key=key), - 'circular_reference_handler': lambda o, key=None, ref_key=None: \ - do_transforms('circular_reference', o, key=key, ref_key=ref_key), + 'tuple_handler': lambda o, key=None: do_transform('tuple', o, key=key), + 'namedtuple_handler': lambda o, key=None: do_transform('namedtuple', o, key=key), + 'list_handler': lambda o, key=None: do_transform('list', o, key=key), + 'set_handler': lambda o, key=None: do_transform('set', o, key=key), + 'mapping_handler': lambda o, key=None: do_transform('dict', o, key=key), + 'circular_reference_handler': lambda o, key=None, ref_key=None: + do_transform('circular_reference', o, key=key, ref_key=ref_key), 'default_handler': default_handler, 'allowed_circular_reference_types': _ALLOWED_CIRCULAR_REFERENCE_TYPES } diff --git a/rollbar/lib/transforms/scrub.py b/rollbar/lib/transforms/scrub.py index 453d6a9c..8032f648 100644 --- a/rollbar/lib/transforms/scrub.py +++ b/rollbar/lib/transforms/scrub.py @@ -1,4 +1,3 @@ -import os import random from rollbar.lib import build_key_matcher, text diff --git a/rollbar/lib/transforms/scrub_redact.py b/rollbar/lib/transforms/scrub_redact.py new file mode 100644 index 00000000..a81234b9 --- /dev/null +++ b/rollbar/lib/transforms/scrub_redact.py @@ -0,0 +1,19 @@ +from rollbar.lib.transforms.scrub import ScrubTransform + + +class RedactRef(object): + pass + + +REDACT_REF = RedactRef() + + +class ScrubRedactTransform(ScrubTransform): + def default(self, o, key=None): + if o is REDACT_REF: + return self.redact(o) + + return super(ScrubRedactTransform, self).default(o, key=key) + + +__all__ = ['ScrubRedactTransform'] diff --git a/rollbar/lib/transforms/scruburl.py b/rollbar/lib/transforms/scruburl.py index d14a505a..7b570462 100644 --- a/rollbar/lib/transforms/scruburl.py +++ b/rollbar/lib/transforms/scruburl.py @@ -24,12 +24,9 @@ def __init__(self, self.params_to_scrub = set(map(lambda x: x.lower(), params_to_scrub)) def in_scrub_fields(self, key): - if not key: - # This can happen if the transform is applied to a non-object, - # like a string. - return True - - return super(ScrubUrlTransform, self).in_scrub_fields(key) + # Returning True here because we want to scrub URLs out of + # every string, not just ones that we know the key for. + return True def redact(self, url_string): _redact = super(ScrubUrlTransform, self).redact @@ -42,7 +39,7 @@ def redact(self, url_string): try: url_parts = urlsplit(url_string) - qs_params = parse_qs(url_parts.query) + qs_params = parse_qs(url_parts.query, keep_blank_values=True) except: # This isn't a URL, return url_string which is a no-op # for this transform @@ -91,5 +88,4 @@ def default(self, o, key=None): return o - __all__ = ['ScrubUrlTransform'] diff --git a/rollbar/lib/transforms/serializable.py b/rollbar/lib/transforms/serializable.py index 6c27f983..cdde1611 100644 --- a/rollbar/lib/transforms/serializable.py +++ b/rollbar/lib/transforms/serializable.py @@ -1,7 +1,9 @@ -import base64 +import math from rollbar.lib import binary_type, string_types -from rollbar.lib import circular_reference_label, undecodable_object_label, unencodable_object_label +from rollbar.lib import ( + circular_reference_label, float_infinity_label, float_nan_label, + undecodable_object_label, unencodable_object_label) from rollbar.lib import iteritems, python_major_version, text from rollbar.lib.transforms import Transform @@ -25,6 +27,14 @@ def transform_namedtuple(self, o, key=None): return '<%s>' % text(o._make(new_vals)) + def transform_number(self, o, key=None): + if math.isnan(o): + return float_nan_label(o) + elif math.isinf(o): + return float_infinity_label(o) + else: + return o + def transform_py2_str(self, o, key=None): try: o.decode('utf8') @@ -70,26 +80,38 @@ def transform_dict(self, o, key=None): return super(SerializableTransform, self).transform_dict(ret, key=key) - def transform_custom(self, o, key=None): if o is None: return None - if any(filter(lambda x: isinstance(o, x), self.whitelist)): - try: - return repr(o) - except TypeError: - pass - - # If self.safe_repr is False, use repr() to serialize the object - if not self.safe_repr: + # Best to be very careful when we call user code in the middle of + # preparing a stack trace. So we put a try/except around it all. + try: + if any(filter(lambda x: isinstance(o, x), self.whitelist)): + try: + return repr(o) + except TypeError: + pass + + # If self.safe_repr is False, use repr() to serialize the object + if not self.safe_repr: + try: + return repr(o) + except TypeError: + pass + + # Otherwise, just use the type name + return str(type(o)) + + except Exception as e: + exc_str = '' try: - return repr(o) - except TypeError: - pass - - # Otherwise, just use the type name - return str(type(o)) + exc_str = str(e) + except Exception as e2: + exc_str = '[%s while calling str(%s)]' % ( + e2.__class__.__name__, e.__class__.__name__) + return '<%s in %s.__repr__: %s>' % ( + e.__class__.__name__, o.__class__.__name__, exc_str) __all__ = ['SerializableTransform'] diff --git a/rollbar/lib/transforms/shortener.py b/rollbar/lib/transforms/shortener.py index 490eb0c5..2301eca0 100644 --- a/rollbar/lib/transforms/shortener.py +++ b/rollbar/lib/transforms/shortener.py @@ -1,8 +1,10 @@ from array import array import collections -import math +import itertools -from rollbar.lib import integer_types, iteritems, key_in, reprlib, string_types, text +from rollbar.lib import ( + integer_types, iteritems, key_in, number_types, reprlib, sequence_types, + string_types, text) from rollbar.lib.transforms import Transform @@ -49,24 +51,24 @@ def _shorten_sequence(self, obj, max_keys): return self._repr.repr(obj) + def _shorten_mapping(self, obj, max_keys): + _len = len(obj) + if _len <= max_keys: + return obj + + return {k: obj[k] for k in itertools.islice(obj.keys(), max_keys)} + def _shorten_basic(self, obj, max_len): val = text(obj) if len(val) <= max_len: return obj - return self._repr.repr(val) + return self._repr.repr(obj) def _shorten_other(self, obj): if obj is None: return None - if isinstance(obj, float): - if math.isinf(obj): - return 'Infinity' - - if math.isnan(obj): - return 'NaN' - if self.safe_repr: obj = text(obj) @@ -75,10 +77,12 @@ def _shorten_other(self, obj): def _shorten(self, val): max_size = self._get_max_size(val) - if isinstance(val, (string_types, collections.Mapping, list, tuple, set, collections.deque)): + if isinstance(val, dict): + return self._shorten_mapping(val, max_size) + if isinstance(val, (string_types, sequence_types)): return self._shorten_sequence(val, max_size) - if isinstance(val, integer_types): + if isinstance(val, number_types): return self._shorten_basic(val, self._repr.maxlong) return self._shorten_other(val) diff --git a/rollbar/lib/transport.py b/rollbar/lib/transport.py new file mode 100644 index 00000000..cf722a5a --- /dev/null +++ b/rollbar/lib/transport.py @@ -0,0 +1,41 @@ +import requests +import threading + + +_local = threading.local() + + +def _session(): + if hasattr(_local, 'session'): + return _local.session + _local.session = requests.Session() + return _local.session + + +def _get_proxy_cfg(kw): + proxy = kw.pop('proxy', None) + proxy_user = kw.pop('proxy_user', None) + proxy_password = kw.pop('proxy_password', None) + if proxy and proxy_user and proxy_password: + return { + 'http': 'http://{}:{}@{}'.format(proxy_user, proxy_password, proxy), + 'https': 'http://{}:{}@{}'.format(proxy_user, proxy_password, proxy), + } + elif proxy: + return { + 'http': 'http://{}'.format(proxy), + 'https': 'http://{}'.format(proxy), + } + + +def post(*args, **kw): + proxies = _get_proxy_cfg(kw) + return _session().post(*args, proxies=proxies, **kw) + + +def get(*args, **kw): + proxies = _get_proxy_cfg(kw) + return _session().get(*args, proxies=proxies, **kw) + + +__all__ = ['post', 'get'] diff --git a/rollbar/lib/traverse.py b/rollbar/lib/traverse.py index 9169933d..7be65283 100644 --- a/rollbar/lib/traverse.py +++ b/rollbar/lib/traverse.py @@ -1,8 +1,8 @@ import collections +import logging from rollbar.lib import binary_type, iteritems, string_types, circular_reference_label - CIRCULAR = -1 DEFAULT = 0 MAPPING = 1 @@ -12,6 +12,8 @@ SET = 5 STRING = 6 +log = logging.getLogger(__name__) + def _noop_circular(a, **kw): return circular_reference_label(a, ref_key=kw.get('ref_key')) @@ -114,24 +116,29 @@ def traverse(obj, } kw.update(custom_handlers) - if obj_type is STRING: - return string_handler(obj, key=key) - elif obj_type is TUPLE: - return tuple_handler(tuple(traverse(elem, key=key + (i,), **kw) for i, elem in enumerate(obj)), key=key) - elif obj_type is NAMEDTUPLE: - return namedtuple_handler(obj._make(traverse(v, key=key + (k,), **kw) for k, v in iteritems(obj._asdict())), key=key) - elif obj_type is LIST: - return list_handler(list(traverse(elem, key=key + (i,), **kw) for i, elem in enumerate(obj)), key=key) - elif obj_type is SET: - return set_handler(set(traverse(elem, key=key + (i,), **kw) for i, elem in enumerate(obj)), key=key) - elif obj_type is MAPPING: - return mapping_handler(dict((k, traverse(v, key=key + (k,), **kw)) for k, v in iteritems(obj)), key=key) - elif obj_type is DEFAULT: - for handler_type, handler in iteritems(custom_handlers): - if isinstance(obj, handler_type): - return handler(obj, key=key) + try: + if obj_type is STRING: + return string_handler(obj, key=key) + elif obj_type is TUPLE: + return tuple_handler(tuple(traverse(elem, key=key + (i,), **kw) for i, elem in enumerate(obj)), key=key) + elif obj_type is NAMEDTUPLE: + return namedtuple_handler(obj._make(traverse(v, key=key + (k,), **kw) for k, v in iteritems(obj._asdict())), key=key) + elif obj_type is LIST: + return list_handler(list(traverse(elem, key=key + (i,), **kw) for i, elem in enumerate(obj)), key=key) + elif obj_type is SET: + return set_handler(set(traverse(elem, key=key + (i,), **kw) for i, elem in enumerate(obj)), key=key) + elif obj_type is MAPPING: + return mapping_handler(dict((k, traverse(v, key=key + (k,), **kw)) for k, v in iteritems(obj)), key=key) + elif obj_type is DEFAULT: + for handler_type, handler in iteritems(custom_handlers): + if isinstance(obj, handler_type): + return handler(obj, key=key) + except: + # use the default handler for unknown object types + log.debug("Exception while traversing object using type-specific " + "handler. Switching to default handler.", exc_info=True) return default_handler(obj, key=key) -__all__ = ['traverse'] \ No newline at end of file +__all__ = ['traverse'] diff --git a/rollbar/logger.py b/rollbar/logger.py index 0c17dd23..304dd2a4 100644 --- a/rollbar/logger.py +++ b/rollbar/logger.py @@ -18,13 +18,17 @@ logger.addHandler(rollbar_handler) """ -import copy import logging import threading -import time import rollbar +# hack to fix backward compatibility in Python3 +try: + from logging import _checkLevel +except ImportError: + _checkLevel = lambda lvl: lvl + class RollbarHandler(logging.Handler): SUPPORTED_LEVELS = set(('debug', 'info', 'warning', 'error', 'critical')) @@ -36,14 +40,15 @@ def __init__(self, environment=None, level=logging.INFO, history_size=10, - history_level=logging.DEBUG): + history_level=logging.DEBUG, + **kw): logging.Handler.__init__(self) if access_token is not None: - rollbar.init(access_token, environment) + rollbar.init(access_token, environment, **kw) - self.notify_level = level + self.notify_level = _checkLevel(level) self.history_size = history_size if history_size > 0: @@ -57,7 +62,7 @@ def setLevel(self, level): log records we notify Rollbar about instead of which records we save to the history. """ - self.notify_level = level + self.notify_level = _checkLevel(level) def setHistoryLevel(self, level): """ @@ -68,6 +73,11 @@ def setHistoryLevel(self, level): logging.Handler.setLevel(self, level) def emit(self, record): + # If the record came from Rollbar's own logger don't report it + # to Rollbar + if record.name == rollbar.__log_name__: + return + level = record.levelname.lower() if level not in self.SUPPORTED_LEVELS: @@ -75,11 +85,7 @@ def emit(self, record): exc_info = record.exc_info - # use the formatted message, not the message template - if not hasattr(record, 'message'): - return - - message = record.message + message = record.getMessage() extra_data = { 'args': record.args, 'record': { @@ -161,7 +167,8 @@ def _add_history(self, record, payload_data): def _build_history_data(self, record): data = {'timestamp': record.created, - 'message': record.getMessage()} + 'format': record.msg, + 'args': record.args} if hasattr(record, 'rollbar_uuid'): data['uuid'] = record.rollbar_uuid diff --git a/rollbar/test/contrib/__init__.py b/rollbar/test/flask_tests/__init__.py similarity index 100% rename from rollbar/test/contrib/__init__.py rename to rollbar/test/flask_tests/__init__.py diff --git a/rollbar/test/contrib/flask/test_flask.py b/rollbar/test/flask_tests/test_flask.py similarity index 67% rename from rollbar/test/contrib/flask/test_flask.py rename to rollbar/test/flask_tests/test_flask.py index c12b1330..9693c87c 100644 --- a/rollbar/test/contrib/flask/test_flask.py +++ b/rollbar/test/flask_tests/test_flask.py @@ -37,13 +37,6 @@ def index(): def cause_error(): raise Exception("Uh oh") - @app.before_first_request - def init_rollbar(): - rollbar.init(TOKEN, 'flasktest', - root=os.path.dirname(os.path.realpath(__file__)), - allow_logging_basic_config=True) - got_request_exception.connect(rollbar.contrib.flask.report_exception, app) - class CustomRequest(Request): @property def rollbar_person(self): @@ -53,11 +46,21 @@ def rollbar_person(self): return app +def init_rollbar(app): + from flask import got_request_exception + rollbar.init(TOKEN, 'flasktest', + root=os.path.dirname(os.path.realpath(__file__)), + allow_logging_basic_config=True, + capture_email=True, + capture_username=True) + got_request_exception.connect(rollbar.contrib.flask.report_exception, app) + if ALLOWED_PYTHON_VERSION and FLASK_INSTALLED: class FlaskTest(BaseTest): def setUp(self): super(FlaskTest, self).setUp() self.app = create_app() + init_rollbar(self.app) self.client = self.app.test_client() def test_index(self): @@ -82,7 +85,7 @@ def test_uncaught(self, send_payload): self.assertEqual(resp.status_code, 500) self.assertEqual(send_payload.called, True) - payload = json.loads(send_payload.call_args[0][0]) + payload = send_payload.call_args[0][0] data = payload['data'] self.assertIn('body', data) @@ -110,7 +113,7 @@ def test_uncaught_json_request(self, send_payload): self.assertEqual(resp.status_code, 500) self.assertEqual(send_payload.called, True) - payload = json.loads(send_payload.call_args[0][0]) + payload = send_payload.call_args[0][0] data = payload['data'] self.assertIn('body', data) @@ -126,3 +129,34 @@ def test_uncaught_json_request(self, send_payload): self.assertEqual(data['request']['body'], json_body) self.assertEqual(data['request']['user_ip'], '5.6.7.8') self.assertEqual(data['request']['method'], 'POST') + + @mock.patch('rollbar.send_payload') + def test_uncaught_no_username_no_email(self, send_payload): + rollbar.SETTINGS['capture_email'] = False + rollbar.SETTINGS['capture_username'] = False + + resp = self.client.get('/cause_error?foo=bar', + headers={'X-Real-Ip': '1.2.3.4', 'User-Agent': 'Flask Test'}) + self.assertEqual(resp.status_code, 500) + + self.assertEqual(send_payload.called, True) + payload = send_payload.call_args[0][0] + data = payload['data'] + + self.assertIn('body', data) + self.assertEqual(data['body']['trace']['exception']['class'], 'Exception') + self.assertStringEqual(data['body']['trace']['exception']['message'], 'Uh oh') + + self.assertIn('person', data) + self.assertDictEqual(data['person'], + {'id': '123', 'username': None, 'email': None}) + + self.assertIn('request', data) + self.assertEqual(data['request']['url'], 'http://localhost/cause_error?foo=bar') + self.assertDictEqual(data['request']['GET'], {'foo': ['bar']}) + self.assertEqual(data['request']['user_ip'], '1.2.3.4') + self.assertEqual(data['request']['method'], 'GET') + self.assertEqual(data['request']['headers']['User-Agent'], 'Flask Test') + + rollbar.SETTINGS['capture_email'] = True + rollbar.SETTINGS['capture_username'] = True diff --git a/rollbar/test/test_basic_filters.py b/rollbar/test/test_basic_filters.py new file mode 100644 index 00000000..81569ede --- /dev/null +++ b/rollbar/test/test_basic_filters.py @@ -0,0 +1,29 @@ +from rollbar.lib import events, filters + +from rollbar.test import BaseTest + + +class BasicFiltersTest(BaseTest): + def setUp(self): + events.reset() + filters.add_builtin_filters({}) + + def test_rollbar_ignored_exception(self): + class IgnoredException(Exception): + _rollbar_ignore = True + + class NotIgnoredException(Exception): + _rollbar_ignore = False + + self.assertFalse(events.on_exception_info((None, IgnoredException(), None))) + self.assertIsNot(events.on_exception_info((None, NotIgnoredException(), None)), False) + + def test_filter_by_level(self): + self.assertFalse(events.on_exception_info((None, 123, None), level='ignored')) + self.assertIsNot(events.on_exception_info((None, 123, None), level='error'), False) + + self.assertFalse(events.on_message('hello world', level='ignored')) + self.assertIsNot(events.on_message('hello world', level='error'), False) + + self.assertFalse(events.on_payload({}, level='ignored')) + self.assertIsNot(events.on_message({}, level='error'), False) diff --git a/rollbar/test/test_custom_filters.py b/rollbar/test/test_custom_filters.py new file mode 100644 index 00000000..518c9610 --- /dev/null +++ b/rollbar/test/test_custom_filters.py @@ -0,0 +1,50 @@ +import re + +from rollbar.lib import events, filters + +from rollbar.test import BaseTest + + +class CustomFiltersTest(BaseTest): + def setUp(self): + events.reset() + filters.add_builtin_filters({}) + + def test_ignore_by_setting_rollbar_ignore(self): + class NotIgnoredByDefault(Exception): + pass + + def _ignore_if_cruel_world_filter(exc_info, **kw): + cls, exc, trace = exc_info + if 'cruel world' in str(exc): + exc._rollbar_ignore = True + + return exc_info + + events.add_exception_info_handler(_ignore_if_cruel_world_filter, pos=0) + + self.assertIsNot(events.on_exception_info((None, NotIgnoredByDefault('hello world'), None)), False) + self.assertFalse(events.on_exception_info((None, NotIgnoredByDefault('hello cruel world'), None))) + + def test_ignore_messages_by_regex(self): + regex = re.compile(r'cruel') + + def _ignore_cruel_world_substring(message, **kw): + if regex.search(message): + return False + + return message + + events.add_message_handler(_ignore_cruel_world_substring) + + self.assertFalse(events.on_message('hello cruel world')) + self.assertIsNot(events.on_message('hello world'), False) + + def test_modify_payload(self): + def _add_test_key(payload, **kw): + payload['test'] = 333 + return payload + + events.add_payload_handler(_add_test_key) + + self.assertEqual(events.on_payload({'hello': 'world'}), {'hello': 'world', 'test': 333}) diff --git a/rollbar/test/test_lib.py b/rollbar/test/test_lib.py new file mode 100644 index 00000000..201a2ed1 --- /dev/null +++ b/rollbar/test/test_lib.py @@ -0,0 +1,59 @@ +from rollbar.lib import dict_merge + +from rollbar.test import BaseTest + +class RollbarLibTest(BaseTest): + def test_dict_merge_not_dict(self): + a = {'a': {'b': 42}} + b = 99 + result = dict_merge(a, b) + + self.assertEqual(99, result) + + def test_dict_merge_dicts_independent(self): + a = {'a': {'b': 42}} + b = {'x': {'y': 99}} + result = dict_merge(a, b) + + self.assertIn('a', result) + self.assertIn('b', result['a']) + self.assertEqual(42, result['a']['b']) + self.assertIn('x', result) + self.assertIn('y', result['x']) + self.assertEqual(99, result['x']['y']) + + def test_dict_merge_dicts(self): + a = {'a': {'b': 42}} + b = {'a': {'c': 99}} + result = dict_merge(a, b) + + self.assertIn('a', result) + self.assertIn('b', result['a']) + self.assertIn('c', result['a']) + self.assertEqual(42, result['a']['b']) + self.assertEqual(99, result['a']['c']) + + def test_dict_merge_dicts_second_wins(self): + a = {'a': {'b': 42}} + b = {'a': {'b': 99}} + result = dict_merge(a, b) + + self.assertIn('a', result) + self.assertIn('b', result['a']) + self.assertEqual(99, result['a']['b']) + + def test_dict_merge_dicts_select_poll(self): + import select + poll = getattr(select, 'poll', None) + if poll is None: + return + p = poll() + a = {'a': {'b': 42}} + b = {'a': {'y': p}} + result = dict_merge(a, b) + + self.assertIn('a', result) + self.assertIn('b', result['a']) + self.assertEqual(42, result['a']['b']) + self.assertIn('y', result['a']) + self.assertRegex(result['a']['y'], r'Uncopyable obj') diff --git a/rollbar/test/test_loghandler.py b/rollbar/test/test_loghandler.py index 2a80da25..8838b72b 100644 --- a/rollbar/test/test_loghandler.py +++ b/rollbar/test/test_loghandler.py @@ -17,6 +17,9 @@ _test_environment = 'test' _default_settings = copy.deepcopy(rollbar.SETTINGS) +class CauseException(Exception): + pass + class LogHandlerTest(BaseTest): def setUp(self): @@ -27,23 +30,39 @@ def _create_logger(self): logger = logging.getLogger(__name__) logger.setLevel(logging.DEBUG) - rollbar_handler = RollbarHandler(_test_access_token, _test_environment) - rollbar_handler.setLevel(logging.WARNING) + self.rollbar_handler = RollbarHandler(_test_access_token, _test_environment) + self.rollbar_handler.setLevel(logging.WARNING) - logger.addHandler(rollbar_handler) + logger.addHandler(self.rollbar_handler) return logger # @mock.patch('rollbar.send_payload') # def test_message_stays_unformatted(self, send_payload): # logger = self._create_logger() -# logger.warning("Hello %d %s", 1, 'world') +# logger.warning("Hello %d %s", 1, 'world') # -# payload = json.loads(send_payload.call_args[0][0]) +# payload = send_payload.call_args[0][0] # -# self.assertEqual(payload['data']['body']['message']['body'], "Hello %d %s") -# self.assertEqual(payload['data']['body']['message']['args'], [1, 'world']) -# self.assertEqual(payload['data']['body']['message']['record']['name'], __name__) +# self.assertEqual(payload['data']['body']['message']['body'], "Hello %d %s") +# self.assertEqual(payload['data']['body']['message']['args'], (1, 'world')) +# self.assertEqual(payload['data']['body']['message']['record']['name'], __name__) + + @mock.patch('rollbar.send_payload') + def test_string_or_int_level(self, send_payload): + logger = self._create_logger() + logger.setLevel(logging.ERROR) + self.rollbar_handler.setLevel('WARNING') + logger.error("I am an error") + + payload = send_payload.call_args[0][0] + + self.assertEqual(payload['data']['level'], 'error') + + self.rollbar_handler.setLevel(logging.WARNING) + logger.error("I am an error") + + self.assertEqual(payload['data']['level'], 'error') def test_request_is_get_from_log_record_if_present(self): logger = self._create_logger() @@ -77,3 +96,71 @@ def test_request_is_get_from_log_record_if_present(self): logger.exception("Exception message", extra={"request": request}) self.assertEqual(report_exc_info.call_args[1]["request"], request) report_message_mock.assert_not_called() + + @mock.patch('rollbar.send_payload') + def test_nested_exception_trace_chain(self, send_payload): + logger = self._create_logger() + + def _raise_context(): + bar_local = 'bar' + raise CauseException('bar') + + def _raise_ex(): + try: + _raise_context() + except CauseException as context: + # python2 won't automatically assign this traceback... + exc_info = sys.exc_info() + setattr(context, '__traceback__', exc_info[2]) + try: + foo_local = 'foo' + # in python3 __context__ is automatically set when an exception is raised in an except block + e = Exception('foo') + setattr(e, '__context__', context) # PEP-3134 + raise e + except: + logger.exception("Bad time") + + _raise_ex() + + self.assertEqual(send_payload.called, True) + payload = send_payload.call_args[0][0] + body = payload['data']['body'] + trace = body['trace'] if 'trace' in body else None + trace_chain = body['trace_chain'] if 'trace_chain' in body else None + has_only_trace_chain = trace is None and trace_chain is not None + has_only_trace = trace is not None and trace_chain is None + self.assertTrue(has_only_trace or has_only_trace_chain) + if trace_chain is not None: + self.assertEqual('Bad time', payload['data']['custom']['exception']['description']) + if trace is not None: + self.assertEqual('Bad time', trace['exception']['description']) + + @mock.patch('rollbar.send_payload') + def test_not_nested_exception_trace_chain(self, send_payload): + logger = self._create_logger() + + def _raise_context(): + bar_local = 'bar' + raise CauseException('bar') + + def _raise_ex(): + try: + _raise_context() + except: + logger.exception("Bad time") + + _raise_ex() + + self.assertEqual(send_payload.called, True) + payload = send_payload.call_args[0][0] + body = payload['data']['body'] + trace = body['trace'] if 'trace' in body else None + trace_chain = body['trace_chain'] if 'trace_chain' in body else None + has_only_trace_chain = trace is None and trace_chain is not None + has_only_trace = trace is not None and trace_chain is None + self.assertTrue(has_only_trace or has_only_trace_chain) + if trace_chain is not None: + self.assertEqual('Bad time', payload['data']['custom']['exception']['description']) + if trace is not None: + self.assertEqual('Bad time', trace['exception']['description']) diff --git a/rollbar/test/test_pyramid.py b/rollbar/test/test_pyramid.py new file mode 100644 index 00000000..0cb1404e --- /dev/null +++ b/rollbar/test/test_pyramid.py @@ -0,0 +1,39 @@ +import mock + +from rollbar.test import BaseTest + +try: + from pyramid.request import Request + + PYRAMID_INSTALLED = True +except ImportError: + PYRAMID_INSTALLED = False + + +if PYRAMID_INSTALLED: + + class PyramidMiddlewareTest(BaseTest): + def test_catch_exception_in_the_wsgi_app(self): + from rollbar.contrib.pyramid import RollbarMiddleware + + def wsgi_app(environ, start_resp): + raise RuntimeError("oops") + + middleware = RollbarMiddleware({}, wsgi_app) + + with mock.patch("rollbar.report_exc_info") as mock_report: + with self.assertRaises(RuntimeError): + middleware(environ={}, start_resp=lambda: None) + + self.assertEqual(mock_report.call_count, 1) + + args, kwargs = mock_report.call_args + self.assertEqual(kwargs, {}) + + exc_info, request = args + + exc_type, exc_value, exc_tb = exc_info + self.assertEqual(exc_type, RuntimeError) + self.assertIsInstance(exc_value, RuntimeError) + + self.assertIsInstance(request, Request) diff --git a/rollbar/test/test_rollbar.py b/rollbar/test/test_rollbar.py index 58019405..d095359d 100644 --- a/rollbar/test/test_rollbar.py +++ b/rollbar/test/test_rollbar.py @@ -2,6 +2,10 @@ import copy import json import mock +import socket +import uuid + +import sys try: from StringIO import StringIO @@ -36,7 +40,7 @@ def setUp(self): rollbar.init(_test_access_token, locals={'enabled': True}, dummy_key='asdf', handler='blocking', timeout=12345) def test_merged_settings(self): - expected = {'enabled': True, 'sizes': rollbar.DEFAULT_LOCALS_SIZES, 'safe_repr': True, 'whitelisted_types': []} + expected = {'enabled': True, 'sizes': rollbar.DEFAULT_LOCALS_SIZES, 'safe_repr': True, 'scrub_varargs': True, 'whitelisted_types': []} self.assertDictEqual(rollbar.SETTINGS['locals'], expected) self.assertEqual(rollbar.SETTINGS['timeout'], 12345) self.assertEqual(rollbar.SETTINGS['dummy_key'], 'asdf') @@ -121,17 +125,205 @@ def _raise(): self.assertEqual(send_payload.called, True) - payload = json.loads(send_payload.call_args[0][0]) + payload = send_payload.call_args[0][0] self.assertEqual(payload['access_token'], _test_access_token) self.assertIn('body', payload['data']) self.assertIn('trace', payload['data']['body']) + self.assertNotIn('trace_chain', payload['data']['body']) self.assertIn('exception', payload['data']['body']['trace']) self.assertEqual(payload['data']['body']['trace']['exception']['message'], 'foo') self.assertEqual(payload['data']['body']['trace']['exception']['class'], 'Exception') - self.assertNotIn('args', payload['data']['body']['trace']['frames'][-1]) - self.assertNotIn('kwargs', payload['data']['body']['trace']['frames'][-1]) + self.assertNotIn('argspec', payload['data']['body']['trace']['frames'][-1]) + self.assertNotIn('varargspec', payload['data']['body']['trace']['frames'][-1]) + self.assertNotIn('keywordspec', payload['data']['body']['trace']['frames'][-1]) + self.assertNotIn('locals', payload['data']['body']['trace']['frames'][-1]) + + @mock.patch('rollbar._post_api') + def test_lambda_function_good(self, _post_api): + rollbar.SETTINGS['handler'] = 'thread' + fake_event = {'a': 42} + fake_context = MockLambdaContext(99) + @rollbar.lambda_function + def my_lambda_func(event, context): + return [event['a'], context.x] + + result = my_lambda_func(fake_event, fake_context) + + self.assertEqual(len(result), 2) + self.assertEqual(result[0], 42) + self.assertEqual(result[1], 99) + self.assertEqual(_post_api.called, False) + + rollbar._CURRENT_LAMBDA_CONTEXT = None + rollbar.SETTINGS['handler'] = 'blocking' + + @mock.patch('rollbar._post_api') + def test_lambda_function_bad(self, _post_api): + rollbar.SETTINGS['handler'] = 'thread' + fake_event = {'a': 42} + fake_context = MockLambdaContext(99) + @rollbar.lambda_function + def my_lambda_func(event, context): + raise event['a'] + + result = None + try: + result = my_lambda_func(fake_event, fake_context) + except: + pass + + self.assertEqual(result, None) + self.assertEqual(_post_api.called, True) + + rollbar._CURRENT_LAMBDA_CONTEXT = None + rollbar.SETTINGS['handler'] = 'blocking' + + @mock.patch('rollbar._post_api') + def test_lambda_function_method_good(self, _post_api): + rollbar.SETTINGS['handler'] = 'thread' + fake_event = {'a': 42} + fake_context = MockLambdaContext(99) + + class LambdaClass(object): + def __init__(self): + self.a = 13 + + def my_lambda_func(self, event, context): + return [event['a'], context.x, self.a] + + app = LambdaClass() + app.my_lambda_func = rollbar.lambda_function(app.my_lambda_func) + result = app.my_lambda_func(fake_event, fake_context) + + self.assertEqual(len(result), 3) + self.assertEqual(result[0], 42) + self.assertEqual(result[1], 99) + self.assertEqual(result[2], 13) + self.assertEqual(_post_api.called, False) + + rollbar._CURRENT_LAMBDA_CONTEXT = None + rollbar.SETTINGS['handler'] = 'blocking' + + @mock.patch('rollbar._post_api') + def test_lambda_function_method_bad(self, _post_api): + rollbar.SETTINGS['handler'] = 'thread' + fake_event = {'a': 42} + fake_context = MockLambdaContext(99) + + class LambdaClass(object): + def __init__(self): + self.a = 13 + + def my_lambda_func(self, event, context): + raise self.a + + app = LambdaClass() + app.my_lambda_func = rollbar.lambda_function(app.my_lambda_func) + + result = None + try: + result = app.my_lambda_func(fake_event, fake_context) + except: + pass + + self.assertEqual(result, None) + self.assertEqual(_post_api.called, True) + + rollbar._CURRENT_LAMBDA_CONTEXT = None + rollbar.SETTINGS['handler'] = 'blocking' + + @mock.patch('rollbar.send_payload') + def test_report_exception_with_cause(self, send_payload): + def _raise_cause(): + bar_local = 'bar' + raise CauseException('bar') + + def _raise_ex(): + try: + _raise_cause() + except CauseException as cause: + # python2 won't automatically assign this traceback... + exc_info = sys.exc_info() + setattr(cause, '__traceback__', exc_info[2]) + + try: + foo_local = 'foo' + # in python3 this would normally be expressed as + # raise Exception('foo') from cause + e = Exception('foo') + setattr(e, '__cause__', cause) # PEP-3134 + raise e + except: + rollbar.report_exc_info() + + _raise_ex() + + self.assertEqual(send_payload.called, True) + + payload = send_payload.call_args[0][0] + + self.assertEqual(payload['access_token'], _test_access_token) + self.assertIn('body', payload['data']) + self.assertNotIn('trace', payload['data']['body']) + self.assertIn('trace_chain', payload['data']['body']) + self.assertEqual(2, len(payload['data']['body']['trace_chain'])) + + self.assertIn('exception', payload['data']['body']['trace_chain'][0]) + self.assertEqual(payload['data']['body']['trace_chain'][0]['exception']['message'], 'foo') + self.assertEqual(payload['data']['body']['trace_chain'][0]['exception']['class'], 'Exception') + self.assertEqual(payload['data']['body']['trace_chain'][0]['frames'][-1]['locals']['foo_local'], 'foo') + + self.assertIn('exception', payload['data']['body']['trace_chain'][1]) + self.assertEqual(payload['data']['body']['trace_chain'][1]['exception']['message'], 'bar') + self.assertEqual(payload['data']['body']['trace_chain'][1]['exception']['class'], 'CauseException') + self.assertEqual(payload['data']['body']['trace_chain'][1]['frames'][-1]['locals']['bar_local'], 'bar') + + @mock.patch('rollbar.send_payload') + def test_report_exception_with_context(self, send_payload): + def _raise_context(): + bar_local = 'bar' + raise CauseException('bar') + + def _raise_ex(): + try: + _raise_context() + except CauseException as context: + # python2 won't automatically assign this traceback... + exc_info = sys.exc_info() + setattr(context, '__traceback__', exc_info[2]) + + try: + foo_local = 'foo' + # in python3 __context__ is automatically set when an exception is raised in an except block + e = Exception('foo') + setattr(e, '__context__', context) # PEP-3134 + raise e + except: + rollbar.report_exc_info() + + _raise_ex() + + self.assertEqual(send_payload.called, True) + + payload = send_payload.call_args[0][0] + + self.assertEqual(payload['access_token'], _test_access_token) + self.assertIn('body', payload['data']) + self.assertNotIn('trace', payload['data']['body']) + self.assertIn('trace_chain', payload['data']['body']) + self.assertEqual(2, len(payload['data']['body']['trace_chain'])) + + self.assertIn('exception', payload['data']['body']['trace_chain'][0]) + self.assertEqual(payload['data']['body']['trace_chain'][0]['exception']['message'], 'foo') + self.assertEqual(payload['data']['body']['trace_chain'][0]['exception']['class'], 'Exception') + self.assertEqual(payload['data']['body']['trace_chain'][0]['frames'][-1]['locals']['foo_local'], 'foo') + + self.assertIn('exception', payload['data']['body']['trace_chain'][1]) + self.assertEqual(payload['data']['body']['trace_chain'][1]['exception']['message'], 'bar') + self.assertEqual(payload['data']['body']['trace_chain'][1]['exception']['class'], 'CauseException') + self.assertEqual(payload['data']['body']['trace_chain'][1]['frames'][-1]['locals']['bar_local'], 'bar') @mock.patch('rollbar.send_payload') def test_exception_filters(self, send_payload): @@ -169,14 +361,13 @@ def _raise_api_exception(): _raise_api_exception() self.assertEqual(1, send_payload.call_count) - @mock.patch('rollbar.send_payload') def test_report_messsage(self, send_payload): rollbar.report_message('foo') self.assertEqual(send_payload.called, True) - payload = json.loads(send_payload.call_args[0][0]) + payload = send_payload.call_args[0][0] self.assertEqual(payload['access_token'], _test_access_token) self.assertIn('body', payload['data']) @@ -188,7 +379,7 @@ def test_report_messsage(self, send_payload): def test_uuid(self, send_payload): uuid = rollbar.report_message('foo') - payload = json.loads(send_payload.call_args[0][0]) + payload = send_payload.call_args[0][0] self.assertEqual(payload['data']['uuid'], uuid) @@ -201,7 +392,7 @@ def test_report_exc_info_level(self, send_payload): rollbar.report_exc_info() self.assertEqual(send_payload.called, True) - payload = json.loads(send_payload.call_args[0][0]) + payload = send_payload.call_args[0][0] self.assertEqual(payload['data']['level'], 'error') try: @@ -210,7 +401,7 @@ def test_report_exc_info_level(self, send_payload): rollbar.report_exc_info(level='info') self.assertEqual(send_payload.called, True) - payload = json.loads(send_payload.call_args[0][0]) + payload = send_payload.call_args[0][0] self.assertEqual(payload['data']['level'], 'info') # payload takes precendence over 'level' @@ -220,9 +411,87 @@ def test_report_exc_info_level(self, send_payload): rollbar.report_exc_info(level='info', payload_data={'level': 'warn'}) self.assertEqual(send_payload.called, True) - payload = json.loads(send_payload.call_args[0][0]) + payload = send_payload.call_args[0][0] self.assertEqual(payload['data']['level'], 'warn') + @mock.patch('rollbar.send_payload') + def test_report_exc_info_nones(self, send_payload): + + rollbar.report_exc_info(exc_info=(None, None, None)) + + self.assertEqual(send_payload.called, True) + payload = send_payload.call_args[0][0] + self.assertEqual(payload['data']['level'], 'error') + + @mock.patch('rollbar._send_failsafe') + @mock.patch('rollbar.lib.transport.post', + side_effect=lambda *args, **kw: MockResponse({'status': 'Payload Too Large'}, 413)) + def test_trigger_failsafe(self, post, _send_failsafe): + rollbar.report_message('derp') + self.assertEqual(_send_failsafe.call_count, 1) + + try: + raise Exception('trigger_failsafe') + except: + rollbar.report_exc_info() + self.assertEqual(_send_failsafe.call_count, 2) + + @mock.patch('rollbar._send_failsafe') + @mock.patch('rollbar.lib.transport.post', + side_effect=lambda *args, **kw: MockRawResponse('\r\n' \ + '502 Bad Gateway\r\n' \ + '\r\n' \ + '

502 Bad Gateway

\r\n' \ + '
nginx
\r\n' \ + '\r\n' \ + '\r\n', 502)) + def test_502_failsafe(self, post, _send_failsafe): + rollbar.report_message('derp') + # self.assertEqual(_send_failsafe.call_count, 1) + + try: + raise Exception('trigger_failsafe') + except: + rollbar._post_api('/api/1/item', {'derp'}) + + @mock.patch('rollbar.send_payload') + def test_send_failsafe(self, send_payload): + test_uuid = str(uuid.uuid4()) + test_host = socket.gethostname() + test_data = { + 'access_token': _test_access_token, + 'data': { + 'body': { + 'message': { + 'body': 'Failsafe from pyrollbar: test message. ' + 'Original payload may be found in your server ' + 'logs by searching for the UUID.' + } + }, + 'failsafe': True, + 'level': 'error', + 'custom': { + 'orig_host': test_host, + 'orig_uuid': test_uuid + }, + 'environment': rollbar.SETTINGS['environment'], + 'internal': True, + 'notifier': rollbar.SETTINGS['notifier'] + } + } + + rollbar._send_failsafe('test message', test_uuid, test_host) + self.assertEqual(send_payload.call_count, 1) + self.assertEqual(send_payload.call_args[0][0], test_data) + + @mock.patch('rollbar.log.exception') + @mock.patch('rollbar.send_payload', side_effect=Exception('Monkey Business!')) + def test_fail_to_send_failsafe(self, send_payload, mock_log): + test_uuid = str(uuid.uuid4()) + test_host = socket.gethostname() + rollbar._send_failsafe('test message', test_uuid, test_host) + self.assertEqual(mock_log.call_count, 1) + @mock.patch('rollbar.send_payload') def test_args_constructor(self, send_payload): @@ -238,10 +507,30 @@ def __init__(self, arg1): self.assertEqual(send_payload.called, True) - payload = json.loads(send_payload.call_args[0][0]) + payload = send_payload.call_args[0][0] + + self.assertIn('argspec', payload['data']['body']['trace']['frames'][-1]) + self.assertNotIn('varargspec', payload['data']['body']['trace']['frames'][-1]) + self.assertNotIn('keywordspec', payload['data']['body']['trace']['frames'][-1]) - self.assertIn('args', payload['data']['body']['trace']['frames'][-1]) - self.assertEqual(33, payload['data']['body']['trace']['frames'][-1]['args'][1]) + self.assertEqual('arg1', payload['data']['body']['trace']['frames'][-1]['argspec'][1]) + self.assertEqual(33, payload['data']['body']['trace']['frames'][-1]['locals']['arg1']) + + @mock.patch('rollbar.send_payload') + def test_failed_locals_serialization(self, send_payload): + + class tmp(object): + @property + def __class__(self): + foo() + + try: + t = tmp() + raise Exception('trigger_serialize') + except: + rollbar.report_exc_info() + + self.assertEqual(send_payload.called, True) @mock.patch('rollbar.send_payload') def test_args_lambda_no_args(self, send_payload): @@ -255,10 +544,12 @@ def test_args_lambda_no_args(self, send_payload): self.assertEqual(send_payload.called, True) - payload = json.loads(send_payload.call_args[0][0]) + payload = send_payload.call_args[0][0] - self.assertNotIn('args', payload['data']['body']['trace']['frames'][-1]) - self.assertNotIn('kwargs', payload['data']['body']['trace']['frames'][-1]) + self.assertNotIn('argspec', payload['data']['body']['trace']['frames'][-1]) + self.assertNotIn('varargspec', payload['data']['body']['trace']['frames'][-1]) + self.assertNotIn('keywordspec', payload['data']['body']['trace']['frames'][-1]) + self.assertNotIn('locals', payload['data']['body']['trace']['frames'][-1]) @mock.patch('rollbar.send_payload') def test_args_lambda_with_args(self, send_payload): @@ -272,13 +563,17 @@ def test_args_lambda_with_args(self, send_payload): self.assertEqual(send_payload.called, True) - payload = json.loads(send_payload.call_args[0][0]) + payload = send_payload.call_args[0][0] - self.assertIn('args', payload['data']['body']['trace']['frames'][-1]) - self.assertNotIn('kwargs', payload['data']['body']['trace']['frames'][-1]) - self.assertEqual(2, len(payload['data']['body']['trace']['frames'][-1]['args'])) - self.assertEqual('arg1-value', payload['data']['body']['trace']['frames'][-1]['args'][0]) - self.assertEqual('arg2-value', payload['data']['body']['trace']['frames'][-1]['args'][1]) + self.assertIn('argspec', payload['data']['body']['trace']['frames'][-1]) + self.assertNotIn('varargspec', payload['data']['body']['trace']['frames'][-1]) + self.assertNotIn('keywordspec', payload['data']['body']['trace']['frames'][-1]) + + self.assertEqual(2, len(payload['data']['body']['trace']['frames'][-1]['argspec'])) + self.assertEqual('arg1', payload['data']['body']['trace']['frames'][-1]['argspec'][0]) + self.assertEqual('arg1-value', payload['data']['body']['trace']['frames'][-1]['locals']['arg1']) + self.assertEqual('arg2', payload['data']['body']['trace']['frames'][-1]['argspec'][1]) + self.assertEqual('arg2-value', payload['data']['body']['trace']['frames'][-1]['locals']['arg2']) @mock.patch('rollbar.send_payload') def test_args_lambda_with_defaults(self, send_payload): @@ -292,15 +587,17 @@ def test_args_lambda_with_defaults(self, send_payload): self.assertEqual(send_payload.called, True) - payload = json.loads(send_payload.call_args[0][0]) + payload = send_payload.call_args[0][0] - self.assertIn('args', payload['data']['body']['trace']['frames'][-1]) - self.assertNotIn('kwargs', payload['data']['body']['trace']['frames'][-1]) + self.assertIn('argspec', payload['data']['body']['trace']['frames'][-1]) + self.assertNotIn('varargspec', payload['data']['body']['trace']['frames'][-1]) + self.assertNotIn('keywordspec', payload['data']['body']['trace']['frames'][-1]) # NOTE(cory): Lambdas are a bit strange. We treat default values for lambda args # as positional. - self.assertEqual(1, len(payload['data']['body']['trace']['frames'][-1]['args'])) - self.assertEqual('arg1-value', payload['data']['body']['trace']['frames'][-1]['args'][0]) + self.assertEqual(1, len(payload['data']['body']['trace']['frames'][-1]['argspec'])) + self.assertEqual('arg1', payload['data']['body']['trace']['frames'][-1]['argspec'][0]) + self.assertEqual('arg1-value', payload['data']['body']['trace']['frames'][-1]['locals']['arg1']) @mock.patch('rollbar.send_payload') def test_args_lambda_with_star_args(self, send_payload): @@ -314,13 +611,16 @@ def test_args_lambda_with_star_args(self, send_payload): self.assertEqual(send_payload.called, True) - payload = json.loads(send_payload.call_args[0][0]) + payload = send_payload.call_args[0][0] + + self.assertNotIn('argspec', payload['data']['body']['trace']['frames'][-1]) + self.assertIn('varargspec', payload['data']['body']['trace']['frames'][-1]) + self.assertNotIn('keywordspec', payload['data']['body']['trace']['frames'][-1]) - self.assertIn('args', payload['data']['body']['trace']['frames'][-1]) - self.assertNotIn('kwargs', payload['data']['body']['trace']['frames'][-1]) + varargs = payload['data']['body']['trace']['frames'][-1]['varargspec'] - self.assertEqual(1, len(payload['data']['body']['trace']['frames'][-1]['args'])) - self.assertEqual('arg1-value', payload['data']['body']['trace']['frames'][-1]['args'][0]) + self.assertEqual(1, len(payload['data']['body']['trace']['frames'][-1]['locals'][varargs])) + self.assertRegex(payload['data']['body']['trace']['frames'][-1]['locals'][varargs][0], '\*+') @mock.patch('rollbar.send_payload') def test_args_lambda_with_star_args_and_args(self, send_payload): @@ -334,20 +634,26 @@ def test_args_lambda_with_star_args_and_args(self, send_payload): self.assertEqual(send_payload.called, True) - payload = json.loads(send_payload.call_args[0][0]) + payload = send_payload.call_args[0][0] - self.assertIn('args', payload['data']['body']['trace']['frames'][-1]) - self.assertNotIn('kwargs', payload['data']['body']['trace']['frames'][-1]) + self.assertIn('argspec', payload['data']['body']['trace']['frames'][-1]) + self.assertIn('varargspec', payload['data']['body']['trace']['frames'][-1]) + self.assertNotIn('keywordspec', payload['data']['body']['trace']['frames'][-1]) - self.assertEqual(3, len(payload['data']['body']['trace']['frames'][-1]['args'])) - self.assertEqual('arg1-value', payload['data']['body']['trace']['frames'][-1]['args'][0]) - self.assertEqual(1, payload['data']['body']['trace']['frames'][-1]['args'][1]) - self.assertEqual(2, payload['data']['body']['trace']['frames'][-1]['args'][2]) + varargs = payload['data']['body']['trace']['frames'][-1]['varargspec'] + + self.assertEqual(1, len(payload['data']['body']['trace']['frames'][-1]['argspec'])) + self.assertEqual('arg1', payload['data']['body']['trace']['frames'][-1]['argspec'][0]) + self.assertEqual('arg1-value', payload['data']['body']['trace']['frames'][-1]['locals']['arg1']) + + self.assertEqual(2, len(payload['data']['body']['trace']['frames'][-1]['locals'][varargs])) + self.assertRegex(payload['data']['body']['trace']['frames'][-1]['locals'][varargs][0], '\*+') + self.assertRegex(payload['data']['body']['trace']['frames'][-1]['locals'][varargs][1], '\*+') @mock.patch('rollbar.send_payload') def test_args_lambda_with_kwargs(self, send_payload): - _raise = lambda **args: foo(arg1) + _raise = lambda **kwargs: foo(arg1) try: _raise(arg1='arg1-value', arg2=2) @@ -356,19 +662,22 @@ def test_args_lambda_with_kwargs(self, send_payload): self.assertEqual(send_payload.called, True) - payload = json.loads(send_payload.call_args[0][0]) + payload = send_payload.call_args[0][0] + + self.assertNotIn('argspec', payload['data']['body']['trace']['frames'][-1]) + self.assertNotIn('varargspec', payload['data']['body']['trace']['frames'][-1]) + self.assertIn('keywordspec', payload['data']['body']['trace']['frames'][-1]) - self.assertNotIn('args', payload['data']['body']['trace']['frames'][-1]) - self.assertIn('kwargs', payload['data']['body']['trace']['frames'][-1]) + keywords = payload['data']['body']['trace']['frames'][-1]['keywordspec'] - self.assertEqual(2, len(payload['data']['body']['trace']['frames'][-1]['kwargs'])) - self.assertEqual('arg1-value', payload['data']['body']['trace']['frames'][-1]['kwargs']['arg1']) - self.assertEqual(2, payload['data']['body']['trace']['frames'][-1]['kwargs']['arg2']) + self.assertEqual(2, len(payload['data']['body']['trace']['frames'][-1]['locals'][keywords])) + self.assertEqual('arg1-value', payload['data']['body']['trace']['frames'][-1]['locals'][keywords]['arg1']) + self.assertEqual(2, payload['data']['body']['trace']['frames'][-1]['locals'][keywords]['arg2']) @mock.patch('rollbar.send_payload') def test_args_lambda_with_kwargs_and_args(self, send_payload): - _raise = lambda arg1, arg2, **args: foo(arg1) + _raise = lambda arg1, arg2, **kwargs: foo(arg1) try: _raise('a1', 'a2', arg3='arg3-value', arg4=2) @@ -377,22 +686,28 @@ def test_args_lambda_with_kwargs_and_args(self, send_payload): self.assertEqual(send_payload.called, True) - payload = json.loads(send_payload.call_args[0][0]) + payload = send_payload.call_args[0][0] + + self.assertIn('argspec', payload['data']['body']['trace']['frames'][-1]) + self.assertNotIn('varargspec', payload['data']['body']['trace']['frames'][-1]) + self.assertIn('keywordspec', payload['data']['body']['trace']['frames'][-1]) + + keywords = payload['data']['body']['trace']['frames'][-1]['keywordspec'] - self.assertIn('args', payload['data']['body']['trace']['frames'][-1]) - self.assertIn('kwargs', payload['data']['body']['trace']['frames'][-1]) + self.assertEqual(2, len(payload['data']['body']['trace']['frames'][-1]['argspec'])) + self.assertEqual('arg1', payload['data']['body']['trace']['frames'][-1]['argspec'][0]) + self.assertEqual('arg2', payload['data']['body']['trace']['frames'][-1]['argspec'][1]) + self.assertEqual('a1', payload['data']['body']['trace']['frames'][-1]['locals']['arg1']) + self.assertEqual('a2', payload['data']['body']['trace']['frames'][-1]['locals']['arg2']) - self.assertEqual(2, len(payload['data']['body']['trace']['frames'][-1]['args'])) - self.assertEqual(2, len(payload['data']['body']['trace']['frames'][-1]['kwargs'])) - self.assertEqual('a1', payload['data']['body']['trace']['frames'][-1]['args'][0]) - self.assertEqual('a2', payload['data']['body']['trace']['frames'][-1]['args'][1]) - self.assertEqual('arg3-value', payload['data']['body']['trace']['frames'][-1]['kwargs']['arg3']) - self.assertEqual(2, payload['data']['body']['trace']['frames'][-1]['kwargs']['arg4']) + self.assertEqual(2, len(payload['data']['body']['trace']['frames'][-1]['locals'][keywords])) + self.assertEqual('arg3-value', payload['data']['body']['trace']['frames'][-1]['locals'][keywords]['arg3']) + self.assertEqual(2, payload['data']['body']['trace']['frames'][-1]['locals'][keywords]['arg4']) @mock.patch('rollbar.send_payload') def test_args_lambda_with_kwargs_and_args_and_defaults(self, send_payload): - _raise = lambda arg1, arg2, arg3='default-value', **args: foo(arg1) + _raise = lambda arg1, arg2, arg3='default-value', **kwargs: foo(arg1) try: _raise('a1', 'a2', arg3='arg3-value', arg4=2) @@ -401,19 +716,26 @@ def test_args_lambda_with_kwargs_and_args_and_defaults(self, send_payload): self.assertEqual(send_payload.called, True) - payload = json.loads(send_payload.call_args[0][0]) + payload = send_payload.call_args[0][0] - self.assertIn('args', payload['data']['body']['trace']['frames'][-1]) - self.assertIn('kwargs', payload['data']['body']['trace']['frames'][-1]) + self.assertIn('argspec', payload['data']['body']['trace']['frames'][-1]) + self.assertNotIn('varargspec', payload['data']['body']['trace']['frames'][-1]) + self.assertIn('keywordspec', payload['data']['body']['trace']['frames'][-1]) + + keywords = payload['data']['body']['trace']['frames'][-1]['keywordspec'] # NOTE(cory): again, default values are strange for lambdas and we include them as # positional args. - self.assertEqual(3, len(payload['data']['body']['trace']['frames'][-1]['args'])) - self.assertEqual(1, len(payload['data']['body']['trace']['frames'][-1]['kwargs'])) - self.assertEqual('a1', payload['data']['body']['trace']['frames'][-1]['args'][0]) - self.assertEqual('a2', payload['data']['body']['trace']['frames'][-1]['args'][1]) - self.assertEqual('arg3-value', payload['data']['body']['trace']['frames'][-1]['args'][2]) - self.assertEqual(2, payload['data']['body']['trace']['frames'][-1]['kwargs']['arg4']) + self.assertEqual(3, len(payload['data']['body']['trace']['frames'][-1]['argspec'])) + self.assertEqual('arg1', payload['data']['body']['trace']['frames'][-1]['argspec'][0]) + self.assertEqual('arg2', payload['data']['body']['trace']['frames'][-1]['argspec'][1]) + self.assertEqual('arg3', payload['data']['body']['trace']['frames'][-1]['argspec'][2]) + self.assertEqual('a1', payload['data']['body']['trace']['frames'][-1]['locals']['arg1']) + self.assertEqual('a2', payload['data']['body']['trace']['frames'][-1]['locals']['arg2']) + self.assertEqual('arg3-value', payload['data']['body']['trace']['frames'][-1]['locals']['arg3']) + + self.assertEqual(1, len(payload['data']['body']['trace']['frames'][-1]['locals'][keywords])) + self.assertEqual(2, payload['data']['body']['trace']['frames'][-1]['locals'][keywords]['arg4']) @mock.patch('rollbar.send_payload') def test_args_generators(self, send_payload): @@ -432,13 +754,15 @@ def _raise(arg1): self.assertEqual(send_payload.called, True) - payload = json.loads(send_payload.call_args[0][0]) + payload = send_payload.call_args[0][0] - self.assertIn('args', payload['data']['body']['trace']['frames'][-1]) - self.assertNotIn('kwargs', payload['data']['body']['trace']['frames'][-1]) + self.assertIn('argspec', payload['data']['body']['trace']['frames'][-1]) + self.assertNotIn('varargspec', payload['data']['body']['trace']['frames'][-1]) + self.assertNotIn('keywordspec', payload['data']['body']['trace']['frames'][-1]) - self.assertEqual(1, len(payload['data']['body']['trace']['frames'][-1]['args'])) - self.assertEqual('hello world', payload['data']['body']['trace']['frames'][-1]['args'][0]) + self.assertEqual(1, len(payload['data']['body']['trace']['frames'][-1]['argspec'])) + self.assertEqual('arg1', payload['data']['body']['trace']['frames'][-1]['argspec'][0]) + self.assertEqual('hello world', payload['data']['body']['trace']['frames'][-1]['locals']['arg1']) @mock.patch('rollbar.send_payload') def test_anonymous_tuple_args(self, send_payload): @@ -454,20 +778,21 @@ def test_anonymous_tuple_args(self, send_payload): self.assertEqual(send_payload.called, True) - payload = json.loads(send_payload.call_args[0][0]) + payload = send_payload.call_args[0][0] - self.assertIn('args', payload['data']['body']['trace']['frames'][-1]) - self.assertNotIn('kwargs', payload['data']['body']['trace']['frames'][-1]) + self.assertIn('argspec', payload['data']['body']['trace']['frames'][-1]) + self.assertNotIn('varargspec', payload['data']['body']['trace']['frames'][-1]) + self.assertNotIn('keywordspec', payload['data']['body']['trace']['frames'][-1]) - self.assertEqual(4, len(payload['data']['body']['trace']['frames'][-1]['args'])) - self.assertEqual(1, payload['data']['body']['trace']['frames'][-1]['args'][0]) - self.assertEqual(2, payload['data']['body']['trace']['frames'][-1]['args'][1]) - self.assertEqual(3, payload['data']['body']['trace']['frames'][-1]['args'][2]) - self.assertEqual(4, payload['data']['body']['trace']['frames'][-1]['args'][3]) + self.assertEqual(4, len(payload['data']['body']['trace']['frames'][-1]['argspec'])) + self.assertEqual(1, payload['data']['body']['trace']['frames'][-1]['argspec'][0]) + self.assertEqual(2, payload['data']['body']['trace']['frames'][-1]['argspec'][1]) + self.assertEqual(3, payload['data']['body']['trace']['frames'][-1]['argspec'][2]) + self.assertEqual(4, payload['data']['body']['trace']['frames'][-1]['argspec'][3]) self.assertEqual(10, payload['data']['body']['trace']['frames'][-1]['locals']['ret']) @mock.patch('rollbar.send_payload') - def test_scrub_kwargs(self, send_payload): + def test_scrub_defaults(self, send_payload): def _raise(password='sensitive', clear='text'): raise Exception() @@ -479,14 +804,70 @@ def _raise(password='sensitive', clear='text'): self.assertEqual(send_payload.called, True) - payload = json.loads(send_payload.call_args[0][0]) + payload = send_payload.call_args[0][0] - self.assertNotIn('args', payload['data']['body']['trace']['frames'][-1]) - self.assertIn('kwargs', payload['data']['body']['trace']['frames'][-1]) + self.assertIn('argspec', payload['data']['body']['trace']['frames'][-1]) + self.assertNotIn('kwargs', payload['data']['body']['trace']['frames'][-1]['locals']) - self.assertEqual(2, len(payload['data']['body']['trace']['frames'][-1]['kwargs'])) - self.assertRegex(payload['data']['body']['trace']['frames'][-1]['kwargs']['password'], '\*+') - self.assertEqual('text', payload['data']['body']['trace']['frames'][-1]['kwargs']['clear']) + self.assertEqual(2, len(payload['data']['body']['trace']['frames'][-1]['argspec'])) + self.assertEqual('password', payload['data']['body']['trace']['frames'][-1]['argspec'][0]) + self.assertRegex(payload['data']['body']['trace']['frames'][-1]['locals']['password'], '\*+') + self.assertEqual('clear', payload['data']['body']['trace']['frames'][-1]['argspec'][1]) + self.assertEqual('text', payload['data']['body']['trace']['frames'][-1]['locals']['clear']) + + @mock.patch('rollbar.send_payload') + def test_dont_scrub_star_args(self, send_payload): + rollbar.SETTINGS['locals']['scrub_varargs'] = False + + def _raise(*args): + raise Exception() + + try: + _raise('sensitive', 'text') + except: + rollbar.report_exc_info() + + self.assertEqual(send_payload.called, True) + + payload = send_payload.call_args[0][0] + + self.assertNotIn('argspec', payload['data']['body']['trace']['frames'][-1]) + self.assertIn('varargspec', payload['data']['body']['trace']['frames'][-1]) + self.assertNotIn('keywordspec', payload['data']['body']['trace']['frames'][-1]) + self.assertIn('locals', payload['data']['body']['trace']['frames'][-1]) + + varargspec = payload['data']['body']['trace']['frames'][-1]['varargspec'] + + self.assertEqual(2, len(payload['data']['body']['trace']['frames'][-1]['locals'][varargspec])) + self.assertEqual(payload['data']['body']['trace']['frames'][-1]['locals'][varargspec][0], 'sensitive') + self.assertEqual(payload['data']['body']['trace']['frames'][-1]['locals'][varargspec][1], 'text') + + @mock.patch('rollbar.send_payload') + def test_scrub_kwargs(self, send_payload): + + def _raise(**kwargs): + raise Exception() + + try: + _raise(password='sensitive', clear='text') + except: + rollbar.report_exc_info() + + self.assertEqual(send_payload.called, True) + + payload = send_payload.call_args[0][0] + + self.assertNotIn('argspec', payload['data']['body']['trace']['frames'][-1]) + self.assertNotIn('varargspec', payload['data']['body']['trace']['frames'][-1]) + self.assertIn('keywordspec', payload['data']['body']['trace']['frames'][-1]) + + keywords = payload['data']['body']['trace']['frames'][-1]['keywordspec'] + + self.assertEqual(2, len(payload['data']['body']['trace']['frames'][-1]['locals'][keywords])) + self.assertIn('password', payload['data']['body']['trace']['frames'][-1]['locals'][keywords]) + self.assertRegex(payload['data']['body']['trace']['frames'][-1]['locals'][keywords]['password'], '\*+') + self.assertIn('clear', payload['data']['body']['trace']['frames'][-1]['locals'][keywords]) + self.assertEqual('text', payload['data']['body']['trace']['frames'][-1]['locals'][keywords]['clear']) @mock.patch('rollbar.send_payload') def test_scrub_locals(self, send_payload): @@ -513,7 +894,7 @@ def _raise(): self.assertEqual(send_payload.called, True) - payload = json.loads(send_payload.call_args[0][0]) + payload = send_payload.call_args[0][0] self.assertRegex(payload['data']['body']['trace']['frames'][-1]['locals']['password'], '\*+') self.assertRegex(payload['data']['body']['trace']['frames'][-1]['locals']['Password'], '\*+') @@ -527,6 +908,7 @@ def _raise(): def test_scrub_nans(self, send_payload): def _raise(): infinity = float('Inf') + negative_infinity = float('-Inf') not_a_number = float('NaN') raise Exception() @@ -537,10 +919,11 @@ def _raise(): self.assertEqual(send_payload.called, True) - payload = json.loads(send_payload.call_args[0][0]) + payload = send_payload.call_args[0][0] - self.assertEqual('Infinity', payload['data']['body']['trace']['frames'][-1]['locals']['infinity']) - self.assertEqual('NaN', payload['data']['body']['trace']['frames'][-1]['locals']['not_a_number']) + self.assertEqual('', payload['data']['body']['trace']['frames'][-1]['locals']['infinity']) + self.assertEqual('', payload['data']['body']['trace']['frames'][-1]['locals']['negative_infinity']) + self.assertEqual('', payload['data']['body']['trace']['frames'][-1]['locals']['not_a_number']) @mock.patch('rollbar.send_payload') def test_scrub_self_referencing(self, send_payload): @@ -548,7 +931,7 @@ def _raise(obj): raise Exception() try: - obj = {} + obj = {'x': 42.3} obj['child'] = { 'parent': obj } @@ -561,7 +944,7 @@ def _raise(obj): self.assertEqual(send_payload.called, True) - payload = json.loads(send_payload.call_args[0][0]) + payload = send_payload.call_args[0][0] self.assertTrue( (isinstance(payload['data']['body']['trace']['frames'][-1]['locals']['obj'], dict) and @@ -573,6 +956,17 @@ def _raise(obj): payload['data']['body']['trace']['frames'][-1]['locals']['obj'].startswith('") def test_encode_with_custom_repr_returns_unicode(self): @@ -218,3 +253,28 @@ def __repr__(self): expected['custom'] = SNOWMAN self._assertSerialized(start, expected, whitelist=[CustomRepr]) + def test_encode_with_bad_repr_doesnt_die(self): + class CustomRepr(object): + def __repr__(self): + assert False + + start = {'hello': 'world', 'custom': CustomRepr()} + serializable = SerializableTransform(whitelist_types=[CustomRepr]) + result = transforms.transform(start, serializable) + self.assertRegex(result['custom'], "") + + def test_encode_with_bad_str_doesnt_die(self): + + class UnStringableException(Exception): + def __str__(self): + raise Exception('asdf') + + class CustomRepr(object): + + def __repr__(self): + raise UnStringableException() + + start = {'hello': 'world', 'custom': CustomRepr()} + serializable = SerializableTransform(whitelist_types=[CustomRepr]) + result = transforms.transform(start, serializable) + self.assertRegex(result['custom'], "") diff --git a/rollbar/test/test_shortener_transform.py b/rollbar/test/test_shortener_transform.py new file mode 100644 index 00000000..c42b287f --- /dev/null +++ b/rollbar/test/test_shortener_transform.py @@ -0,0 +1,132 @@ +import sys +from array import array +from collections import deque + +import six +from rollbar import DEFAULT_LOCALS_SIZES +from rollbar.lib import transforms +from rollbar.lib.transforms.shortener import ShortenerTransform +from rollbar.test import BaseTest + + +class TestClassWithAVeryVeryVeryVeryVeryVeryVeryLongName: + pass + + +class ShortenerTransformTest(BaseTest): + def setUp(self): + self.data = { + 'string': 'x' * 120, + 'long': 17955682733916468498414734863645002504519623752387, + 'dict': { + 'one': 'one', + 'two': 'two', + 'three': 'three', + 'four': 'four', + 'five': 'five', + 'six': 'six', + 'seven': 'seven', + 'eight': 'eight', + 'nine': 'nine', + 'ten': 'ten', + 'eleven': 'eleven' + }, + 'list': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11], + 'tuple': (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11), + 'set': set([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]), + 'frozenset': frozenset([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]), + 'array': array('l', [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]), + 'deque': deque([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11], 15), + 'other': TestClassWithAVeryVeryVeryVeryVeryVeryVeryLongName() + } + + def _assert_shortened(self, key, expected): + shortener = ShortenerTransform(keys=[(key,)], **DEFAULT_LOCALS_SIZES) + result = transforms.transform(self.data, shortener) + + if key == 'dict': + self.assertEqual(expected, len(result)) + else: + # the repr output can vary between Python versions + stripped_result_key = result[key].strip("'\"u") + + if key == 'other': + self.assertIn(expected, stripped_result_key) + elif key != 'dict': + self.assertEqual(expected, stripped_result_key) + + # make sure nothing else was shortened + result.pop(key) + self.assertNotIn('...', str(result)) + self.assertNotIn('...', str(self.data)) + + def test_no_shorten(self): + shortener = ShortenerTransform(**DEFAULT_LOCALS_SIZES) + result = transforms.transform(self.data, shortener) + self.assertEqual(self.data, result) + + def test_shorten_string(self): + expected = '{}...{}'.format('x'*47, 'x'*48) + self._assert_shortened('string', expected) + + def test_shorten_long(self): + expected = '179556827339164684...002504519623752387L' + if six.PY3: + expected = '179556827339164684...5002504519623752387' + self._assert_shortened('long', expected) + + def test_shorten_mapping(self): + # here, expected is the number of key value pairs + expected = 10 + self._assert_shortened('dict', expected) + + def test_shorten_list(self): + expected = '[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...]' + self._assert_shortened('list', expected) + + def test_shorten_tuple(self): + expected = '(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...)' + self._assert_shortened('tuple', expected) + + def test_shorten_set(self): + expected = 'set([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...])' + if sys.version_info >= (3, 5): + expected = '{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...}' + self._assert_shortened('set', expected) + + def test_shorten_frozenset(self): + expected = 'frozenset([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...])' + if sys.version_info >= (3, 5): + expected = 'frozenset({1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...})' + self._assert_shortened('frozenset', expected) + + def test_shorten_array(self): + expected = 'array(\'l\', [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...])' + self._assert_shortened('array', expected) + + def test_shorten_deque(self): + expected = 'deque([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...])' + if sys.version_info >= (3, 5): + expected = '[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...]' + self._assert_shortened('deque', expected) + + def test_shorten_other(self): + expected = '=0.12.1', + 'six>=1.9.0' + ], + tests_require=tests_require, ) - diff --git a/shell.nix b/shell.nix new file mode 100644 index 00000000..edeea083 --- /dev/null +++ b/shell.nix @@ -0,0 +1,31 @@ +{ + pkgs ? import {} +}: + +with pkgs; +let +python = let + packageOverrides = self: super: { + pandas = super.pandas.overridePythonAttrs(old: { + doCheck = false; + }); + + twine = super.twine.overridePythonAttrs(old: { + doCheck = false; + }); + + tqdm = super.tqdm.overridePythonAttrs(old: { + doCheck = false; + }); + }; +in python36.override { inherit packageOverrides; }; +pyrollbar = pkgs.callPackage ./. { inherit python; }; +pyenv = python.withPackages(ps: with ps; [ pyrollbar twine unittest2 mock pyramid ]); + +in + +stdenv.mkDerivation { + name = "pyrollbar-shell"; + buildInputs = [ pyenv ]; +} +