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 [](https://travis-ci.org/rollbar/pyrollbar)
+# Pyrollbar
+[](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 = '