diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d3588da1..0999de84 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,47 +14,53 @@ jobs: matrix: python-version: [2.7, 3.4, 3.5, 3.6, 3.7, 3.8] framework: - - FLASK_VERSION=0.10.1 Werkzeug\>=0.7,\<1.0 - - FLASK_VERSION=0.11.1 Werkzeug\>=0.7,\<1.0 - - FLASK_VERSION=0.12.4 Werkzeug\>=0.7,\<1.0 - - FLASK_VERSION=1.0.2 - - TWISTED_VERSION=15.5.0 treq==15.1.0 zope.interface==4.1.3 - - TWISTED_VERSION=16.1.0 treq==16.12.0 zope.interface==4.1.3 - - TWISTED_VERSION=16.2.0 treq==16.12.0 zope.interface==4.1.3 - - TWISTED_VERSION=16.3.0 treq==16.12.0 zope.interface==4.2.0 - - TWISTED_VERSION=16.4.0 treq==16.12.0 zope.interface==4.5.0 - - TWISTED_VERSION=16.5.0 treq==16.12.0 zope.interface==4.5.0 - - TWISTED_VERSION=16.6.0 treq==16.12.0 zope.interface==4.5.0 - - TWISTED_VERSION=17.1.0 treq==16.12.0 zope.interface==4.5.0 - - DJANGO_VERSION=1.11.20 - - DJANGO_VERSION=2.0.13 - - DJANGO_VERSION=2.1.7 - - DJANGO_VERSION=2.1.15 - - PYRAMID_VERSION=1.9.2 - - PYRAMID_VERSION=1.10.4 - - STARLETTE_VERSION=0.12.12 httpx==0.18.1 python-multipart==0.0.5 - - STARLETTE_VERSION=0.12.13 httpx==0.18.1 python-multipart==0.0.5 - - STARLETTE_VERSION=0.14.2 httpx==0.18.1 python-multipart==0.0.5 - - FASTAPI_VERSION=0.40.0 httpx==0.18.1 python-multipart==0.0.5 - - FASTAPI_VERSION=0.50.0 httpx==0.18.1 python-multipart==0.0.5 - - FASTAPI_VERSION=0.63.0 httpx==0.18.1 python-multipart==0.0.5 + - FLASK_VERSION=0.10.1 Werkzeug\>=0.7,\<1.0 chardet==3.0.4 idna==2.7 mock==3.0.5 + - FLASK_VERSION=0.11.1 Werkzeug\>=0.7,\<1.0 chardet==3.0.4 idna==2.7 mock==3.0.5 + - FLASK_VERSION=0.12.4 Werkzeug\>=0.7,\<1.0 chardet==3.0.4 idna==2.7 mock==3.0.5 + - FLASK_VERSION=1.0.2 chardet==3.0.4 idna==2.7 mock==3.0.5 + - TWISTED_VERSION=15.5.0 treq==15.1.0 zope.interface==4.1.3 mock==2.0.0 + - TWISTED_VERSION=16.1.0 treq==16.12.0 zope.interface==4.1.3 mock==2.0.0 + - TWISTED_VERSION=16.2.0 treq==16.12.0 zope.interface==4.1.3 mock==2.0.0 + - TWISTED_VERSION=16.3.0 treq==16.12.0 zope.interface==4.2.0 mock==2.0.0 + - TWISTED_VERSION=16.4.0 treq==16.12.0 zope.interface==4.5.0 mock==2.0.0 + - TWISTED_VERSION=16.5.0 treq==16.12.0 zope.interface==4.5.0 mock==2.0.0 + - TWISTED_VERSION=16.6.0 treq==16.12.0 zope.interface==4.5.0 mock==2.0.0 + - TWISTED_VERSION=17.1.0 treq==16.12.0 zope.interface==4.5.0 mock==2.0.0 + - DJANGO_VERSION=1.11.20 chardet==3.0.4 idna==2.7 mock==3.0.5 + - DJANGO_VERSION=2.0.13 mock==2.0.0 + - DJANGO_VERSION=2.1.7 mock==2.0.0 + - DJANGO_VERSION=2.1.15 mock==2.0.0 + - PYRAMID_VERSION=1.9.2 chardet==3.0.4 idna==2.7 mock==3.0.5 + - PYRAMID_VERSION=1.10.4 chardet==3.0.4 idna==2.7 mock==3.0.5 + - STARLETTE_VERSION=0.12.12 httpx==0.18.1 python-multipart==0.0.5 mock==3.0.5 + - STARLETTE_VERSION=0.12.13 httpx==0.18.1 python-multipart==0.0.5 mock==3.0.5 + - STARLETTE_VERSION=0.14.2 httpx==0.18.1 python-multipart==0.0.5 mock==3.0.5 + - FASTAPI_VERSION=0.40.0 httpx==0.18.1 python-multipart==0.0.5 mock==3.0.5 + - FASTAPI_VERSION=0.50.0 httpx==0.18.1 python-multipart==0.0.5 mock==3.0.5 + - FASTAPI_VERSION=0.63.0 httpx==0.18.1 python-multipart==0.0.5 mock==3.0.5 exclude: - python-version: 2.7 - framework: DJANGO_VERSION=2.0.13 + framework: DJANGO_VERSION=2.0.13 mock==2.0.0 - python-version: 2.7 - framework: DJANGO_VERSION=2.1.7 + framework: DJANGO_VERSION=2.1.7 mock==2.0.0 - python-version: 2.7 - framework: DJANGO_VERSION=2.1.15 + framework: DJANGO_VERSION=2.1.15 mock==2.0.0 - python-version: 3.4 - framework: DJANGO_VERSION=2.1.7 + framework: DJANGO_VERSION=2.0.13 mock==2.0.0 - python-version: 3.4 - framework: DJANGO_VERSION=2.1.15 + framework: DJANGO_VERSION=2.1.7 mock==2.0.0 + - python-version: 3.4 + framework: DJANGO_VERSION=2.1.15 mock==2.0.0 + - python-version: 3.5 + framework: DJANGO_VERSION=2.1.15 mock==2.0.0 + - python-version: 3.5 + framework: DJANGO_VERSION=2.0.13 mock==2.0.0 - python-version: 3.5 - framework: DJANGO_VERSION=2.1.15 + framework: DJANGO_VERSION=2.1.7 mock==2.0.0 - python-version: 3.6 - framework: DJANGO_VERSION=2.1.15 + framework: DJANGO_VERSION=2.1.15 mock==2.0.0 - python-version: 3.7 - framework: DJANGO_VERSION=2.1.15 + framework: DJANGO_VERSION=2.1.15 mock==2.0.0 # twisted/treq setup.py allows: # Twisted < 18.7.0 on python < 3.7 @@ -95,83 +101,89 @@ jobs: framework: TWISTED_VERSION=17.1.0 treq==20.4.1 zope.interface==4.3.0 - python-version: 2.7 - framework: STARLETTE_VERSION=0.12.12 httpx==0.18.1 python-multipart==0.0.5 + framework: STARLETTE_VERSION=0.12.12 httpx==0.18.1 python-multipart==0.0.5 mock==3.0.5 - python-version: 2.7 - framework: STARLETTE_VERSION=0.12.13 httpx==0.18.1 python-multipart==0.0.5 + framework: STARLETTE_VERSION=0.12.13 httpx==0.18.1 python-multipart==0.0.5 mock==3.0.5 - python-version: 2.7 - framework: STARLETTE_VERSION=0.14.2 httpx==0.18.1 python-multipart==0.0.5 + framework: STARLETTE_VERSION=0.14.2 httpx==0.18.1 python-multipart==0.0.5 mock==3.0.5 - python-version: 3.4 - framework: STARLETTE_VERSION=0.12.12 httpx==0.18.1 python-multipart==0.0.5 + framework: STARLETTE_VERSION=0.12.12 httpx==0.18.1 python-multipart==0.0.5 mock==3.0.5 - python-version: 3.4 - framework: STARLETTE_VERSION=0.12.13 httpx==0.18.1 python-multipart==0.0.5 + framework: STARLETTE_VERSION=0.12.13 httpx==0.18.1 python-multipart==0.0.5 mock==3.0.5 - python-version: 3.4 - framework: STARLETTE_VERSION=0.14.2 httpx==0.18.1 python-multipart==0.0.5 + framework: STARLETTE_VERSION=0.14.2 httpx==0.18.1 python-multipart==0.0.5 mock==3.0.5 - python-version: 3.5 - framework: STARLETTE_VERSION=0.12.12 httpx==0.18.1 python-multipart==0.0.5 + framework: STARLETTE_VERSION=0.12.12 httpx==0.18.1 python-multipart==0.0.5 mock==3.0.5 - python-version: 3.5 - framework: STARLETTE_VERSION=0.12.13 httpx==0.18.1 python-multipart==0.0.5 + framework: STARLETTE_VERSION=0.12.13 httpx==0.18.1 python-multipart==0.0.5 mock==3.0.5 - python-version: 3.5 - framework: STARLETTE_VERSION=0.14.2 httpx==0.18.1 python-multipart==0.0.5 + framework: STARLETTE_VERSION=0.14.2 httpx==0.18.1 python-multipart==0.0.5 mock==3.0.5 - python-version: 2.7 - framework: FASTAPI_VERSION=0.40.0 httpx==0.18.1 python-multipart==0.0.5 + framework: FASTAPI_VERSION=0.40.0 httpx==0.18.1 python-multipart==0.0.5 mock==3.0.5 - python-version: 2.7 - framework: FASTAPI_VERSION=0.50.0 httpx==0.18.1 python-multipart==0.0.5 + framework: FASTAPI_VERSION=0.50.0 httpx==0.18.1 python-multipart==0.0.5 mock==3.0.5 - python-version: 2.7 - framework: FASTAPI_VERSION=0.63.0 httpx==0.18.1 python-multipart==0.0.5 + framework: FASTAPI_VERSION=0.63.0 httpx==0.18.1 python-multipart==0.0.5 mock==3.0.5 - python-version: 3.4 - framework: FASTAPI_VERSION=0.40.0 httpx==0.18.1 python-multipart==0.0.5 + framework: FASTAPI_VERSION=0.40.0 httpx==0.18.1 python-multipart==0.0.5 mock==3.0.5 - python-version: 3.4 - framework: FASTAPI_VERSION=0.50.0 httpx==0.18.1 python-multipart==0.0.5 + framework: FASTAPI_VERSION=0.50.0 httpx==0.18.1 python-multipart==0.0.5 mock==3.0.5 - python-version: 3.4 - framework: FASTAPI_VERSION=0.63.0 httpx==0.18.1 python-multipart==0.0.5 + framework: FASTAPI_VERSION=0.63.0 httpx==0.18.1 python-multipart==0.0.5 mock==3.0.5 - python-version: 3.5 - framework: FASTAPI_VERSION=0.40.0 httpx==0.18.1 python-multipart==0.0.5 + framework: FASTAPI_VERSION=0.40.0 httpx==0.18.1 python-multipart==0.0.5 mock==3.0.5 - python-version: 3.5 - framework: FASTAPI_VERSION=0.50.0 httpx==0.18.1 python-multipart==0.0.5 + framework: FASTAPI_VERSION=0.50.0 httpx==0.18.1 python-multipart==0.0.5 mock==3.0.5 - python-version: 3.5 - framework: FASTAPI_VERSION=0.63.0 httpx==0.18.1 python-multipart==0.0.5 + framework: FASTAPI_VERSION=0.63.0 httpx==0.18.1 python-multipart==0.0.5 mock==3.0.5 include: - python-version: 2.7 framework: FLASK_VERSION=0.9 - python-version: 3.3 - framework: FLASK_VERSION=0.10.1 + framework: FLASK_VERSION=0.10.1 mock==2.0.0 - python-version: 3.3 - framework: FLASK_VERSION=0.11.1 + framework: FLASK_VERSION=0.11.1 mock==2.0.0 - python-version: 3.3 - framework: FLASK_VERSION=0.12.4 + framework: FLASK_VERSION=0.12.4 mock==2.0.0 - python-version: 3.3 - framework: FLASK_VERSION=1.0.2 + framework: FLASK_VERSION=1.0.2 mock==2.0.0 - python-version: 3.3 - framework: DJANGO_VERSION=1.6.11 + framework: DJANGO_VERSION=1.6.11 mock==2.0.0 - python-version: 3.3 - framework: DJANGO_VERSION=1.8.19 + framework: DJANGO_VERSION=1.8.19 mock==2.0.0 - python-version: 3.4 - framework: DJANGO_VERSION=1.7.11 + framework: DJANGO_VERSION=1.7.11 chardet==3.0.4 idna==2.7 mock==3.0.5 - python-version: 3.4 - framework: DJANGO_VERSION=1.8.19 + framework: DJANGO_VERSION=1.8.19 chardet==3.0.4 idna==2.7 mock==3.0.5 - python-version: 3.4 - framework: DJANGO_VERSION=1.9.13 + framework: DJANGO_VERSION=1.9.13 chardet==3.0.4 idna==2.7 mock==3.0.5 - python-version: 3.4 - framework: DJANGO_VERSION=1.10.8 + framework: DJANGO_VERSION=1.10.8 chardet==3.0.4 idna==2.7 mock==3.0.5 + - python-version: 3.4 + framework: DJANGO_VERSION=2.0.13 chardet==3.0.4 idna==2.7 mock==3.0.5 + - python-version: 3.5 + framework: DJANGO_VERSION=1.8.19 mock==3.0.5 + - python-version: 3.5 + framework: DJANGO_VERSION=1.9.13 mock==3.0.5 - python-version: 3.5 - framework: DJANGO_VERSION=1.8.19 + framework: DJANGO_VERSION=1.10.8 mock==3.0.5 - python-version: 3.5 - framework: DJANGO_VERSION=1.9.13 + framework: DJANGO_VERSION=2.0.13 mock==3.0.5 - python-version: 3.5 - framework: DJANGO_VERSION=1.10.8 + framework: DJANGO_VERSION=2.1.7 mock==3.0.5 - python-version: 3.7 - framework: TWISTED_VERSION=18.9.0 treq==20.4.1 zope.interface==4.5.0 + framework: TWISTED_VERSION=18.9.0 treq==20.4.1 zope.interface==4.5.0 mock==3.0.5 - python-version: 3.7 - framework: TWISTED_VERSION=19.10.0 treq==20.4.1 zope.interface==4.6.0 + framework: TWISTED_VERSION=19.10.0 treq==20.4.1 zope.interface==4.6.0 mock==3.0.5 - python-version: 3.7 - framework: TWISTED_VERSION=20.3.0 treq==20.4.1 zope.interface==4.7.0 + framework: TWISTED_VERSION=20.3.0 treq==20.4.1 zope.interface==4.7.0 mock==3.0.5 - python-version: 3.8 - framework: TWISTED_VERSION=18.9.0 treq==20.4.1 zope.interface==4.5.0 + framework: TWISTED_VERSION=18.9.0 treq==20.4.1 zope.interface==4.5.0 mock==3.0.5 - python-version: 3.8 - framework: TWISTED_VERSION=19.10.0 treq==20.4.1 zope.interface==4.6.0 + framework: TWISTED_VERSION=19.10.0 treq==20.4.1 zope.interface==4.6.0 mock==3.0.5 - python-version: 3.8 - framework: TWISTED_VERSION=20.3.0 treq==20.4.1 zope.interface==4.7.0 + framework: TWISTED_VERSION=20.3.0 treq==20.4.1 zope.interface==4.7.0 mock==3.0.5 steps: - uses: actions/checkout@v2 with: diff --git a/rollbar/__init__.py b/rollbar/__init__.py index 5101ea35..e07bbafd 100644 --- a/rollbar/__init__.py +++ b/rollbar/__init__.py @@ -10,7 +10,6 @@ import socket import sys import threading -import time import traceback import types import uuid @@ -20,7 +19,8 @@ import requests import six -from rollbar.lib import events, filters, dict_merge, parse_qs, text, transport, urljoin, iteritems, defaultJSONEncode +from collections import deque +from rollbar.lib import events, filters, dict_merge, parse_qs, telemetry, text, transport, urljoin, iteritems, defaultJSONEncode, get_current_timestamp __version__ = '0.16.1' @@ -322,8 +322,12 @@ def _get_fastapi_request(): 'request_pool_connections': None, 'request_pool_maxsize': None, 'request_max_retries': None, + 'telemetry_queue_size': 50, } + +TELEMETRY_QUEUE = deque([], SETTINGS['telemetry_queue_size']) + _CURRENT_LAMBDA_CONTEXT = None _LAST_RESPONSE_STATUS = None @@ -381,6 +385,18 @@ def init(access_token, environment='production', scrub_fields=None, url_fields=N if SETTINGS.get('allow_logging_basic_config'): logging.basicConfig() + queue_size = SETTINGS.get('telemetry_queue_size') + if queue_size: + TELEMETRY_QUEUE = deque([], queue_size) + + if SETTINGS.get('log_telemetry'): + formatter = SETTINGS.get('log_telemetry_formatter') + telemetry.enable_log_telemetry(formatter) + if SETTINGS.get('network_telemetry'): + enable_req_headers = SETTINGS.get('enable_req_headers') + enable_resp_headers = SETTINGS.get('enable_resp_headers') + telemetry.enable_network_telemetry(enable_req_headers, enable_resp_headers) + if SETTINGS.get('handler') == 'agent': agent_log = _create_agent_log() @@ -777,6 +793,7 @@ def _report_exc_info(exc_info, request, extra_data, payload_data, level=None): _add_request_data(data, request) _add_person_data(data, request) _add_lambda_context_data(data) + _add_telemetry(data) data['server'] = _build_server_data() if payload_data: @@ -857,6 +874,7 @@ def _report_message(message, level, request, extra_data, payload_data): _add_request_data(data, request) _add_person_data(data, request) _add_lambda_context_data(data) + _add_telemetry(data) data['server'] = _build_server_data() if payload_data: @@ -887,7 +905,7 @@ def _check_config(): def _build_base_data(request, level='error'): data = { - 'timestamp': int(time.time()), + 'timestamp': get_current_timestamp(), 'environment': SETTINGS['environment'], 'level': level, 'language': 'python %s' % '.'.join(str(x) for x in sys.version_info[:3]), @@ -1119,6 +1137,12 @@ def _add_request_data(data, request): data['request'] = request_data +def _add_telemetry(data): + telemetry_data = list(TELEMETRY_QUEUE) + if telemetry_data: + data['body']['telemetry'] = telemetry_data + + def _check_add_locals(frame, frame_num, total_frames): """ Returns True if we should record local variables for the given frame. diff --git a/rollbar/lib/__init__.py b/rollbar/lib/__init__.py index 78a32d87..8eae5e50 100644 --- a/rollbar/lib/__init__.py +++ b/rollbar/lib/__init__.py @@ -4,7 +4,7 @@ import os import sys from array import array -import json +import time try: # Python 3 @@ -229,3 +229,7 @@ def defaultJSONEncode(o): o._setup() return o._wrapped return repr(o) + " is not JSON serializable" + + +def get_current_timestamp(): + return int(time.time()) diff --git a/rollbar/lib/telemetry.py b/rollbar/lib/telemetry.py new file mode 100644 index 00000000..9cfff91f --- /dev/null +++ b/rollbar/lib/telemetry.py @@ -0,0 +1,113 @@ +import logging +import requests +import copy + +import rollbar +from rollbar.lib import transforms +from rollbar.lib.transforms.scrub import ScrubTransform + +try: + # 3.x + import urllib.request as ulib +except ImportError: + # 2.x + import urllib as ulib + + + +except ImportError: + # 2.x + print('please install httpx for python3') + + +class TelemetryLogHandler(logging.Handler): + def __init__(self, formatter=None): + super(TelemetryLogHandler, self).__init__() + self.formatter = formatter + + def emit(self, record): + self.setFormatter(self.formatter) + msg = {'message': self.format(record)} + data = { + 'body': msg, + 'source': 'client', + 'timestamp_ms': rollbar.get_current_timestamp(), + 'type': 'log', + 'level': record.levelname, + } + + rollbar.TELEMETRY_QUEUE.append(data) + + +def enable_log_telemetry(log_formatter=None): + logging.getLogger().addHandler(TelemetryLogHandler(log_formatter)) + + +def request(request_function, enable_req_headers, enable_response_headers, request_type): + scrubber = ScrubTransform(suffixes=rollbar.SETTINGS['scrub_fields'], redact_char='*', randomize_len=False) + + def telemetry(*args, **kwargs): + + data = {'level': 'info'} + data_body = {'status_code': None} + try: + response = request_function(*args, **kwargs) + except: # noqa: E722 + response = None + data_body['status_code'] = 0 + data['level'] = 'critical' + + if response is not None: + if request_type in ['requests', 'httpx']: + data_body['status_code'] = response.status_code + elif request_type == 'urllib': + data_body['status_code'] = response.code + + if data_body['status_code'] >= 500: + data['level'] = 'critical' + elif data_body['status_code'] >= 400: + data['level'] = 'error' + if enable_response_headers: + response_headers = copy.deepcopy(response.headers) + if not response_headers: + response_headers = {} + data_body['response'] = {'headers': transforms.transform(response_headers, scrubber)} + if enable_req_headers: + data_body['request_headers'] = transforms.transform(copy.deepcopy(kwargs.get('headers', {})), scrubber) + data_body['url'] = args[0] + if request_type in ['requests', 'httpx']: + data_body['method'] = request_function.__name__.upper() + if request_type == 'urllib': + data_body['method'] = kwargs.get('method', 'GET') + data_body['subtype'] = 'http' + data['body'] = data_body + + data['source'] = 'client' + data['timestamp_ms'] = rollbar.get_current_timestamp() + data['type'] = 'network' + rollbar.TELEMETRY_QUEUE.append(data) + + return response + + return telemetry + + +def enable_network_telemetry(enable_req_headers, enable_resp_headers): + requests.get = request(requests.get, enable_req_headers, enable_resp_headers, 'requests') + requests.post = request(requests.post, enable_req_headers, enable_resp_headers, 'requests') + requests.put = request(requests.put, enable_req_headers, enable_resp_headers, 'requests') + requests.patch = request(requests.patch, enable_req_headers, enable_resp_headers, 'requests') + requests.delete = request(requests.delete, enable_req_headers, enable_resp_headers, 'requests') + + ulib.urlopen = request(ulib.urlopen, enable_req_headers, enable_resp_headers, 'urllib') + + try: + # 3.x + import httpx + httpx.get = request(httpx.get, enable_req_headers, enable_resp_headers, 'httpx') + httpx.post = request(httpx.post, enable_req_headers, enable_resp_headers, 'httpx') + httpx.put = request(httpx.put, enable_req_headers, enable_resp_headers, 'httpx') + httpx.patch = request(httpx.patch, enable_req_headers, enable_resp_headers, 'httpx') + httpx.delete = request(httpx.delete, enable_req_headers, enable_resp_headers, 'httpx') + except ImportError: + pass diff --git a/rollbar/test/test_telemetry.py b/rollbar/test/test_telemetry.py new file mode 100644 index 00000000..ae30e803 --- /dev/null +++ b/rollbar/test/test_telemetry.py @@ -0,0 +1,86 @@ +from rollbar.lib import telemetry +from rollbar.test import BaseTest +import logging +import mock +import requests +import rollbar + + +try: + # 3.x + import urllib.request as ulib +except ImportError: + # 2.x + import urllib as ulib + + +class RollbarTelemetryTest(BaseTest): + @classmethod + def setUpClass(self): + formatter = logging.Formatter('%(name)s :: %(levelname)s :: %(message)s') + telemetry.enable_log_telemetry(formatter) + telemetry.enable_network_telemetry(False, False) + + @mock.patch('rollbar.get_current_timestamp') + def test_telemetry_log(self, timestamp): + timestamp.return_value = 1000000 + logging.warning("test loggin") + items = list(rollbar.TELEMETRY_QUEUE) + self.assertEqual(1, len(items)) + + result = { + 'body': {'message': 'root :: WARNING :: test loggin'}, + 'source': 'client', + 'level': 'WARNING', + 'type': 'log', + 'timestamp_ms': 1000000, + } + + self.assertEqual(result, items[0]) + rollbar.TELEMETRY_QUEUE.clear() + + @mock.patch('rollbar.get_current_timestamp') + def test_telemetry_request(self, timestamp): + timestamp.return_value = 1000000 + + requests.get("http://example.com") + items = list(rollbar.TELEMETRY_QUEUE) + self.assertEqual(1, len(items)) + + result = { + 'body': { + 'url': 'http://example.com', + 'status_code': 200, + 'method': 'GET', + 'subtype': 'http', + }, + 'source': 'client', + 'timestamp_ms': 1000000, + 'type': 'network', + 'level': 'info', + } + self.assertEqual(result, items[0]) + rollbar.TELEMETRY_QUEUE.clear() + + @mock.patch('rollbar.get_current_timestamp') + def test_telemetry_urllib_request(self, timestamp): + timestamp.return_value = 1000000 + + ulib.urlopen("http://example.com") + items = list(rollbar.TELEMETRY_QUEUE) + self.assertEqual(1, len(items)) + + result = { + 'body': { + 'url': 'http://example.com', + 'status_code': 200, + 'method': 'GET', + 'subtype': 'http', + }, + 'source': 'client', + 'timestamp_ms': 1000000, + 'type': 'network', + 'level': 'info', + } + self.assertEqual(result, items[0]) + rollbar.TELEMETRY_QUEUE.clear()