class RequestStoreTest(TestCase): def setUp(self): self.mock_get_frames = Mock() frames = [{'function': 'something_expensive', 'abs_path': '/var/parent-opbeat/opbeat_python/tests/contrib/django/testapp/views.py', 'lineno': 52, 'module': 'tests.contrib.django.testapp.views', 'filename': 'tests/contrib/django/testapp/views.py'}, {'function': '_resolve_lookup', 'abs_path': '/home/ron/.virtualenvs/opbeat_python/local/lib/python2.7/site-packages/django/template/base.py', 'lineno': 789, 'module': 'django.template.base', 'filename': 'django/template/base.py'}, {'function': 'resolve', 'abs_path': '/home/ron/.virtualenvs/opbeat_python/local/lib/python2.7/site-packages/django/template/base.py', 'lineno': 735, 'module': 'django.template.base', 'filename': 'django/template/base.py'}, {'function': 'resolve', 'abs_path': '/home/ron/.virtualenvs/opbeat_python/local/lib/python2.7/site-packages/django/template/base.py', 'lineno': 585, 'module': 'django.template.base', 'filename': 'django/template/base.py'}, {'lineno': 4, 'filename': u'/var/parent-opbeat/opbeat_python/tests/contrib/django/testapp/templates/list_fish.html'}, {'function': 'render', 'abs_path': '/home/ron/.virtualenvs/opbeat_python/local/lib/python2.7/site-packages/django/template/defaulttags.py', 'lineno': 4, 'module': 'django.template.defaulttags', 'filename': 'django/template/defaulttags.py'}, {'function': 'render_node', 'abs_path': '/home/ron/.virtualenvs/opbeat_python/local/lib/python2.7/site-packages/django/template/debug.py', 'lineno': 78, 'module': 'django.template.debug', 'filename': 'django/template/debug.py'}, {'function': 'render', 'abs_path': '/home/ron/.virtualenvs/opbeat_python/local/lib/python2.7/site-packages/django/template/base.py', 'lineno': 840, 'module': 'django.template.base', 'filename': 'django/template/base.py'}, {'function': 'instrumented_test_render', 'abs_path': '/home/ron/.virtualenvs/opbeat_python/local/lib/python2.7/site-packages/django/test/utils.py', 'lineno': 85, 'module': 'django.test.utils', 'filename': 'django/test/utils.py'}, {'function': 'render', 'abs_path': '/home/ron/.virtualenvs/opbeat_python/local/lib/python2.7/site-packages/django/template/base.py', 'lineno': 140, 'module': 'django.template.base', 'filename': 'django/template/base.py'}, {'function': 'rendered_content', 'abs_path': '/home/ron/.virtualenvs/opbeat_python/local/lib/python2.7/site-packages/django/template/response.py', 'lineno': 82, 'module': 'django.template.response', 'filename': 'django/template/response.py'}, {'function': 'render', 'abs_path': '/home/ron/.virtualenvs/opbeat_python/local/lib/python2.7/site-packages/django/template/response.py', 'lineno': 105, 'module': 'django.template.response', 'filename': 'django/template/response.py'}, {'function': 'get_response', 'abs_path': '/home/ron/.virtualenvs/opbeat_python/local/lib/python2.7/site-packages/django/core/handlers/base.py', 'lineno': 137, 'module': 'django.core.handlers.base', 'filename': 'django/core/handlers/base.py'}, {'function': '__call__', 'abs_path': '/home/ron/.virtualenvs/opbeat_python/local/lib/python2.7/site-packages/django/test/client.py', 'lineno': 109, 'module': 'django.test.client', 'filename': 'django/test/client.py'}, {'function': 'request', 'abs_path': '/home/ron/.virtualenvs/opbeat_python/local/lib/python2.7/site-packages/django/test/client.py', 'lineno': 426, 'module': 'django.test.client', 'filename': 'django/test/client.py'}, {'function': 'get', 'abs_path': '/home/ron/.virtualenvs/opbeat_python/local/lib/python2.7/site-packages/django/test/client.py', 'lineno': 280, 'module': 'django.test.client', 'filename': 'django/test/client.py'}, {'function': 'get', 'abs_path': '/home/ron/.virtualenvs/opbeat_python/local/lib/python2.7/site-packages/django/test/client.py', 'lineno': 473, 'module': 'django.test.client', 'filename': 'django/test/client.py'}, {'function': 'test_template_name_as_view', 'abs_path': '/var/parent-opbeat/opbeat_python/tests/contrib/django/django_tests.py', 'lineno': 710, 'module': 'tests.contrib.django.django_tests', 'filename': 'tests/contrib/django/django_tests.py'}] self.mock_get_frames.return_value = frames self.requests_store = RequestsStore(self.mock_get_frames, 99999) def test_lru_get_frames_cache(self): self.requests_store.transaction_start(None, "transaction.test") for i in range(10): with trace("bleh", "custom"): time.sleep(0.01) self.assertEqual(self.mock_get_frames.call_count, 1) def test_leaf_tracing(self): self.requests_store.transaction_start(None, "transaction.test") with trace("root", "custom"): with trace("child1-leaf", "custom", leaf=True): # These two traces should not show up with trace("ignored-child1", "custom", leaf=True): time.sleep(0.01) with trace("ignored-child2", "custom", leaf=False): time.sleep(0.01) self.requests_store.transaction_end(None, "transaction") transactions, traces = self.requests_store.get_all() self.assertEqual(len(traces), 3) signatures = ['transaction', 'root', 'child1-leaf'] self.assertEqual(set([t['signature'] for t in traces]), set(signatures))
class RequestStoreTest(TestCase): def setUp(self): self.mock_get_frames = Mock() frames = [{ 'function': 'something_expensive', 'abs_path': '/var/parent-opbeat/opbeat_python/tests/contrib/django/testapp/views.py', 'lineno': 52, 'module': 'tests.contrib.django.testapp.views', 'filename': 'tests/contrib/django/testapp/views.py' }, { 'function': '_resolve_lookup', 'abs_path': '/home/ron/.virtualenvs/opbeat_python/local/lib/python2.7/site-packages/django/template/base.py', 'lineno': 789, 'module': 'django.template.base', 'filename': 'django/template/base.py' }, { 'function': 'resolve', 'abs_path': '/home/ron/.virtualenvs/opbeat_python/local/lib/python2.7/site-packages/django/template/base.py', 'lineno': 735, 'module': 'django.template.base', 'filename': 'django/template/base.py' }, { 'function': 'resolve', 'abs_path': '/home/ron/.virtualenvs/opbeat_python/local/lib/python2.7/site-packages/django/template/base.py', 'lineno': 585, 'module': 'django.template.base', 'filename': 'django/template/base.py' }, { 'lineno': 4, 'filename': u'/var/parent-opbeat/opbeat_python/tests/contrib/django/testapp/templates/list_fish.html' }, { 'function': 'render', 'abs_path': '/home/ron/.virtualenvs/opbeat_python/local/lib/python2.7/site-packages/django/template/defaulttags.py', 'lineno': 4, 'module': 'django.template.defaulttags', 'filename': 'django/template/defaulttags.py' }, { 'function': 'render_node', 'abs_path': '/home/ron/.virtualenvs/opbeat_python/local/lib/python2.7/site-packages/django/template/debug.py', 'lineno': 78, 'module': 'django.template.debug', 'filename': 'django/template/debug.py' }, { 'function': 'render', 'abs_path': '/home/ron/.virtualenvs/opbeat_python/local/lib/python2.7/site-packages/django/template/base.py', 'lineno': 840, 'module': 'django.template.base', 'filename': 'django/template/base.py' }, { 'function': 'instrumented_test_render', 'abs_path': '/home/ron/.virtualenvs/opbeat_python/local/lib/python2.7/site-packages/django/test/utils.py', 'lineno': 85, 'module': 'django.test.utils', 'filename': 'django/test/utils.py' }, { 'function': 'render', 'abs_path': '/home/ron/.virtualenvs/opbeat_python/local/lib/python2.7/site-packages/django/template/base.py', 'lineno': 140, 'module': 'django.template.base', 'filename': 'django/template/base.py' }, { 'function': 'rendered_content', 'abs_path': '/home/ron/.virtualenvs/opbeat_python/local/lib/python2.7/site-packages/django/template/response.py', 'lineno': 82, 'module': 'django.template.response', 'filename': 'django/template/response.py' }, { 'function': 'render', 'abs_path': '/home/ron/.virtualenvs/opbeat_python/local/lib/python2.7/site-packages/django/template/response.py', 'lineno': 105, 'module': 'django.template.response', 'filename': 'django/template/response.py' }, { 'function': 'get_response', 'abs_path': '/home/ron/.virtualenvs/opbeat_python/local/lib/python2.7/site-packages/django/core/handlers/base.py', 'lineno': 137, 'module': 'django.core.handlers.base', 'filename': 'django/core/handlers/base.py' }, { 'function': '__call__', 'abs_path': '/home/ron/.virtualenvs/opbeat_python/local/lib/python2.7/site-packages/django/test/client.py', 'lineno': 109, 'module': 'django.test.client', 'filename': 'django/test/client.py' }, { 'function': 'request', 'abs_path': '/home/ron/.virtualenvs/opbeat_python/local/lib/python2.7/site-packages/django/test/client.py', 'lineno': 426, 'module': 'django.test.client', 'filename': 'django/test/client.py' }, { 'function': 'get', 'abs_path': '/home/ron/.virtualenvs/opbeat_python/local/lib/python2.7/site-packages/django/test/client.py', 'lineno': 280, 'module': 'django.test.client', 'filename': 'django/test/client.py' }, { 'function': 'get', 'abs_path': '/home/ron/.virtualenvs/opbeat_python/local/lib/python2.7/site-packages/django/test/client.py', 'lineno': 473, 'module': 'django.test.client', 'filename': 'django/test/client.py' }, { 'function': 'test_template_name_as_view', 'abs_path': '/var/parent-opbeat/opbeat_python/tests/contrib/django/django_tests.py', 'lineno': 710, 'module': 'tests.contrib.django.django_tests', 'filename': 'tests/contrib/django/django_tests.py' }] self.mock_get_frames.return_value = frames self.requests_store = RequestsStore(self.mock_get_frames, 99999) def test_lru_get_frames_cache(self): self.requests_store.transaction_start() for i in range(10): with self.requests_store.trace("bleh", "custom"): time.sleep(0.01) self.assertEqual(self.mock_get_frames.call_count, 1) def test_leaf_tracing(self): self.requests_store.transaction_start() with self.requests_store.trace("root", "custom"): with self.requests_store.trace("child1-leaf", "custom", leaf=True): # These two traces should not show up with self.requests_store.trace("ignored-child1", "custom", leaf=True): time.sleep(0.01) with self.requests_store.trace("ignored-child2", "custom", leaf=False): time.sleep(0.01) self.requests_store.transaction_end(None, "transaction") transactions, traces = self.requests_store.get_all() self.assertEqual(len(traces), 3) signatures = ['transaction', 'root', 'child1-leaf'] self.assertEqual(set([t['signature'] for t in traces]), set(signatures))
class Client(object): """ The base opbeat client, which handles communication over the HTTP API to Opbeat servers. Will read default configuration from the environment variable ``OPBEAT_ORGANIZATION_ID``, ``OPBEAT_APP_ID`` and ``OPBEAT_SECRET_TOKEN`` if available. :: >>> from opbeat import Client >>> # Read configuration from environment >>> client = Client() >>> # Configure the client manually >>> client = Client( >>> include_paths=['my.package'], >>> organization_id='org_id', >>> app_id='app_id', >>> secret_token='secret_token', >>> ) >>> # Record an exception >>> try: >>> 1/0 >>> except ZeroDivisionError: >>> ident = client.get_ident(client.capture_exception()) >>> print ("Exception caught; reference is %%s" %% ident) """ logger = logging.getLogger('opbeat') protocol_version = '1.0' def __init__(self, organization_id=None, app_id=None, secret_token=None, transport_class=None, include_paths=None, exclude_paths=None, timeout=None, hostname=None, auto_log_stacks=None, key=None, string_max_length=None, list_max_length=None, processors=None, filter_exception_types=None, servers=None, api_path=None, asynk=None, async_mode=None, traces_send_freq_secs=None, transactions_ignore_patterns=None, framework_version='', **kwargs): # configure loggers first cls = self.__class__ self.logger = logging.getLogger('%s.%s' % (cls.__module__, cls.__name__)) self.error_logger = logging.getLogger('opbeat.errors') self.state = ClientState() if organization_id is None and os.environ.get( 'OPBEAT_ORGANIZATION_ID'): msg = "Configuring opbeat from environment variable 'OPBEAT_ORGANIZATION_ID'" self.logger.info(msg) organization_id = os.environ['OPBEAT_ORGANIZATION_ID'] if app_id is None and os.environ.get('OPBEAT_APP_ID'): msg = "Configuring opbeat from environment variable 'OPBEAT_APP_ID'" self.logger.info(msg) app_id = os.environ['OPBEAT_APP_ID'] if secret_token is None and os.environ.get('OPBEAT_SECRET_TOKEN'): msg = "Configuring opbeat from environment variable 'OPBEAT_SECRET_TOKEN'" self.logger.info(msg) secret_token = os.environ['OPBEAT_SECRET_TOKEN'] self.servers = servers or defaults.SERVERS if asynk is not None and async_mode is None: warnings.warn( 'Usage of "async" argument is deprecated. Use "async_mode"', category=DeprecationWarning, stacklevel=2, ) async_mode = asynk self.async_mode = (async_mode is True or (defaults.ASYNC_MODE and async_mode is not False)) if not transport_class: transport_class = (defaults.ASYNC_TRANSPORT_CLASS if self.async_mode else defaults.SYNC_TRANSPORT_CLASS) self._transport_class = import_string(transport_class) self._transports = {} # servers may be set to a NoneType (for Django) if self.servers and not (organization_id and app_id and secret_token): msg = 'Missing configuration for Opbeat client. Please see documentation.' self.logger.info(msg) self.is_send_disabled = (os.environ.get('OPBEAT_DISABLE_SEND', '').lower() in ('1', 'true')) if self.is_send_disabled: self.logger.info( 'Not sending any data to Opbeat due to OPBEAT_DISABLE_SEND ' 'environment variable') self.include_paths = set(include_paths or defaults.INCLUDE_PATHS) self.exclude_paths = set(exclude_paths or defaults.EXCLUDE_PATHS) self.timeout = int(timeout or defaults.TIMEOUT) self.hostname = six.text_type(hostname or defaults.HOSTNAME) self.auto_log_stacks = bool(auto_log_stacks or defaults.AUTO_LOG_STACKS) self.string_max_length = int(string_max_length or defaults.MAX_LENGTH_STRING) self.list_max_length = int(list_max_length or defaults.MAX_LENGTH_LIST) self.traces_send_freq_secs = (traces_send_freq_secs or defaults.TRACES_SEND_FREQ_SECS) self.organization_id = six.text_type(organization_id) self.app_id = six.text_type(app_id) self.secret_token = six.text_type(secret_token) self.filter_exception_types_dict = {} for exc_to_filter in (filter_exception_types or []): exc_to_filter_type = exc_to_filter.split(".")[-1] exc_to_filter_module = ".".join(exc_to_filter.split(".")[:-1]) self.filter_exception_types_dict[ exc_to_filter_type] = exc_to_filter_module if processors is None: self.processors = defaults.PROCESSORS else: self.processors = processors self._framework_version = framework_version self.module_cache = ModuleProxyCache() self.instrumentation_store = RequestsStore( lambda: self.get_stack_info_for_trace(iter_stack_frames(), False), self.traces_send_freq_secs, transactions_ignore_patterns) atexit_register(self.close) def get_processors(self): for processor in self.processors: yield self.module_cache[processor](self) def get_ident(self, result): """ Returns a searchable string representing a message. >>> result = client.process(**kwargs) >>> ident = client.get_ident(result) """ return result def get_handler(self, name): return self.module_cache[name](self) def get_stack_info_for_trace(self, frames, extended=True): """Overrideable in derived clients to add frames/info, e.g. templates 4.0: Use for error frames too. """ return stacks.get_stack_info(frames, extended) def build_msg_for_logging(self, event_type, data=None, date=None, extra=None, stack=None, **kwargs): """ Captures, processes and serializes an event into a dict object """ # create ID client-side so that it can be passed to application event_id = uuid.uuid4().hex if data is None: data = {} if extra is None: extra = {} if not date: date = datetime.datetime.utcnow() if stack is None: stack = self.auto_log_stacks self.build_msg(data=data) # if '.' not in event_type: # Assume it's a builtin event_type = 'opbeat.events.%s' % event_type handler = self.get_handler(event_type) result = handler.capture(**kwargs) # data (explicit) culprit takes over auto event detection culprit = result.pop('culprit', None) if data.get('culprit'): culprit = data['culprit'] for k, v in six.iteritems(result): if k not in data: data[k] = v if stack and 'stacktrace' not in data: if stack is True: frames = iter_stack_frames() else: frames = stack data.update({ 'stacktrace': { 'frames': varmap( lambda k, v: shorten(v, string_length=self. string_max_length, list_length=self.list_max_length), stacks.get_stack_info(frames)) }, }) if 'stacktrace' in data and not culprit: culprit = get_culprit(data['stacktrace']['frames'], self.include_paths, self.exclude_paths) if not data.get('level'): data['level'] = 'error' if isinstance(data['level'], six.integer_types): data['level'] = logging.getLevelName(data['level']).lower() data.setdefault('extra', {}) # Shorten lists/strings for k, v in six.iteritems(extra): data['extra'][k] = shorten(v, string_length=self.string_max_length, list_length=self.list_max_length) if culprit: data['culprit'] = culprit # Run the data through processors for processor in self.get_processors(): data.update(processor.process(data)) # Make sure all data is coerced data = transform(data) if 'message' not in data: data['message'] = handler.to_string(data) # Make sure certain values are not too long for v in defaults.MAX_LENGTH_VALUES: if v in data: data[v] = shorten(data[v], string_length=defaults.MAX_LENGTH_VALUES[v]) data.update({ 'timestamp': date, # 'time_spent': time_spent, 'client_supplied_id': event_id, }) return data def build_msg(self, data=None, **kwargs): data.setdefault('machine', {'hostname': self.hostname}) data.setdefault('organization_id', self.organization_id) data.setdefault('app_id', self.app_id) data.setdefault('secret_token', self.secret_token) return data def capture(self, event_type, data=None, date=None, api_path=None, extra=None, stack=None, **kwargs): """ Captures and processes an event and pipes it off to Client.send. To use structured data (interfaces) with capture: >>> client.capture('Message', message='foo', data={ >>> 'http': { >>> 'url': '...', >>> 'data': {}, >>> 'query_string': '...', >>> 'method': 'POST', >>> }, >>> 'logger': 'logger.name', >>> 'site': 'site.name', >>> }, extra={ >>> 'key': 'value', >>> }) The finalized ``data`` structure contains the following (some optional) builtin values: >>> { >>> # the culprit and version information >>> 'culprit': 'full.module.name', # or /arbitrary/path >>> >>> # arbitrary data provided by user >>> 'extra': { >>> 'key': 'value', >>> } >>> } :param event_type: the module path to the Event class. Builtins can use shorthand class notation and exclude the full module path. :param data: the data base :param date: the datetime of this event :param extra: a dictionary of additional standard metadata :return: a 32-length string identifying this event """ data = self.build_msg_for_logging(event_type, data, date, extra, stack, **kwargs) if not api_path: api_path = defaults.ERROR_API_PATH.format(data['organization_id'], data['app_id']) data['servers'] = [server + api_path for server in self.servers] self.send(**data) return data['client_supplied_id'] def _send_remote(self, url, data, headers=None): if headers is None: headers = {} parsed = urlparse.urlparse(url) transport = self._get_transport(parsed) if transport.async_mode: transport.send_async( data, headers, success_callback=self.handle_transport_success, fail_callback=self.handle_transport_fail) else: url = transport.send(data, headers, timeout=self.timeout) self.handle_transport_success(url=url) def _get_log_message(self, data): # decode message so we can show the actual event try: data = self.decode(data) except Exception: message = '<failed decoding data>' else: message = data.pop('message', '<no message value>') return message def _get_transport(self, parsed_url): if self.async_mode and is_master_process(): # when in the master process, always use SYNC mode. This avoids # the danger of being forked into an inconsistent threading state self.logger.info( 'Sending message synchronously while in master ' 'process. PID: %s', os.getpid()) return import_string(defaults.SYNC_TRANSPORT_CLASS)(parsed_url) if parsed_url not in self._transports: self._transports[parsed_url] = self._transport_class(parsed_url) return self._transports[parsed_url] def _filter_exception_type(self, data): exception = data.get('exception') if not exception: return False exc_type = exception.get('type') exc_module = exception.get('module') if exc_module == 'None': exc_module = None if exc_type in self.filter_exception_types_dict: exc_to_filter_module = self.filter_exception_types_dict[exc_type] if not exc_to_filter_module or exc_to_filter_module == exc_module: return True return False def send_remote(self, url, data, headers=None): if not self.state.should_try(): message = self._get_log_message(data) self.error_logger.error(message) return try: self._send_remote(url=url, data=data, headers=headers) except Exception as e: self.handle_transport_fail(exception=e) def send(self, secret_token=None, auth_header=None, servers=None, **data): """ Serializes the message and passes the payload onto ``send_encoded``. """ if self.is_send_disabled or self._filter_exception_type(data): return message = self.encode(data) return self.send_encoded(message, secret_token=secret_token, auth_header=auth_header, servers=servers) def send_encoded(self, message, secret_token, auth_header=None, servers=None, **kwargs): """ Given an already serialized message, signs the message and passes the payload off to ``send_remote`` for each server specified in the servers configuration. """ servers = servers or self.servers if not servers: warnings.warn('opbeat client has no remote servers configured') return if not auth_header: if not secret_token: secret_token = self.secret_token auth_header = "Bearer %s" % (secret_token) for url in servers: headers = { 'Authorization': auth_header, 'Content-Type': 'application/octet-stream', 'User-Agent': 'opbeat-python/%s' % opbeat.VERSION, 'X-Opbeat-Platform': self.get_platform_info() } self.send_remote(url=url, data=message, headers=headers) def encode(self, data): """ Serializes ``data`` into a raw string. """ return zlib.compress(json.dumps(data).encode('utf8')) def decode(self, data): """ Unserializes a string, ``data``. """ return json.loads(zlib.decompress(data).decode('utf8')) def capture_message(self, message, **kwargs): """ Creates an event from ``message``. >>> client.capture_message('My event just happened!') """ return self.capture('Message', message=message, **kwargs) @deprecated(alternative="capture_message()") def captureMessage(self, message, **kwargs): """ Deprecated :param message: :type message: :param kwargs: :type kwargs: :return: :rtype: """ self.capture_message(message, **kwargs) def capture_exception(self, exc_info=None, **kwargs): """ Creates an event from an exception. >>> try: >>> exc_info = sys.exc_info() >>> client.capture_exception(exc_info) >>> finally: >>> del exc_info If exc_info is not provided, or is set to True, then this method will perform the ``exc_info = sys.exc_info()`` and the requisite clean-up for you. """ return self.capture('Exception', exc_info=exc_info, **kwargs) @deprecated(alternative="capture_exception()") def captureException(self, exc_info=None, **kwargs): """ Deprecated """ self.capture_exception(exc_info, **kwargs) def capture_query(self, query, params=(), engine=None, **kwargs): """ Creates an event for a SQL query. >>> client.capture_query('SELECT * FROM foo') """ return self.capture('Query', query=query, params=params, engine=engine, **kwargs) @deprecated(alternative="capture_query()") def captureQuery(self, *args, **kwargs): """ Deprecated """ self.capture_query(*args, **kwargs) def begin_transaction(self, kind): """Register the start of a transaction on the client 'kind' should follow the convention of '<transaction-kind>.<provider>' e.g. 'web.django', 'task.celery'. """ self.instrumentation_store.transaction_start(self, kind) def end_transaction(self, name, status_code=None): self.instrumentation_store.transaction_end(status_code, name) if self.instrumentation_store.should_collect(): self._traces_collect() def close(self): self._traces_collect() for url, transport in self._transports.items(): transport.close() def handle_transport_success(self, **kwargs): """ Success handler called by the transport """ if kwargs.get('url'): self.logger.info('Logged error at ' + kwargs['url']) self.state.set_success() def handle_transport_fail(self, exception=None, **kwargs): """ Failure handler called by the transport """ if isinstance(exception, TransportException): message = self._get_log_message(exception.data) self.error_logger.error(exception.args[0]) else: # stdlib exception message = str(exception) self.error_logger.error('Failed to submit message: %r', message, exc_info=getattr(exception, 'print_trace', True)) self.state.set_fail() def _traces_collect(self): transactions, traces = self.instrumentation_store.get_all() if not transactions or not traces: return data = self.build_msg({ 'transactions': transactions, 'traces': traces, }) api_path = defaults.TRANSACTIONS_API_PATH.format( self.organization_id, self.app_id, ) data['servers'] = [server + api_path for server in self.servers] self.send(**data) def get_platform_info(self): platform_bits = {'lang': 'python/' + platform.python_version()} implementation = platform.python_implementation() if implementation == 'PyPy': implementation += '/' + '.'.join( map(str, sys.pypy_version_info[:3])) platform_bits['platform'] = implementation if self._framework_version: platform_bits['framework'] = self._framework_version return ' '.join('%s=%s' % item for item in platform_bits.items())