Repository: rollbar/pyrollbar Branch: master Commit: c01b721d24c1 Files: 104 Total size: 465.5 KB Directory structure: gitextract_s7qvive8/ ├── .flake8 ├── .github/ │ ├── pull_request_template.md │ └── workflows/ │ └── ci.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── THANKS.md ├── pyproject.toml └── rollbar/ ├── __init__.py ├── cli.py ├── contrib/ │ ├── __init__.py │ ├── asgi/ │ │ ├── __init__.py │ │ ├── integration.py │ │ ├── middleware.py │ │ └── types.py │ ├── bottle/ │ │ └── __init__.py │ ├── django/ │ │ ├── __init__.py │ │ ├── context_processors.py │ │ ├── middleware.py │ │ ├── models.py │ │ ├── tests.py │ │ └── utils.py │ ├── django_rest_framework/ │ │ └── __init__.py │ ├── fastapi/ │ │ ├── __init__.py │ │ ├── logger.py │ │ ├── middleware.py │ │ ├── routing.py │ │ └── utils.py │ ├── flask/ │ │ └── __init__.py │ ├── pyramid/ │ │ └── __init__.py │ ├── quart/ │ │ └── __init__.py │ ├── rq/ │ │ └── __init__.py │ └── starlette/ │ ├── __init__.py │ ├── logger.py │ ├── middleware.py │ └── requests.py ├── examples/ │ ├── asgi/ │ │ └── app.py │ ├── django/ │ │ └── app.py │ ├── fastapi/ │ │ ├── app.py │ │ ├── app_global_request.py │ │ ├── app_logger.py │ │ └── app_middleware.py │ ├── flask/ │ │ └── app.py │ ├── starlette/ │ │ ├── app.py │ │ ├── app_global_request.py │ │ └── app_logger.py │ └── twisted/ │ └── simpleserv.py ├── lib/ │ ├── __init__.py │ ├── _async.py │ ├── events.py │ ├── filters/ │ │ ├── __init__.py │ │ └── basic.py │ ├── payload.py │ ├── session.py │ ├── thread_pool.py │ ├── transform.py │ ├── transforms/ │ │ ├── __init__.py │ │ ├── batched.py │ │ ├── scrub.py │ │ ├── scrub_redact.py │ │ ├── scruburl.py │ │ ├── serializable.py │ │ └── shortener.py │ ├── transport.py │ ├── traverse.py │ └── type_info.py ├── logger.py └── test/ ├── __init__.py ├── asgi_tests/ │ ├── __init__.py │ ├── test_integration.py │ ├── test_middleware.py │ └── test_spec.py ├── async_tests/ │ ├── __init__.py │ └── test_async.py ├── fastapi_tests/ │ ├── __init__.py │ ├── test_logger.py │ ├── test_middleware.py │ ├── test_routing.py │ └── test_utils.py ├── flask_tests/ │ ├── __init__.py │ └── test_flask.py ├── starlette_tests/ │ ├── __init__.py │ ├── test_logger.py │ ├── test_middleware.py │ └── test_requests.py ├── test_basic_filters.py ├── test_batched_transform.py ├── test_custom_filters.py ├── test_lib.py ├── test_loghandler.py ├── test_pyramid.py ├── test_rollbar.py ├── test_scrub_redact_transform.py ├── test_scrub_transform.py ├── test_scruburl_transform.py ├── test_serializable_transform.py ├── test_session.py ├── test_shortener_transform.py ├── test_transform.py ├── test_traverse.py ├── twisted_tests/ │ ├── __init__.py │ └── test_twisted.py └── utils.py ================================================ FILE CONTENTS ================================================ ================================================ FILE: .flake8 ================================================ [flake8] max-line-length = 100 max-complexity = 10 exclude = test/* ================================================ FILE: .github/pull_request_template.md ================================================ ## Description of the change > Please include a summary of the change and which issues are fixed. > Please also include relevant motivation and context. ## Type of change - [ ] Bug fix (non-breaking change that fixes an issue) - [ ] New feature (non-breaking change that adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) - [ ] Maintenance - [ ] New release ## Related issues > Shortcut stories and GitHub issues (delete irrelevant) - Fix [SC-] - Fix #1 ## Checklists ### Development - [ ] Lint rules pass locally - [ ] The code changed/added as part of this pull request has been covered with tests - [ ] All tests related to the changed code pass in development ### Code review - [ ] This pull request has a descriptive title and information useful to a reviewer. There may be a screenshot or screencast attached - [ ] "Ready for review" label attached to the PR and reviewers assigned - [ ] Issue from task tracker has a link to this pull request - [ ] Changes have been reviewed by at least one other engineer ================================================ FILE: .github/workflows/ci.yml ================================================ name: Pyrollbar CI on: push: branches: [ master ] tags: [ v* ] pull_request: branches: [ master ] jobs: build: runs-on: ubuntu-latest strategy: matrix: python-version: [3.9, '3.10', 3.11, 3.12, 3.13, 3.14] framework: - NONE - FLASK_VERSION=2.3.3 - FLASK_VERSION=3.1.2 - DJANGO_VERSION=4.2.25 - DJANGO_VERSION=5.2.7 - PYRAMID_VERSION=1.10.8 - PYRAMID_VERSION=2.0.2 - FASTAPI_VERSION=0.115.1 httpx==0.27.2 python-multipart==0.0.12 - FASTAPI_VERSION=0.118.3 httpx==0.28.1 python-multipart==0.0.20 exclude: # Test frameworks on the python versions they support, according to pypi registry # Django - framework: DJANGO_VERSION=5.2.7 python-version: 3.9 steps: - uses: actions/checkout@v2 with: submodules: recursive - name: Setup Python uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - name: Install package with test dependencies run: pip install --group test . - name: Set the framework if: ${{ matrix.framework != 'NONE' }} run: echo ${{ matrix.framework }} >> $GITHUB_ENV - name: Install Flask if: ${{ contains(matrix.framework, 'FLASK_VERSION') }} run: pip install Flask==$FLASK_VERSION - name: Install Twisted if: ${{ contains(matrix.framework, 'TWISTED_VERSION') }} run: pip install Twisted==$TWISTED_VERSION idna==2.10 - name: Install Django if: ${{ contains(matrix.framework, 'DJANGO_VERSION') }} run: pip install Django==$DJANGO_VERSION - name: Install Pyramid if: ${{ contains(matrix.framework, 'PYRAMID_VERSION') }} run: pip install pyramid==$PYRAMID_VERSION - name: Install Starlette if: ${{ contains(matrix.framework, 'STARLETTE_VERSION') }} run: pip install starlette==$STARLETTE_VERSION - name: Install FastAPI if: ${{ contains(matrix.framework, 'FASTAPI_VERSION') }} run: pip install fastapi==$FASTAPI_VERSION - name: Run tests run: pytest ================================================ FILE: .gitignore ================================================ *.pyc *.swp *.swo dist/ build/ rollbar.egg-info/ *.egg .eggs/ .idea/ *~ Pipfile Pipfile.lock .pytest_cache/ .python-version .tox/ /venv* ================================================ FILE: CHANGELOG.md ================================================ # Change Log The change log is also available on the [GitHub Releases Page](https://github.com/rollbar/pyrollbar/releases). **1.4.0-beta** - Added support for Python 3.14 by @danielmorell in [#484](https://github.com/rollbar/pyrollbar/pull/484) - Added support for error correlation headers. by @danielmorell in [#491](https://github.com/rollbar/pyrollbar/pull/491) **1.3.0** - Added support for classes to define the method `__rollbar_repr__` to control how objects are serialized by @danielmorell in [#479](https://github.com/rollbar/pyrollbar/pull/479) - Added support for Python 3.13 by @danielmorell in [#477](https://github.com/rollbar/pyrollbar/pull/477) - Removed support for Python 3.6 by @danielmorell in [#480](https://github.com/rollbar/pyrollbar/pull/480) - Updated framework versions tested against by @danielmorell in [#480](https://github.com/rollbar/pyrollbar/pull/480) **1.2.0** - Added support for custom payload transforms by @danielmorell in [#470](https://github.com/rollbar/pyrollbar/pull/470) - Added custom data to the message body by @waltjones in [#473](https://github.com/rollbar/pyrollbar/pull/473) - Added support for host override setting by @danielmorell in [#468](https://github.com/rollbar/pyrollbar/pull/468) - Fixed `RollbarHandler` reconfigures root logger by @ilkecan in [#463](https://github.com/rollbar/pyrollbar/pull/463) - Fixed `include_request_body` setting not checked for various frameworks by @danielmorell in [#469](https://github.com/rollbar/pyrollbar/pull/469) - Fixed `namedtuple` fields not being scrubbed by @danielmorell in [#474](https://github.com/rollbar/pyrollbar/pull/474) **1.1.2** Fixed build missing entrypoints for pyramid and cli in [#471](https://github.com/rollbar/pyrollbar/pull/471) **1.1.1** Fixed #465 httpx 0.28.0 not compatible with pyrollbar by @danielmorell in [#466](https://github.com/rollbar/pyrollbar/pull/466) **1.1.0** - Updated supported/tested frameworks and modernized tests and packaging by @danielmorell in [#455](https://github.com/rollbar/pyrollbar/pull/455) - Fixed #398 FastAPI integration fails if docs are disabled by @danielmorell in [#459](https://github.com/rollbar/pyrollbar/pull/459) - Support `pathlib.Path()` objects by @singingwolfboy in [$450](https://github.com/rollbar/pyrollbar/pull/450) - Added support for Python 3.12 by @danielmorell in [#460](https://github.com/rollbar/pyrollbar/pull/460) - Changed the `ShortenerTransform` to use breadth first traversal by @danielmorell in [#461](https://github.com/rollbar/pyrollbar/pull/461) - Fixed shortener multi level shortening by @danielmorell and @pawelsz-rb. See [#449](https://github.com/rollbar/pyrollbar/pull/449) **1.1.0-beta** - Updated supported/tested frameworks and modernized tests and packaging by @danielmorell in [#455](https://github.com/rollbar/pyrollbar/pull/455) - Fixed #398 FastAPI integration fails if docs are disabled by @danielmorell in [#459](https://github.com/rollbar/pyrollbar/pull/459) - Support `pathlib.Path()` objects by @singingwolfboy in [$450](https://github.com/rollbar/pyrollbar/pull/450) - Added support for Python 3.12 by @danielmorell in [#460](https://github.com/rollbar/pyrollbar/pull/460) - Changed the `ShortenerTransform` to use breadth first traversal by @danielmorell in [#461](https://github.com/rollbar/pyrollbar/pull/461) **1.1.0-alpha** - Fixed shortener multi level shortening by @danielmorell and @pawelsz-rb. See [#449](https://github.com/rollbar/pyrollbar/pull/449) **1.0.0** - Fixed handling `sensitive_post_parameters` decorator in Django by @pawelsz-rb. See [#413](https://github.com/rollbar/pyrollbar/pull/413) - Fixed Werkzeug DeprecationWarning of `BaseRequest` by @compyman. See [#410](https://github.com/rollbar/pyrollbar/pull/410) - Fixed missing locals shortening on items with "trace_chain" instead of "trace" by @terencehonles. See [#365](https://github.com/rollbar/pyrollbar/pull/365) - Fixed FastAPI version comparison by @ayharano. See [#433](https://github.com/rollbar/pyrollbar/pull/433) - Fixed #436 `WSGIRequest` has no attribute `sensitive_post_parameters`. by @danielmorell. See [#437](https://github.com/rollbar/pyrollbar/pull/437) - Added new `thread_pool` handler by @danielmorell. See [#416](https://github.com/rollbar/pyrollbar/pull/416) - Added Rollbar branding to the readme by @paulserraino. See [#418](https://github.com/rollbar/pyrollbar/pull/418) - Added batched transform to increase sanitization and serialization performance by @ijsnow. See [#421](https://github.com/rollbar/pyrollbar/pull/421) - Replaced unittest2 with unittest by @brianr. See [#420](https://github.com/rollbar/pyrollbar/pull/420) - Removed unittest2 by @mcepl. See [#419](https://github.com/rollbar/pyrollbar/pull/419) - Remove deprecated flask `before_first_request` by @albertyw. See [#428](https://github.com/rollbar/pyrollbar/pull/428) - Removed support for Python 2 by @danielmorell. See [#435](https://github.com/rollbar/pyrollbar/pull/435) - Updated the base Ubuntu for CI to 20.04 test runner by @danielmorell and @waltjones. See [#427](https://github.com/rollbar/pyrollbar/pull/427) - Replaced `httpx.post` `data` kwarg with `content` by @ayharano. See [#425](https://github.com/rollbar/pyrollbar/pull/425) **0.16.3** - Pinned Python 2 CI tests to legacy version of dependencies. See [#408](https://github.com/rollbar/pyrollbar/pull/408) - Add PyPI badge for supported Python versions. See [#408](https://github.com/rollbar/pyrollbar/pull/401) - Add Django 4.0+ compatibility. See [#408](https://github.com/rollbar/pyrollbar/pull/400) - Update PR template. See [#408](https://github.com/rollbar/pyrollbar/pull/395) - SC-95272: Remove support for Python 3.3. See [#408](https://github.com/rollbar/pyrollbar/pull/394) **0.16.2** - Fix building person data in Django. See [#385](https://github.com/rollbar/pyrollbar/pull/385) - Fix circular error logging for non-HTTP events in Starlette. See [#390](https://github.com/rollbar/pyrollbar/pull/390) - Fix Python 3.4 builds. See [#389](https://github.com/rollbar/pyrollbar/pull/389) **0.16.1** - Fix PyPI artifacts **0.16.0** - Add support for FastAPI framework. See [#373](https://github.com/rollbar/pyrollbar/pull/373) - Add support for Starlette framework. See [#373](https://github.com/rollbar/pyrollbar/pull/373) - Add support for ASGI-based frameworks. See [#373](https://github.com/rollbar/pyrollbar/pull/373) - Add support for HTTPX async handler. See [#373](https://github.com/rollbar/pyrollbar/pull/373) - Add support for async report_exc_info and report_message. See [#373](https://github.com/rollbar/pyrollbar/pull/373) - Collect user IP from X-Forwarded-For, fall back to X-Real-Ip. See [#370](https://github.com/rollbar/pyrollbar/pull/370) - Improve examples. See [#368](https://github.com/rollbar/pyrollbar/pull/368) - Fix Python 3.3 builds. See [#374](https://github.com/rollbar/pyrollbar/pull/374) - Fix Flask 0.x builds. See [#376](https://github.com/rollbar/pyrollbar/pull/376) **0.15.2** - Add support for whitelist/blacklist for safelist/blocklist. See [#354](https://github.com/rollbar/pyrollbar/pull/343) - Add Twisted to the available frameworks. See [#360](https://github.com/rollbar/pyrollbar/pull/360) **0.15.1** - Add support to Python 3.8. See [#351](https://github.com/rollbar/pyrollbar/pull/351) - Fix deque test. See [#349](https://github.com/rollbar/pyrollbar/pull/349) - Add alternatives to exception handler. See [#335](https://github.com/rollbar/pyrollbar/pull/335) - Change dict_merge to allow strict mode. See [#339](https://github.com/rollbar/pyrollbar/pull/339) - Improve scrubbing test case. See [#343](https://github.com/rollbar/pyrollbar/pull/343) **0.15.0** - Prevent recursive re-raising of exceptions. See [#317](https://github.com/rollbar/pyrollbar/pull/317) - Correctly apply logger formatting to Rollbar messages. See [#312](https://github.com/rollbar/pyrollbar/pull/312) - Fix deprecation warnings. See [#325](https://github.com/rollbar/pyrollbar/pull/319) and [#331](https://github.com/rollbar/pyrollbar/pull/331) - Allow the request pool to be configured. See [#305](https://github.com/rollbar/pyrollbar/pull/305) - Use callable() instead of try/except TypeError. See [#319](https://github.com/rollbar/pyrollbar/pull/319) - Update Travis CI matrix. See [#317](https://github.com/rollbar/pyrollbar/pull/317) - Fix Travis build errors. See [#328](https://github.com/rollbar/pyrollbar/pull/328) - Update trove classifiers. See [#331](https://github.com/rollbar/pyrollbar/pull/331) **0.14.7** - Allow the raw request body to be included if desired. See [#304](https://github.com/rollbar/pyrollbar/pull/304) - Send Rollbar access token in HTTP header. See [#303](https://github.com/rollbar/pyrollbar/pull/303) - Add support for Django 1.7 & 1.8 in \_build_django_request_data. See [#301](https://github.com/rollbar/pyrollbar/pull/301) - Add support for Quart framework. See [#300](https://github.com/rollbar/pyrollbar/pull/300) **0.14.6** - Add the authorization header to the default scrub fields list. See [#299](https://github.com/rollbar/pyrollbar/pull/299) - Encode the payload properly for newer versions of Twisted. See [#298](https://github.com/rollbar/pyrollbar/pull/298) - Don't fail to send payloads because some inner object is not JSON serializable. See [#297](https://github.com/rollbar/pyrollbar/pull/297) - Allow floats as circular references. See [#291](https://github.com/rollbar/pyrollbar/pull/291) **0.14.5** - Fix bug in which error params were not being passed correctly to Pyramid middleware. See [#287](https://github.com/rollbar/pyrollbar/pull/287) **0.14.4** - Fix bug in Pyramid middleware where exc_info was not being passed to handle_error. See [#283](https://github.com/rollbar/pyrollbar/pull/283) - Fix bug where errors in the serialization of local variables caused errors to be dropped. They will now make it to Rollbar. See [#284](https://github.com/rollbar/pyrollbar/pull/284) **0.14.3** - Add support for HTTP(S) proxies. See [#276](https://github.com/rollbar/pyrollbar/pull/276) **0.14.2** - Fix bug with file-based logging config in Python 3. See [#277](https://github.com/rollbar/pyrollbar/issues/277) - Fix bug in Django middleware when request has no META attribute. See [#273](https://github.com/rollbar/pyrollbar/pull/273) **0.14.1** - Add Django middlewares that differentiate between 404 and other exceptions. See [#270](https://github.com/rollbar/pyrollbar/pull/270) - Make Werkzeug request handling more general. See [#271](https://github.com/rollbar/pyrollbar/pull/271) - Fix incorrect handling rollbar.init() arguments when using LOGGER in Django. See [#235](https://github.com/rollbar/pyrollbar/pull/235) - Use thread local storage for not thead-safe requests.Sessions. See [#269](https://github.com/rollbar/pyrollbar/pull/269) - Swallow known error that happens in add_person_data. See [#268](https://github.com/rollbar/pyrollbar/pull/268) - Only write a log about a rate limit once. See [#267](https://github.com/rollbar/pyrollbar/pull/267) - Adapt Django view monkey patch to support Django 2. See [#257](https://github.com/rollbar/pyrollbar/pull/257) - Add support request objects from Django REST framework. See [#18](https://github.com/rollbar/pyrollbar/pull/18) - Add support for Falcon framework requests. See [#51](https://github.com/rollbar/pyrollbar/pull/51) - Add support for Django Channels' AsgiRequest. See [#272](https://github.com/rollbar/pyrollbar/pull/272) **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 **0.11.3** - Obey safe repr for exceptions. See [#91](https://github.com/rollbar/pyrollbar/pull/91) **0.11.2** - Fixed a bug when calling logging.exception() when not in an exception handler. Now it correctly determines it doesn't have any exception info and uses report_message() instead of report_exc_info(). **0.11.1** - Added a new configuration option to expose the serializer's `safelisted_types` param - Allows users to safelist types to be serialized using `repr(obj)` instead of `str(type(obj))` - Fixed a bug that was not taking the `safe_repr` option into account. See [#87](https://github.com/rollbar/pyrollbar/pull/87) **0.11.0** - Overhauled the scrubbing and serialization mechanisms to provide deep object scrubbing and better handling of UTF-8 data from local variables. See [#75](https://github.com/rollbar/pyrollbar/pull/75) - This fixes a bunch of problems with reporting local variables, including `UnicodeEncodeError`s and attempting to read variables after the thread they were in has died. - Local variables and payload data is now sent over in their original structure. - If a variable was a `dict`, it will be transmitted as a `dict` instead of turned into a string representation of the variable. - The entire payload is now scrubbed and URL password fields are scrubbed as well. - Added a Django example. - Wrote many, many more tests :) - Integrated the `six` library to provide cleaner support for Python3. - Added some additional scrub fields. **0.10.1** - Added a warning message if `init()` is called more than once. **0.10.0** - Added support for Twisted framework. See [#69](https://github.com/rollbar/pyrollbar/pull/69) - Fix a bug that was causing max recursion errors while collecting local variables. See [#77](https://github.com/rollbar/pyrollbar/pull/77) - Added a configuration option, `safe_repr: True` which will cause payload serialization to use the type name for non-built-in objects. This option defaults to `True` which may cause data reported to Rollbar to contain less information for custom types. Prior to this change, serialization of custom objects called `__repr__()` which may have had undesired side effects. - Fixed a bug that did not correctly handle anonymous tuple arguments while gathering local variables. **0.9.14** - Fix logging loop when using Flask in a non-request context, and also using the Rollbar logging handler. See [#68](https://github.com/rollbar/pyrollbar/pull/68) **0.9.13** - If present, get request from log record. Otherwise try to guess current request as usual. **0.9.12** - Fix a bug that was causing a crash while reporting an error that happened in a Werkzeug request that had no `request.json`. See [#64](https://github.com/rollbar/pyrollbar/pull/64) **0.9.11** - Implement workarounds for NaN and Infinity "numbers" in payloads. See [#62](https://github.com/rollbar/pyrollbar/pull/62) **0.9.10** - Fix request data collection in Flask 0.9. See [#61](https://github.com/rollbar/pyrollbar/pull/61) **0.9.9** - Add exception handler for RQ (requires some instrumentation). See [#57](https://github.com/rollbar/pyrollbar/pull/57) - Scrub fields inside `extra_data` - Gather the process PID and report it along with the other 'server' data **0.9.8** - Support bare WSGI requests ([#55](https://github.com/rollbar/pyrollbar/pull/55)) **0.9.7** - Add support for Google App Engine ([#53](https://github.com/rollbar/pyrollbar/pull/53)) **0.9.6** - Fix memory leak when using the RollbarHandler logging handler (see [#43](https://github.com/rollbar/pyrollbar/pull/43)) - Fix bug where named function arguments were not scrubbed correctly **0.9.5** - Fix bug with local variable gathering that was breaking when getting the arguments for a class constructor. **0.9.4** - Request headers are now scrubbed, [pr#41](https://github.com/rollbar/pyrollbar/pull/41). **0.9.3** - `exception_level_filters` can now take a string that defines the class to filter, [#38](https://github.com/rollbar/pyrollbar/pull/38). **0.9.2** - Added an option to disable SSL certificate verification, [#36](https://github.com/rollbar/pyrollbar/pull/36). - Added `__version__` specifier to `__init__.py`. **0.9.1** New features: - For Tornado requests, gather the request start time. See [#33](https://github.com/rollbar/pyrollbar/pull/33) - Add handler which uses Tornado's `AsyncHTTPClient`. To use this, set your 'handler' to 'tornado'. See [#34](https://github.com/rollbar/pyrollbar/pull/34) **0.9.0** - Improvements to RollbarHandler logging handler. It now: - extracts more information out of each record (i.e. metadata like pathname and creation time) - uses the format string, with arguments not yet replaced, as the main message body. This will result in much better grouping in Rollbar. Note about upgrading from 0.8.x: unless you are using RollbarHandler, there are no breaking changes. If you are using RolbarHandler, then this will change the way your data appears in Rollbar (to the better, in our opinion). **0.8.3** - Provide a way to blocklist types from being repr()'d while gathering local variables. **0.8.2** - Fix uncaught ImproperlyConfigured exception when importing Rollbar in a Django REST Framework environment without a settings module loaded ([#28](https://github.com/rollbar/pyrollbar/pull/28)) **0.8.1** - Only attempt local variable extraction if traceback frames are of the correct type, print a warning otherwise - Fix JSON request param extraction for Werkzeug requests (Pyramid, Flask, etc) **0.8.0** - Local variables collection now enabled by default. - Fixed scrubbing for utf8 param names. **0.7.6** - Added local variables for all in-project frames and the last frame. **0.7.5** - Initial support for sending args and kwargs for traceback frames. - Optimization to send the access token in a header. **0.7.4** - Level kwarg added to `rollbar.report_exc_info()` ([#22](https://github.com/rollbar/pyrollbar/pull/22)) **0.7.3** - Added in an optional `endpoint` parameter to `search_items()`. **0.7.2** - Fix for scrubbing werkzeug json bodies ([#20](https://github.com/rollbar/pyrollbar/pull/20)) **0.7.1** - Support scrubbing for werkzeug json bodies ([#19](https://github.com/rollbar/pyrollbar/pull/19)) **0.7.0** - Python 3 support - Now support extracting data from Django REST framework requests - New `enabled` configuration setting **0.6.2** - Fixed json request data formatting for reports in Bottle requests - Now send json request data for Django and Pyramid apps - Set framework and request context properly for all reports in Flask and Bottle apps **0.6.1** - Added Django, Pyramid, Flask and Bottle support for default contexts. **0.6.0** - `report_message()` now returns the UUID of the reported occurrence. **0.5.14** - Fix bug with non-JSON post data in Flask - Add slightly better integration with Flask. See [rollbar-flask-example](https://github.com/rollbar/rollbar-flask-example) for example usage. **0.5.13** - Collect JSON post data in Flask when mimetype is `application/json` **0.5.12** - Add sys.argv to server data **0.5.11** - Don't report bottle.BaseResponse exceptions in the bottle plugin **0.5.10** - Added `code_version` configuration setting - Added support for bottle request objects **0.5.9** - Added a command line interface for reporting messages to Rollbar **0.5.8** - Added `allow_logging_basic_config` config flag for compatability with Flask. If using Flask, set to False. **0.5.7** - Added `exception_level_filters` configuration setting to customize the level that specific exceptions are reported as. **0.5.6** - First argument to `rollbar.report_exc_info()` is now optional. You can now call it with no arguments from within an `except` block, and it will behave is if you had called like `rollbar.report_exc_info(sys.exc_info())` **0.5.5** - Support for ignoring exceptions by setting `exc._rollbar_ignore = True`. Such exceptions reported through rollbar.report_exc_info() -- which is used under the hood in the Django and Pyramid middlewares -- will be ignored instead of reported. **0.5.4** - Django: catch exceptions when patching the debugview, for better support for django 1.3. **0.5.3** - Fixed bug when reporting messages without a request object **0.5.2** - Fixed bug where django debug page can get patched twice **0.5.1** - Catching possible malformed API responses **0.5.0** - Rename to rollbar **0.4.1** - report_exc_info() now takes two additional named args: `extra_data` and `payload_data`, like report_message(). - on 429 response (over rate limit), log a warning but don't parse and print an exception. **0.3.2** - Added new default scrub fields **0.3.1** - Fixed pypi package **0.3.0** - Merge django-ratchet and pyramid_ratchet into pyratchet - Add ability to write to a ratchet-agent log file **0.2.0** - Add "person" support **0.1.14** - Added payload_data arg to report_message() **0.1.13** - Added extra_data arg to report_message() **0.1.12** - Use custom JSON encoder to skip objects that can't be encoded. - Bump default timeout from 1 to 3 seconds. **0.1.11** - Sensitive params now scrubbed out of POST. Param name list is customizable via the `scrub_fields` config option. **0.1.10** - Add support for Tornado request objects (`tornado.httpserver.HTTPRequest`) **0.1.9** - Fix support for Pyramid request objects **0.1.8** - Add support for Django request objects ================================================ FILE: LICENSE ================================================ Copyright (c) 2014 Rollbar, Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================
Proactively discover, predict, and resolve errors in real-time with Rollbar’s error monitoring platform. Start tracking errors today!
[](https://github.com/rollbar/pyrollbar/actions)  Python notifier for reporting exceptions, errors, and log messages to [Rollbar](https://rollbar.com). ## Key benefits of using Pyrollbar are: - **Frameworks:** Pyrollbar supports popular Python frameworks such as Django, Flask, FastAPI, AWS Lambda and more! - **Automatic error grouping:** Rollbar aggregates Occurrences caused by the same error into Items that represent application issues. Learn more about reducing log noise. - **Advanced search:** Filter items by many different properties. Learn more about search. - **Customizable notifications:** Rollbar supports several messaging and incident management tools where your team can get notified about errors and important events by real-time alerts. Learn more about Rollbar notifications. ## Python Versions Supported | PyRollbar Version | Python Version Compatibility | Support Level | |-------------------|-----------------------------------------------|---------------------| | 1.4.0 | 3.9, 3.10, 3.11, 3.12, 3.13, 3.14 | Full | | 0.16.3 | 2.7, 3.4, 3.5, 3.6, 3.7. 3.8, 3.9, 3.10, 3.11 | Security Fixes Only | #### Support Level Definitions **Full** - We will support new features of the library and test against all supported versions. **Security Fixes Only** - We will only provide critical security fixes for the library. ## Frameworks Supported Generally, PyRollbar can be used with any Python framework. However, we have official support for the following frameworks: | Framework | Support Duration | Tested Versions | |-----------|----------------------------|-----------------| | Celery | Release +1 year | None | | Django | Release or LTS end +1 year | 4.2, 5.2 | | FastAPI | Release +1 year | 0.115, 0.118 | | Flask | Release +1 year | 2.3, 3.1 | | Pyramid | Release +1 year | 1.10, 2.0 | Official support means that we ship and maintain integrations for these frameworks. It also means that we test against these frameworks as part of our CI pipeline. Generally, we will support the last year of releases for a framework. If a framework has a defined support period (including LTS releases), we will support the release for the duration of that period plus one year. ### Community Supported There are also a number of community-supported integrations available. For more information, see the [Python SDK docs](https://docs.rollbar.com/docs/python-community-supported-sdks). ## Setup Instructions 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. ## Usage and Reference For complete usage instructions and configuration reference, see our [Python SDK docs](https://docs.rollbar.com/docs/python). ## 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) For bug reports, please [open an issue on GitHub](https://github.com/rollbar/pyrollbar/issues/new). ## 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` ================================================ FILE: THANKS.md ================================================ # Contributors Huge thanks to the following contributors (by github username). For the most up-to-date list, see https://github.com/rollbar/pyrollbar/graphs/contributors - [alex-laties](https://github.com/alex-laties) - [dmitry-mukhin](https://github.com/dmitry-mukhin) - [homm](https://github.com/homm) - [isra17](https://github.com/isra17) - [jamesonjlee](https://github.com/jamesonjlee) - [jbowes](https://github.com/jbowes) - [jbrumwell](https://github.com/jbrumwell) - [jedipi](https://github.com/jedipi) - [juggernaut](https://github.com/juggernaut) - [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) ================================================ FILE: pyproject.toml ================================================ [build-system] requires = ["setuptools>=61.0"] build-backend = "setuptools.build_meta" [project] name = "rollbar" dynamic = ["version"] description = "Easy and powerful exception tracking with Rollbar. Send messages and exceptions with arbitrary context, get back aggregates, and debug production issues quickly." readme = "README.md" license = {file = "LICENSE"} maintainers = [{name = "Rollbar, Inc.", email = "support@rollbar.com"}] classifiers = [ "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", "Programming Language :: Python :: 3 :: Only", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Development Status :: 5 - Production/Stable", "Environment :: Web Environment", "Framework :: AsyncIO", "Framework :: Bottle", "Framework :: Django", "Framework :: Flask", "Framework :: Pylons", "Framework :: Pyramid", "Framework :: Twisted", "Intended Audience :: Developers", "Topic :: Internet :: WWW/HTTP", "Topic :: Software Development", "Topic :: Software Development :: Bug Tracking", "Topic :: Software Development :: Testing", "Topic :: Software Development :: Quality Assurance", "Topic :: System :: Logging", "Topic :: System :: Monitoring", ] requires-python = ">=3.9" dependencies = [ "requests>=0.12.1", ] [dependency-groups] test = [ "blinker", "httpx", "pytest", "webob", ] [project.urls] Homepage = "https://rollbar.com/" Documentation = "https://docs.rollbar.com/docs/python" Changes = "https://github.com/rollbar/pyrollbar/blob/master/CHANGELOG.md" Source = "https://github.com/rollbar/pyrollbar/" [project.scripts] rollbar = "rollbar.cli:main" [project.entry-points."paste.filter_app_factory"] pyramid = "rollbar.contrib.pyramid:create_rollbar_middleware" [tool.pytest] testpaths = [ "rollbar/test", ] [tool.setuptools.dynamic] version = {attr = "rollbar.__version__"} [tool.tox] requires = ["tox>=4.22"] [tool.tox.env_run_base] dependency_groups = ["test"] description = "run unit tests" commands = [["pytest", "{posargs}"]] ================================================ FILE: rollbar/__init__.py ================================================ from __future__ import absolute_import, annotations from __future__ import unicode_literals import copy import functools import inspect import json import logging import os import socket import sys import threading import time import traceback import types import uuid import wsgiref.util import warnings import queue from urllib.parse import parse_qs, urljoin import requests from rollbar.lib import events, filters, dict_merge, transport, defaultJSONEncode from rollbar.lib.payload import Attribute from rollbar.lib.session import get_current_session, set_current_session, parse_session_request_baggage_headers __version__ = '1.4.0-beta' __log_name__ = 'rollbar' log = logging.getLogger(__log_name__) # import request objects from various frameworks, if available try: from webob import BaseRequest as WebobBaseRequest except ImportError: WebobBaseRequest = None try: from django.core.exceptions import ImproperlyConfigured except ImportError: DjangoHttpRequest = None RestFrameworkRequest = None else: try: from django.http import HttpRequest as DjangoHttpRequest except (ImportError, ImproperlyConfigured): DjangoHttpRequest = None try: from rest_framework.request import Request as RestFrameworkRequest except (ImportError, ImproperlyConfigured): RestFrameworkRequest = None del ImproperlyConfigured try: from werkzeug.wrappers import Request as WerkzeugRequest except (ImportError, SyntaxError): WerkzeugRequest = None try: from werkzeug.local import LocalProxy as WerkzeugLocalProxy except (ImportError, SyntaxError): WerkzeugLocalProxy = None try: from tornado.httpserver import HTTPRequest as TornadoRequest except ImportError: TornadoRequest = None try: from bottle import BaseRequest as BottleRequest 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, KeyError): AppEngineFetch = None try: from starlette.requests import Request as StarletteRequest except ImportError: StarletteRequest = None try: from fastapi.requests import Request as FastAPIRequest except ImportError: FastAPIRequest = None try: import httpx except ImportError: httpx = None AsyncHTTPClient = httpx def passthrough_decorator(func): def wrap(*args, **kwargs): return func(*args, **kwargs) return wrap try: from tornado.httpclient import AsyncHTTPClient as TornadoAsyncHTTPClient except ImportError: TornadoAsyncHTTPClient = None try: import treq from twisted.python import log as twisted_log from twisted.web.iweb import IPolicyForHTTPS from twisted.web.client import BrowserLikePolicyForHTTPS, Agent from twisted.internet.ssl import CertificateOptions from twisted.internet import task, defer, ssl, reactor from zope.interface import implementer @implementer(IPolicyForHTTPS) class VerifyHTTPS(object): def __init__(self): # by default, handle requests like a browser would self.default_policy = BrowserLikePolicyForHTTPS() def creatorForNetloc(self, hostname, port): # check if the hostname is in the the whitelist, otherwise return the default policy if not SETTINGS['verify_https']: return ssl.CertificateOptions(verify=False) return self.default_policy.creatorForNetloc(hostname, port) def log_handler(event): """ Default uncaught error handler """ try: if not event.get('isError') or 'failure' not in event: return err = event['failure'] # Don't report Rollbar internal errors to ourselves if issubclass(err.type, ApiException): log.error('Rollbar internal error: %s', err.value) else: report_exc_info((err.type, err.value, err.getTracebackObject())) 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: treq = None try: from falcon import Request as FalconRequest except ImportError: FalconRequest = None def get_request(): """ Get the current request object. Implementation varies on library support. Modified below when we know which framework is being used. """ # TODO(cory): add in a generic _get_locals_request() which # will iterate up through the call stack and look for a variable # that appears to be valid request object. for fn in (_get_fastapi_request, _get_starlette_request, _get_bottle_request, _get_flask_request, _get_pyramid_request, _get_pylons_request): try: req = fn() if req is not None: return req except: pass return None def _get_bottle_request(): if BottleRequest is None: return None from bottle import request return request def _get_flask_request(): if WerkzeugRequest is None: return None from flask import request return request def _get_pyramid_request(): if WebobBaseRequest is None: return None from pyramid.threadlocal import get_current_request return get_current_request() def _get_pylons_request(): if WebobBaseRequest is None: return None from pylons import request return request def _get_starlette_request(): # Do not modify the returned object if StarletteRequest is None: return None from rollbar.contrib.starlette import get_current_request return get_current_request() def _get_fastapi_request(): # Do not modify the returned object if FastAPIRequest is None: return None from rollbar.contrib.fastapi import get_current_request return get_current_request() BASE_DATA_HOOK = None agent_log = None VERSION = __version__ DEFAULT_ENDPOINT = 'https://api.rollbar.com/api/1/' DEFAULT_TIMEOUT = 3 ANONYMIZE = 'anonymize' DEFAULT_LOCALS_SIZES = { 'maxlevel': 5, 'maxdict': 10, 'maxlist': 10, 'maxtuple': 10, 'maxset': 10, 'maxfrozenset': 10, 'maxdeque': 10, 'maxarray': 10, 'maxstring': 100, 'maxlong': 40, 'maxother': 100, } # configuration settings # configure by calling init() or overriding directly SETTINGS = { 'access_token': None, 'enabled': True, 'environment': 'production', 'exception_level_filters': [], 'root': None, # root path to your code 'host': None, # custom hostname of the current host 'branch': None, # git branch name 'code_version': None, # 'blocking', 'thread' (default), 'async', 'agent', 'tornado', 'gae', 'twisted', 'httpx' or 'thread_pool' # 'async' requires Python 3.4 or higher. # 'httpx' requires Python 3.7 or higher. # 'thread_pool' requires Python 3.2 or higher. 'handler': 'default', 'thread_pool_workers': None, 'endpoint': DEFAULT_ENDPOINT, 'timeout': DEFAULT_TIMEOUT, 'agent.log_file': 'log.rollbar', 'scrub_fields': [ 'pw', 'passwd', 'password', 'secret', 'confirm_password', 'confirmPassword', 'password_confirmation', 'passwordConfirmation', 'access_token', 'accessToken', 'auth', 'authentication', 'authorization', ], 'url_fields': ['url', 'link', 'href'], 'notifier': { 'name': 'pyrollbar', 'version': VERSION }, 'allow_logging_basic_config': True, # set to False to avoid a call to logging.basicConfig() 'locals': { 'enabled': True, 'safe_repr': True, 'scrub_varargs': True, 'sizes': DEFAULT_LOCALS_SIZES, 'safelisted_types': [], 'whitelisted_types': [] }, 'verify_https': True, 'shortener_keys': [], 'suppress_reinit_warning': False, 'capture_email': False, 'capture_username': False, 'capture_ip': True, 'log_all_rate_limited_items': True, 'log_payload_on_error': True, 'http_proxy': None, 'http_proxy_user': None, 'http_proxy_password': None, 'include_request_body': False, 'request_pool_connections': None, 'request_pool_maxsize': None, 'request_max_retries': None, 'batch_transforms': False, 'custom_transforms': [], } _CURRENT_LAMBDA_CONTEXT = None _LAST_RESPONSE_STATUS = None # Set in init() _transforms = [] _serialize_transform = None _scrub_redact_transform = None _initialized = False from rollbar.lib.transforms.scrub_redact import REDACT_REF from rollbar.lib import transforms from rollbar.lib import type_info 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 from rollbar.lib.transforms.batched import BatchedTransform ## public api def init(access_token, environment='production', scrub_fields=None, url_fields=None, **kw): """ Saves configuration variables in this module's SETTINGS. access_token: project access token. Get this from the Rollbar UI: - click "Settings" in the top nav - click "Projects" in the left nav - copy-paste the appropriate token. environment: environment name. Can be any string; suggestions: 'production', 'development', 'staging', 'yourname' **kw: provided keyword arguments will override keys in SETTINGS. """ global SETTINGS, agent_log, _initialized, _transforms, _serialize_transform, _scrub_redact_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. if not SETTINGS.get('suppress_reinit_warning'): log.warning('Rollbar already initialized. Ignoring re-init.') return SETTINGS['access_token'] = access_token SETTINGS['environment'] = environment _configure_transport(**SETTINGS) if SETTINGS.get('allow_logging_basic_config'): logging.basicConfig() if SETTINGS.get('handler') == 'agent': agent_log = _create_agent_log() elif SETTINGS.get('handler') == 'thread_pool': from rollbar.lib.thread_pool import init_pool init_pool(SETTINGS.get('thread_pool_workers', None)) if not SETTINGS['locals']['safelisted_types'] and SETTINGS['locals']['whitelisted_types']: warnings.warn('whitelisted_types deprecated use safelisted_types instead', DeprecationWarning) SETTINGS['locals']['safelisted_types'] = SETTINGS['locals']['whitelisted_types'] # We will perform these transforms in order: # 1. Serialize the payload to be all python built-in objects # 2. Scrub the payloads based on the key suffixes in SETTINGS['scrub_fields'] # 3. Scrub URLs in the payload for keys that end with 'url' # 4. Optional - If local variable gathering is enabled, transform the # trace frame values using the ShortReprTransform. _serialize_transform = SerializableTransform(safe_repr=SETTINGS['locals']['safe_repr'], safelist_types=SETTINGS['locals']['safelisted_types']) _scrub_redact_transform = ScrubRedactTransform(suffixes=[(field,) for field in SETTINGS['scrub_fields']], redact_char='*') # 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'), ] if SETTINGS['locals']['enabled']: for prefix in (('body', 'trace'), ('body', 'trace_chain', '*')): shortener_keys.append(prefix + ('frames', '*', 'code')) shortener_keys.append(prefix + ('frames', '*', 'args', '*')) shortener_keys.append(prefix + ('frames', '*', 'kwargs', '*')) shortener_keys.append(prefix + ('frames', '*', 'locals', '*')) shortener_keys.extend(SETTINGS['shortener_keys']) shortener = ShortenerTransform(safe_repr=SETTINGS['locals']['safe_repr'], keys=shortener_keys, **SETTINGS['locals']['sizes']) _transforms = [ shortener, # priority: 10 _scrub_redact_transform, # priority: 20 _serialize_transform, # priority: 30 ScrubUrlTransform(suffixes=[(field,) for field in SETTINGS['url_fields']], params_to_scrub=SETTINGS['scrub_fields']) # priority: 50 ] # Add custom transforms if len(SETTINGS['custom_transforms']) > 0: _transforms.extend(SETTINGS['custom_transforms']) # Sort the transforms by priority _transforms = sorted(_transforms, key=lambda x: x.priority) _threads = queue.Queue() events.reset() filters.add_builtin_filters(SETTINGS) _initialized = True def _configure_transport(**kw): configuration = _requests_configuration(**kw) transport.configure_pool(**configuration) def _requests_configuration(**kw): keys = { 'request_pool_connections': 'pool_connections', 'request_pool_maxsize': 'pool_maxsize', 'request_max_retries': 'max_retries', } return {keys[k]: kw.get(k, None) for k in keys} 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, 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') kw: provided for legacy purposes; unused. Example usage: rollbar.init(access_token='YOUR_PROJECT_ACCESS_TOKEN') try: do_something() except: rollbar.report_exc_info(sys.exc_info(), request, {'foo': 'bar'}, {'level': 'warning'}) """ if exc_info is None: exc_info = sys.exc_info() try: return _report_exc_info(exc_info, request, extra_data, payload_data, level=level) except Exception as e: log.exception("Exception while reporting exc_info to Rollbar. %r", e) def report_message(message, level='error', request=None, extra_data=None, payload_data=None): """ Reports an arbitrary string message to Rollbar. message: the string body of the message level: level to report at. One of: 'critical', 'error', 'warning', 'info', 'debug' request: the request object for the context of the message extra_data: optional, will be included in the 'custom' section of the payload payload_data: param names to pass in the 'data' level of the payload; overrides defaults. """ try: return _report_message(message, level, request, extra_data, payload_data) except Exception as e: log.exception("Exception while reporting message to Rollbar. %r", e) def send_payload(payload, access_token): """ Sends a payload object, (the result of calling _build_payload() + _serialize_payload()). Uses the configured handler from SETTINGS['handler'] Available handlers: - 'blocking': calls _send_payload() (which makes an HTTP request) immediately, blocks on it - 'thread': starts a single-use thread that will call _send_payload(). returns immediately. - 'async': calls _send_payload_async() (which makes an async HTTP request using default async handler) - '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 request using Twisted and Treq) - 'httpx': calls _send_payload_httpx() (which makes an async HTTP request using HTTPX) - 'thread_pool': uses a pool of worker threads to make HTTP requests off the main thread. Returns immediately. """ payload = events.on_payload(payload) if payload is False: return if sys.version_info >= (3, 6): from rollbar.lib._async import get_current_handler handler = get_current_handler() else: handler = SETTINGS.get('handler') if handler == 'twisted': payload['data']['framework'] = 'twisted' payload_str = _serialize_payload(payload) if handler == 'blocking': _send_payload(payload_str, access_token) elif handler == 'agent': agent_log.error(payload_str) elif handler == 'tornado': if TornadoAsyncHTTPClient is None: log.error('Unable to find tornado') return _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_str, access_token) elif handler == 'twisted': if treq is None: log.error('Unable to find Treq') return _send_payload_twisted(payload_str, access_token) elif handler == 'httpx': if httpx is None: log.error('Unable to find HTTPX') return _send_payload_httpx(payload_str, access_token) elif handler == 'async': if AsyncHTTPClient is None: log.error('Unable to find async handler') return _send_payload_async(payload_str, access_token) elif handler == 'thread': _send_payload_thread(payload_str, access_token) elif handler == 'thread_pool': _send_payload_thread_pool(payload_str, access_token) else: # default to 'thread' _send_payload_thread(payload_str, access_token) def search_items(title, return_fields=None, access_token=None, endpoint=None, **search_fields): """ Searches a project for items that match the input criteria. title: all or part of the item's title to search for. return_fields: the fields that should be returned for each item. e.g. ['id', 'project_id', 'status'] will return a dict containing only those fields for each item. access_token: a project access token. If this is not provided, the one provided to init() will be used instead. search_fields: additional fields to include in the search. currently supported: status, level, environment """ if not title: return [] if return_fields is not None: return_fields = ','.join(return_fields) return _get_api('search/', title=title, fields=return_fields, access_token=access_token, endpoint=endpoint, **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 response from an API call. """ pass class ApiError(ApiException): """ This exception will be raised if the API response contains an 'err' field, denoting there was a problem fulfilling the api request. """ pass class Result(object): """ This class encapsulates the response from an API call. Usage: result = search_items(title='foo', fields=['id']) print result.data """ def __init__(self, access_token, path, params, data): self.access_token = access_token self.path = path self.params = params self.data = data def __str__(self): return str(self.data) class PagedResult(Result): """ This class wraps the response from an API call that responded with a page of results. Usage: result = search_items(title='foo', fields=['id']) print 'First page: %d, data: %s' % (result.page, result.data) result = result.next_page() print 'Second page: %d, data: %s' % (result.page, result.data) """ def __init__(self, access_token, path, page_num, params, data, endpoint=None): super(PagedResult, self).__init__(access_token, path, params, data) self.page = page_num self.endpoint = endpoint def next_page(self): params = copy.copy(self.params) params['page'] = self.page + 1 return _get_api(self.path, endpoint=self.endpoint, **params) def prev_page(self): if self.page <= 1: return self params = copy.copy(self.params) params['page'] = self.page - 1 return _get_api(self.path, endpoint=self.endpoint, **params) ## internal functions def _resolve_exception_class(idx, filter): cls, level = filter if isinstance(cls, str): # Lazily resolve class name parts = cls.split('.') module = '.'.join(parts[:-1]) if module in sys.modules and hasattr(sys.modules[module], parts[-1]): cls = getattr(sys.modules[module], parts[-1]) SETTINGS['exception_level_filters'][idx] = (cls, level) else: 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) if cls and isinstance(exception, cls): return level return None def _is_ignored(exception): return _filtered_level(exception) == 'ignored' def _create_agent_log(): """ Creates .rollbar log file for use with rollbar-agent """ 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.") log_file = DEFAULTS['agent.log_file'] retval = logging.getLogger('rollbar_agent') handler = logging.FileHandler(log_file, 'a', 'utf-8') formatter = logging.Formatter('%(message)s') handler.setFormatter(formatter) retval.addHandler(handler) retval.setLevel(logging.WARNING) return retval def _report_exc_info(exc_info, request, extra_data, payload_data, level=None): """ Called by report_exc_info() wrapper """ if not _check_config(): return filtered_level = _filtered_level(exc_info[1]) if level is None: level = filtered_level filtered_exc_info = events.on_exception_info(exc_info, request=request, extra_data=extra_data, payload_data=payload_data, level=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 # walk the trace chain to collect cause and context exceptions trace_chain = _walk_trace_chain(cls, exc, trace) 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 not isinstance(extra_data, dict): extra_data = {'value': extra_data} if extra_trace_data: extra_data = dict_merge(extra_data, extra_trace_data, silence_errors=True) 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) _add_session_data(data) data['server'] = _build_server_data() if payload_data: data = dict_merge(data, payload_data, silence_errors=True) payload = _build_payload(data) send_payload(payload, payload.get('access_token')) return data['uuid'] def _walk_trace_chain(cls, exc, trace): trace_chain = [_trace_data(cls, exc, trace)] seen_exceptions = {exc} 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))) if exc in seen_exceptions: break seen_exceptions.add(exc) 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': str(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 """ 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': filtered_message } } if extra_data: extra_data = extra_data data['body']['message'].update(extra_data) data['custom'] = extra_data request = _get_actual_request(request) _add_request_data(data, request) _add_person_data(data, request) _add_lambda_context_data(data) _add_session_data(data) data['server'] = _build_server_data() if payload_data: data = dict_merge(data, payload_data, silence_errors=True) payload = _build_payload(data) send_payload(payload, payload.get('access_token')) return data['uuid'] def _add_session_data(data: dict) -> None: """ Adds session data to the payload data if it can be found in the current session or request. """ session_data = get_current_session() if session_data: _add_session_attributes(data, session_data) return request = _session_data_from_request(data) if request is None: return session_data = parse_session_request_baggage_headers(request.get('headers', None)) if session_data: _add_session_attributes(data, session_data) def _add_session_attributes(data: dict, session_data: list[Attribute]) -> None: """ Adds session attributes to the payload data. This function is careful to not overwrite any existing data in the payload. """ if 'attributes' not in data: data['attributes'] = session_data return existing_keys = {a['key'] for a in data['attributes']} for attribute in session_data: if attribute['key'] not in existing_keys: data['attributes'].append(attribute) def _session_data_from_request(data: dict) -> dict: """ Tries to find session data in the request object. Use the request object if provided, otherwise check the data as it may already contain the request object. This is true for some frameworks (e.g. Django). """ if data is not None and 'request' in data: return data.get('request', None) return _get_actual_request(_build_request_data(get_request())) def _check_config(): if not SETTINGS.get('enabled'): 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.") return False return True def _build_base_data(request, level='error'): data = { 'timestamp': int(time.time()), 'environment': SETTINGS['environment'], 'level': level, 'language': 'python %s' % '.'.join(str(x) for x in sys.version_info[:3]), 'notifier': SETTINGS['notifier'], 'uuid': str(uuid.uuid4()), } if SETTINGS.get('code_version'): data['code_version'] = SETTINGS['code_version'] if BASE_DATA_HOOK: BASE_DATA_HOOK(request, data) return data 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 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 def _build_person_data(request): """ Returns a dictionary describing the logged-in user using data from `request`. Try request.rollbar_person first, then 'user', then 'user_id' """ if hasattr(request, 'rollbar_person'): rollbar_person_prop = request.rollbar_person person = rollbar_person_prop() if callable(rollbar_person_prop) else rollbar_person_prop if person and isinstance(person, dict): return person else: return None if StarletteRequest: from rollbar.contrib.starlette.requests import hasuser else: def hasuser(request): return True if hasuser(request) and hasattr(request, 'user'): user_prop = request.user user = user_prop() if callable(user_prop) else user_prop if not user: return None elif isinstance(user, dict): return user else: retval = {} if getattr(user, 'id', None): retval['id'] = str(user.id) elif getattr(user, 'user_id', None): retval['id'] = str(user.user_id) # 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': username, 'email': email }) return retval if hasattr(request, 'user_id'): user_id_prop = request.user_id user_id = user_id_prop() if callable(user_id_prop) else user_id_prop if not user_id: return None return {'id': str(user_id)} def _get_func_from_frame(frame): func_name = inspect.getframeinfo(frame).function caller = frame.f_back if caller: func = caller.f_locals.get(func_name, caller.f_globals.get(func_name)) else: func = None return func def _add_locals_data(trace_data, exc_info): if not SETTINGS['locals']['enabled']: return frames = trace_data['frames'] cur_tb = exc_info[2] frame_num = 0 num_frames = len(frames) while cur_tb: cur_frame = frames[frame_num] tb_frame = cur_tb.tb_frame cur_tb = cur_tb.tb_next if not isinstance(tb_frame, types.FrameType): # this can happen if the traceback or frame is wrapped in some way, # for example by `ExceptionInfo` in # https://github.com/celery/billiard/blob/master/billiard/einfo.py log.warning('Traceback frame not a types.FrameType. Ignoring.') frame_num += 1 continue # Create placeholders for argspec/varargspec/keywordspec/locals argspec = None varargspec = None keywordspec = None _locals = {} try: arginfo = inspect.getargvalues(tb_frame) # Optionally fill in locals for this frame if arginfo.locals and _check_add_locals(cur_frame, frame_num, num_frames): # Get all of the named args argspec = 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 argspec: cur_frame['argspec'] = argspec if varargspec: cur_frame['varargspec'] = varargspec if keywordspec: cur_frame['keywordspec'] = keywordspec if _locals: try: cur_frame['locals'] = {k: _serialize_frame_data(v) for k, v in _locals.items()} except Exception: log.exception('Error while serializing frame data.') frame_num += 1 def _serialize_frame_data(data): return transforms.transform( data, [_scrub_redact_transform, _serialize_transform], batch_transforms=SETTINGS['batch_transforms'] ) 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, silence_errors=True) 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): """ Attempts to build request data; if successful, sets the 'request' key on `data`. """ try: request_data = _build_request_data(request) except Exception as e: 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 def _check_add_locals(frame, frame_num, total_frames): """ Returns True if we should record local variables for the given frame. """ # Include the last frames locals # Include any frame locals that came from a file in the project's root root = SETTINGS.get('root') if root: # coerce to string, in case root is a Path object root = str(root) else: root = '' return any(((frame_num == total_frames - 1), ('root' in SETTINGS and (frame.get('filename') or '').lower().startswith(root.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. """ # webob (pyramid) if WebobBaseRequest and isinstance(request, WebobBaseRequest): return _build_webob_request_data(request) # django if DjangoHttpRequest and isinstance(request, DjangoHttpRequest): return _build_django_request_data(request) # django rest framework if RestFrameworkRequest and isinstance(request, RestFrameworkRequest): return _build_django_request_data(request) # werkzeug (flask) if WerkzeugRequest and isinstance(request, WerkzeugRequest): return _build_werkzeug_request_data(request) # tornado if TornadoRequest and isinstance(request, TornadoRequest): return _build_tornado_request_data(request) # bottle 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) # FastAPI (built on top of Starlette, so keep the order) if FastAPIRequest and isinstance(request, FastAPIRequest): return _build_fastapi_request_data(request) # Starlette (should be the last one for Starlette based frameworks) if StarletteRequest and isinstance(request, StarletteRequest): return _build_starlette_request_data(request) return None def _build_webob_request_data(request): request_data = { 'url': request.url, 'GET': dict(request.GET), 'user_ip': _extract_user_ip(request), 'headers': dict(request.headers), 'method': request.method, } try: if request.json: request_data['json'] = request.json except: pass # pyramid matchdict if getattr(request, 'matchdict', None): request_data['params'] = request.matchdict # workaround for webob bug when the request body contains binary data but has a text # content-type try: request_data['POST'] = dict(request.POST) except UnicodeDecodeError: request_data['body'] = request.body return request_data def _extract_wsgi_headers(items): headers = {} for k, v in items: if k.startswith('HTTP_'): header_name = '-'.join(k[len('HTTP_'):].replace('_', ' ').title().split(' ')) headers[header_name] = v return headers def _build_django_request_data(request): url = request.build_absolute_uri() request_data = { 'url': url, 'method': request.method, 'GET': dict(request.GET), 'POST': dict(request.POST), 'user_ip': _wsgi_extract_user_ip(request.META), } if SETTINGS['include_request_body']: try: request_data['body'] = request.body except: pass request_data['headers'] = _extract_wsgi_headers(request.META.items()) return request_data def _build_werkzeug_request_data(request): request_data = { 'url': request.url, 'GET': dict(request.args), 'POST': dict(request.form), 'user_ip': _extract_user_ip(request), 'headers': dict(request.headers), 'method': request.method, 'files_keys': list(request.files.keys()), } if SETTINGS['include_request_body']: try: if request.json: request_data['body'] = request.json except Exception: pass return request_data def _build_tornado_request_data(request): request_data = { 'url': request.full_url(), 'user_ip': request.remote_ip, 'headers': dict(request.headers), 'method': request.method, 'files_keys': request.files.keys(), 'start_time': getattr(request, '_start_time', None), } request_data[request.method] = request.arguments return request_data def _build_bottle_request_data(request): request_data = { 'url': request.url, 'user_ip': request.remote_addr, 'headers': dict(request.headers), 'method': request.method, 'GET': dict(request.query) } if SETTINGS['include_request_body']: if request.json: try: request_data['body'] = request.body.getvalue() except: pass else: request_data['POST'] = dict(request.forms) 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 SETTINGS['include_request_body']: 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), 'user_ip': _wsgi_extract_user_ip(request), 'method': request.get('REQUEST_METHOD'), } if 'QUERY_STRING' in request: request_data['GET'] = parse_qs(request['QUERY_STRING'], keep_blank_values=True) # Collapse single item arrays request_data['GET'] = {k: (v[0] if len(v) == 1 else v) for k, v in request_data['GET'].items()} request_data['headers'] = _extract_wsgi_headers(request.items()) if SETTINGS['include_request_body']: try: length = int(request.get('CONTENT_LENGTH', 0)) except ValueError: length = 0 input = request.get('wsgi.input') if length and input and hasattr(input, 'seek') and hasattr(input, 'tell'): pos = input.tell() input.seek(0, 0) request_data['body'] = input.read(length) input.seek(pos, 0) return request_data def _build_starlette_request_data(request): from starlette.datastructures import UploadFile request_data = { 'url': str(request.url), 'GET': dict(request.query_params), 'headers': dict(request.headers), 'method': request.method, 'user_ip': _starlette_extract_user_ip(request), 'params': dict(request.path_params), } if hasattr(request, '_form') and request._form is not None: request_data['POST'] = { k: v.filename if isinstance(v, UploadFile) else v for k, v in request._form.items() } request_data['files_keys'] = [ field.filename for field in request._form.values() if isinstance(field, UploadFile) ] if hasattr(request, '_body'): body = request._body.decode() else: body = None if body and SETTINGS['include_request_body']: request_data['body'] = body if hasattr(request, '_json'): request_data['json'] = request._json elif body: try: request_data['json'] = json.loads(body) except json.JSONDecodeError: pass # Filter out empty values request_data = {k: v for k, v in request_data.items() if v} return request_data def _build_fastapi_request_data(request): return _build_starlette_request_data(request) 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. """ # server environment host = SETTINGS.get('host') or socket.gethostname() server_data = { 'host': host, '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] return server_data def _transform(obj, key=None): return transforms.transform( obj, _transforms, key=key, batch_transforms=SETTINGS['batch_transforms'] ) def _build_payload(data): """ Returns the full payload as a string. """ for k, v in data.items(): data[k] = _transform(v, key=(k,)) payload = { 'access_token': SETTINGS['access_token'], 'data': data } return payload def _serialize_payload(payload): return json.dumps(payload, default=defaultJSONEncode) def _send_payload(payload_str, access_token): try: _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_thread(payload_str, access_token): thread = threading.Thread(target=_send_payload, args=(payload_str, access_token)) _threads.put(thread) thread.start() def _send_payload_pool(payload_str, access_token): try: _post_api('item/', payload_str, access_token=access_token) except Exception as e: log.exception('Exception while posting item %r', e) def _send_payload_thread_pool(payload_str, access_token): from rollbar.lib.thread_pool import submit submit(_send_payload_pool, payload_str, access_token) def _send_payload_appengine(payload_str, access_token): try: _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_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 = AppEngineFetch(url, method="POST", 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_str, resp) 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 = 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_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 = 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_str, access_token): try: _post_api_tornado('item/', payload_str, access_token=access_token) except Exception as e: log.exception('Exception while posting item %r', e) 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) 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) 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_str, access_token): try: _post_api_twisted('item/', payload_str, access_token=access_token) except Exception as e: log.exception('Exception while posting item %r', e) 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; charset=utf-8']} if access_token is not None: headers['X-Rollbar-Access-Token'] = [access_token] url = urljoin(SETTINGS['endpoint'], path) try: encoded_payload = payload_str.encode('utf8') except (UnicodeDecodeError, UnicodeEncodeError): encoded_payload = payload_str treq_client = treq.client.HTTPClient(Agent(reactor, contextFactory=VerifyHTTPS())) d = treq_client.post(url, encoded_payload, headers=headers, timeout=SETTINGS.get('timeout', DEFAULT_TIMEOUT)) d.addCallback(post_cb) def _send_payload_httpx(payload_str, access_token): from rollbar.lib._async import call_later, _post_api_httpx try: call_later(_post_api_httpx('item/', payload_str, access_token=access_token)) except Exception as e: log.exception('Exception while posting item %r', e) def _send_payload_async(payload_str, access_token): try: _send_payload_httpx(payload_str, access_token=access_token) except Exception as e: log.exception('Exception while posting item %r', e) 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, } payload = _build_payload(data) 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: 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: if SETTINGS['log_all_rate_limited_items'] or not last_response_was_429: log.warning("Rollbar: over rate limit, data was dropped.") if SETTINGS['log_payload_on_error']: log.warning("Payload was: %r", params) return 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.", uuid) if SETTINGS['log_payload_on_error']: log.error("Payload:\n%r", 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) # TODO: Should we also return here? try: json_data = json.loads(data) except (TypeError, ValueError): log.exception('Could not decode Rollbar api response:\n%s', data) raise ApiException('Request to %s returned invalid JSON response', path) else: if json_data.get('err'): raise ApiError(json_data.get('message') or 'Unknown error') result = json_data.get('result', {}) if 'page' in result: return PagedResult(access_token, path, result['page'], params, result, endpoint=endpoint) else: return Result(access_token, path, params, result) def _extract_user_ip_from_headers(request): forwarded_for = request.headers.get('X-Forwarded-For') if forwarded_for: return forwarded_for real_ip = request.headers.get('X-Real-Ip') if real_ip: return real_ip return None def _extract_user_ip(request): return _extract_user_ip_from_headers(request) or request.remote_addr def _wsgi_extract_user_ip(environ): forwarded_for = environ.get('HTTP_X_FORWARDED_FOR') if forwarded_for: return forwarded_for real_ip = environ.get('HTTP_X_REAL_IP') if real_ip: return real_ip return environ['REMOTE_ADDR'] def _starlette_extract_user_ip(request): if not hasattr(request, 'client'): return _extract_user_ip_from_headers(request) if not hasattr(request.client, 'host'): return _extract_user_ip_from_headers(request) return request.client.host or _extract_user_ip_from_headers(request) ================================================ FILE: rollbar/cli.py ================================================ import optparse import sys import rollbar VERSION = '0.1' verbose = False def _gen_report_message(level): def _wrapped(lines): line_data = '\n'.join(lines) if verbose: print('Rollbar [%s]: %s' % (level, line_data)) rollbar.report_message(line_data, level=level, extra_data={'cli_version': VERSION}) return _wrapped CMDS = { 'debug': _gen_report_message('debug'), 'info': _gen_report_message('info'), 'warning': _gen_report_message('warning'), 'error': _gen_report_message('error'), 'critical': _gen_report_message('critical'), } def main(): global verbose parser = optparse.OptionParser(version='%prog version ' + VERSION) parser.add_option('-t', '--access_token', dest='access_token', help="You project's access token from rollbar.com.", metavar='ACCESS_TOKEN') parser.add_option('-e', '--environment', dest='environment', 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', help="The Rollbar API endpoint url to send data to.", metavar='ENDPOINT_URL', default=rollbar.DEFAULT_ENDPOINT) parser.add_option('-m', '--handler', dest='handler', help="The method in which to report errors.", metavar='HANDLER', choices=["thread", "blocking", "agent"], default="blocking") parser.add_option('-v', '--verbose', dest='verbose', help="Print verbose output.", action='store_true', default=False) options, args = parser.parse_args() access_token = options.access_token env = options.environment endpoint = options.endpoint_url handler = options.handler verbose = options.verbose if not access_token: parser.error('missing access_token') if not env: parser.error('missing environment') rollbar.init(access_token, environment=env, endpoint=endpoint, handler=handler) def _do_cmd(cmd_name, line): cmd = CMDS.get(cmd_name.lower()) if cmd: cmd([line]) return True return False if len(args) > 1: sent = _do_cmd(args[0], ' '.join(args[1:])) sys.exit(0 if sent else 1) cur_cmd_name = None try: cur_line = sys.stdin.readline() while cur_line: cur_line = cur_line.strip() parts = cur_line.split(' ') if parts: cur_cmd_name = parts[0] parts = parts[1:] _do_cmd(cur_cmd_name, ' '.join(parts)) cur_line = sys.stdin.readline() except (KeyboardInterrupt, SystemExit) as e: pass ================================================ FILE: rollbar/contrib/__init__.py ================================================ ================================================ FILE: rollbar/contrib/asgi/__init__.py ================================================ __all__ = ['ReporterMiddleware'] from .middleware import ReporterMiddleware ================================================ FILE: rollbar/contrib/asgi/integration.py ================================================ import inspect import functools import rollbar class IntegrationBase: """ Superclass for class integrations. """ def __init__(self): if hasattr(self, '_integrate'): self._integrate() class integrate: """ Integrates functions and classes (derived from IntegrationBase) in the SDK. """ def __init__(self, *, framework_name='unknown'): self._framework_name = framework_name def __call__(self, obj): if inspect.isclass(obj): obj._integrate = self._register_hook return obj else: return self._insert_hook(obj) def _register_hook(self): def _hook(request, data): data['framework'] = self._framework_name rollbar.BASE_DATA_HOOK = _hook def _insert_hook(self, func): @functools.wraps(func) def wrapper(*args, **kwargs): self._register_hook() return func(*args, **kwargs) return wrapper ================================================ FILE: rollbar/contrib/asgi/middleware.py ================================================ import logging import sys from typing import Iterable import rollbar from .integration import IntegrationBase, integrate from .types import ASGIApp, Receive, Scope, Send from rollbar.lib._async import RollbarAsyncError, try_report from rollbar.lib.session import set_current_session, reset_current_session log = logging.getLogger(__name__) @integrate(framework_name='asgi') class ReporterMiddleware(IntegrationBase): def __init__(self, app: ASGIApp) -> None: super().__init__() self.app = app async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: if scope['type'] == 'http': set_current_session(self._format_headers(scope['headers'])) try: await self.app(scope, receive, send) except Exception: if scope['type'] == 'http': exc_info = sys.exc_info() try: await try_report(exc_info) except RollbarAsyncError: log.warning( 'Failed to report asynchronously. Trying to report synchronously.' ) rollbar.report_exc_info(exc_info) raise finally: if scope['type'] == 'http': reset_current_session() @staticmethod def _format_headers(headers: Iterable[tuple[bytes, bytes]]) -> dict[str, str]: """ Convert list of header tuples to a dictionary with string keys and values. Headers are expected to be in the format: [(b'header-name', b'header-value'), ...] """ return {key.decode('latin-1'): value.decode('latin-1') for key, value in headers} ================================================ FILE: rollbar/contrib/asgi/types.py ================================================ # Copyright © 2018, [Encode OSS Ltd](https://www.encode.io/). # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # * Neither the name of the copyright holder nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. import typing Scope = typing.MutableMapping[str, typing.Any] Message = typing.MutableMapping[str, typing.Any] Receive = typing.Callable[[], typing.Awaitable[Message]] Send = typing.Callable[[Message], typing.Awaitable[None]] ASGIApp = typing.Callable[[Scope, Receive, Send], typing.Awaitable[None]] ================================================ FILE: rollbar/contrib/bottle/__init__.py ================================================ import bottle, rollbar, sys class RollbarBottleReporter(object): ''' A Bottle plugin that reports errors to Rollbar All args and kwargs are passed to `rollbar.init` ''' name = 'rollbar-bottle-reporter' api = 2 def __init__(self, *args, **kwargs): if 'exception_level_filters' in kwargs: kwargs['exception_level_filters'].append((bottle.BaseResponse, 'ignored')) else: kwargs['exception_level_filters'] = [(bottle.BaseResponse, 'ignored')] rollbar.init(*args, **kwargs) def hook(request, data): data['framework'] = 'bottle' if request: route = request['bottle.route'] data['context'] = route.name or route.rule rollbar.BASE_DATA_HOOK = hook def __call__(self, callback): def wrapper(*args, **kwargs): try: return callback(*args, **kwargs) except Exception as e: rollbar.report_exc_info(sys.exc_info(), request=bottle.request) raise return wrapper ================================================ FILE: rollbar/contrib/django/__init__.py ================================================ ================================================ FILE: rollbar/contrib/django/context_processors.py ================================================ """ django-rollbar context processor To install, add the following in your settings.py: 1. add 'rollbar.contrib.django.context_processors.rollbar_settings' to TEMPLATE_CONTEXT_PROCESSORS 2. add a section like this: ROLLBAR = { 'client_access_token': 'tokengoeshere', } 3. you can now access your rollbar settings as rollbar_settings from within your django templates See README.rst for full installation and configuration instructions. """ from django.conf import settings def rollbar_settings(request): """Grabs the rollbar settings to make them available to templates.""" if not hasattr(settings, 'ROLLBAR'): return {} return {'rollbar_settings': settings.ROLLBAR} ================================================ FILE: rollbar/contrib/django/middleware.py ================================================ r""" django-rollbar middleware 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(r'/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', } See README.rst for full installation and configuration instructions. """ import logging import sys import rollbar from django.core.exceptions import MiddlewareNotUsed from django.conf import settings from django.http import Http404 from rollbar import set_current_session from rollbar.lib.session import reset_current_session 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__) DEFAULTS = { 'web_base': 'https://rollbar.com', 'enabled': True, 'patch_debugview': True, 'exception_level_filters': [ (Http404, 'warning') ] } def _patch_debugview(rollbar_web_base): try: 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 %}