def test_tag_transaction(): requests_store = TransactionsStore(lambda: [], 99999) t = requests_store.begin_transaction("test") elasticapm.tag(foo="bar") requests_store.end_transaction(200, "test") assert t.tags == {"foo": "bar"} transaction_dict = t.to_dict() assert transaction_dict["context"]["tags"] == {"foo": "bar"}
def test_tag_transaction(): requests_store = TransactionsStore(lambda: [], 99999) t = requests_store.begin_transaction("test") elasticapm.tag(foo='bar') requests_store.end_transaction(200, 'test') assert t.tags == {'foo': 'bar'} transaction_dict = t.to_dict() assert transaction_dict['context']['tags'] == {'foo': 'bar'}
def test_tag_with_non_string_value(): requests_store = TransactionsStore(lambda: [], 99999) t = requests_store.begin_transaction("test") elasticapm.tag(foo=1) requests_store.end_transaction(200, 'test') assert t.tags == {'foo': '1'}
def test_get_transaction_clear(): requests_store = TransactionsStore(lambda: [], 99999) t = requests_store.begin_transaction("test") assert t == get_transaction(clear=True) assert get_transaction() is None
def test_get_transaction(): requests_store = TransactionsStore(lambda: [], 99999) t = requests_store.begin_transaction("test") assert t == get_transaction()
class Client(object): """ The base ElasticAPM client, which handles communication over the HTTP API to the APM Server. Will read default configuration from the environment variable ``ELASTIC_APM_APP_NAME`` and ``ELASTIC_APM_SECRET_TOKEN`` if available. :: >>> from elasticapm import Client >>> # Read configuration from environment >>> client = Client() >>> # Configure the client manually >>> client = Client( >>> include_paths=['my.package'], >>> service_name='myapp', >>> secret_token='secret_token', >>> ) >>> # Record an exception >>> try: >>> 1/0 >>> except ZeroDivisionError: >>> ident = client.capture_exception() >>> print ("Exception caught; reference is %%s" %% ident) """ logger = logging.getLogger('elasticapm') def __init__(self, config=None, **defaults): # configure loggers first cls = self.__class__ self.logger = logging.getLogger('%s.%s' % (cls.__module__, cls.__name__)) self.error_logger = logging.getLogger('elasticapm.errors') self.state = ClientState() self.instrumentation_store = None self.processors = [] self.filter_exception_types_dict = {} self._send_timer = None self._transports = {} self._service_info = None self.config = Config(config, default_dict=defaults) if self.config.errors: for msg in self.config.errors.values(): self.error_logger.error(msg) self.config.disable_send = True self._transport_class = import_string(self.config.transport_class) for exc_to_filter in (self.config.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 self.processors = [import_string(p) for p in self.config.processors ] if self.config.processors else [] if platform.python_implementation() == 'PyPy': # PyPy introduces a `_functools.partial.__call__` frame due to our use # of `partial` in AbstractInstrumentedModule skip_modules = ('elasticapm.', '_functools') else: skip_modules = ('elasticapm.', ) def frames_collector_func(): return self._get_stack_info_for_trace( stacks.iter_stack_frames(skip_top_modules=skip_modules), library_frame_context_lines=self.config. source_lines_span_library_frames, in_app_frame_context_lines=self.config. source_lines_span_app_frames, with_locals=self.config.collect_local_variables in ('all', 'transactions'), locals_processor_func=lambda local_var: varmap( lambda k, v: shorten( v, list_length=self.config.local_var_list_max_length, string_length=self.config.local_var_max_length, ), local_var)) self.instrumentation_store = TransactionsStore( frames_collector_func=frames_collector_func, collect_frequency=self.config.flush_interval, sample_rate=self.config.transaction_sample_rate, max_spans=self.config.transaction_max_spans, span_frames_min_duration=self.config.span_frames_min_duration_ms, max_queue_size=self.config.max_queue_size, ignore_patterns=self.config.transactions_ignore_patterns, ) self.include_paths_re = stacks.get_path_regex( self.config.include_paths) if self.config.include_paths else None self.exclude_paths_re = stacks.get_path_regex( self.config.exclude_paths) if self.config.exclude_paths else None compat.atexit_register(self.close) def get_handler(self, name): return import_string(name) def capture(self, event_type, date=None, context=None, custom=None, stack=None, handled=True, **kwargs): """ Captures and processes an event and pipes it off to Client.send. """ if event_type == 'Exception': # never gather log stack for exceptions stack = False data = self._build_msg_for_logging(event_type, date=date, context=context, custom=custom, stack=stack, handled=handled, **kwargs) if data: url = self.config.server_url + constants.ERROR_API_PATH self.send(url, **data) return data['errors'][0]['id'] def capture_message(self, message=None, param_message=None, **kwargs): """ Creates an event from ``message``. >>> client.capture_message('My event just happened!') """ return self.capture('Message', message=message, param_message=param_message, **kwargs) def capture_exception(self, exc_info=None, handled=True, **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, handled=handled, **kwargs) def send(self, url, **data): """ Encodes and sends data to remote URL using configured transport :param url: URL of endpoint :param data: dictionary of data to send """ if self.config.disable_send or self._filter_exception_type(data): return payload = self.encode(data) headers = { 'Content-Type': 'application/json', 'Content-Encoding': 'deflate', 'User-Agent': 'elasticapm-python/%s' % elasticapm.VERSION, } if self.config.secret_token: headers['Authorization'] = "Bearer %s" % self.config.secret_token if not self.state.should_try(): message = self._get_log_message(payload) self.error_logger.error(message) return try: self._send_remote(url=url, data=payload, headers=headers) except Exception as e: self.handle_transport_fail(exception=e) 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 begin_transaction(self, transaction_type): """Register the start of a transaction on the client """ return self.instrumentation_store.begin_transaction(transaction_type) def end_transaction(self, name=None, result=''): transaction = self.instrumentation_store.end_transaction(result, name) if self.instrumentation_store.should_collect(): self._collect_transactions() if not self._send_timer: # send first batch of data after config._wait_to_first_send self._start_send_timer(timeout=min(self.config._wait_to_first_send, self.config.flush_interval)) return transaction def close(self): self._collect_transactions() if self._send_timer: self._stop_send_timer() for url, transport in list(self._transports.items()): transport.close() self._transports.pop(url) 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 _collect_transactions(self): self._stop_send_timer() transactions = [] if self.instrumentation_store: for transaction in self.instrumentation_store.get_all(): for processor in self.processors: transaction = processor(self, transaction) transactions.append(transaction) if not transactions: return data = self._build_msg({ 'transactions': transactions, }) api_path = constants.TRANSACTIONS_API_PATH self.send(self.config.server_url + api_path, **data) self._start_send_timer() def _start_send_timer(self, timeout=None): timeout = timeout or self.config.flush_interval self._send_timer = threading.Timer(timeout, self._collect_transactions) self._send_timer.start() def _stop_send_timer(self): if self._send_timer and self._send_timer.is_alive( ) and not self._send_timer == threading.current_thread(): self._send_timer.cancel() self._send_timer.join() def _send_remote(self, url, data, headers=None): if headers is None: headers = {} parsed = compat.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.config.server_timeout) self.handle_transport_success(url=url) def get_service_info(self): if self._service_info: return self._service_info language_version = platform.python_version() if hasattr(sys, 'pypy_version_info'): runtime_version = '.'.join(map(str, sys.pypy_version_info[:3])) else: runtime_version = language_version result = { 'name': keyword_field(self.config.service_name), 'environment': keyword_field(self.config.environment), 'version': keyword_field(self.config.service_version), 'agent': { 'name': 'python', 'version': elasticapm.VERSION, }, 'language': { 'name': 'python', 'version': keyword_field(platform.python_version()), }, 'runtime': { 'name': keyword_field(platform.python_implementation()), 'version': keyword_field(runtime_version), } } if self.config.framework_name: result['framework'] = { 'name': keyword_field(self.config.framework_name), 'version': keyword_field(self.config.framework_version), } self._service_info = result return result def get_process_info(self): return { 'pid': os.getpid(), 'ppid': os.getppid() if hasattr(os, 'getppid') else None, 'argv': sys.argv, 'title': None, # Note: if we implement this, the value needs to be wrapped with keyword_field } def get_system_info(self): return { 'hostname': keyword_field(socket.gethostname()), 'architecture': platform.machine(), 'platform': platform.system().lower(), } def _build_msg(self, data=None, **kwargs): data = data or {} data['service'] = self.get_service_info() data['process'] = self.get_process_info() data['system'] = self.get_system_info() data.update(**kwargs) return data def _build_msg_for_logging(self, event_type, date=None, context=None, custom=None, stack=None, handled=True, **kwargs): """ Captures, processes and serializes an event into a dict object """ transaction = get_transaction() if transaction: transaction_context = deepcopy(transaction.context) else: transaction_context = {} event_data = {} if custom is None: custom = {} if not date: date = datetime.datetime.utcnow() if stack is None: stack = self.config.auto_log_stacks if context: transaction_context.update(context) context = transaction_context else: context = transaction_context event_data['context'] = context if transaction and transaction.tags: context['tags'] = deepcopy(transaction.tags) # if '.' not in event_type: # Assume it's a builtin event_type = 'elasticapm.events.%s' % event_type handler = self.get_handler(event_type) result = handler.capture(self, **kwargs) if self._filter_exception_type(result): return # data (explicit) culprit takes over auto event detection culprit = result.pop('culprit', None) if custom.get('culprit'): culprit = custom.pop('culprit') for k, v in compat.iteritems(result): if k not in event_data: event_data[k] = v log = event_data.get('log', {}) if stack and 'stacktrace' not in log: if stack is True: frames = stacks.iter_stack_frames(skip=3) else: frames = stack frames = stacks.get_stack_info( frames, with_locals=self.config.collect_local_variables in ('errors', 'all'), library_frame_context_lines=self.config. source_lines_error_library_frames, in_app_frame_context_lines=self.config. source_lines_error_app_frames, include_paths_re=self.include_paths_re, exclude_paths_re=self.exclude_paths_re, locals_processor_func=lambda local_var: varmap( lambda k, v: shorten( v, list_length=self.config.local_var_list_max_length, string_length=self.config.local_var_max_length, ), local_var)) log['stacktrace'] = frames if 'stacktrace' in log and not culprit: culprit = stacks.get_culprit(log['stacktrace'], self.config.include_paths, self.config.exclude_paths) if 'level' in log and isinstance(log['level'], compat.integer_types): log['level'] = logging.getLevelName(log['level']).lower() if log: event_data['log'] = log if culprit: event_data['culprit'] = culprit if 'custom' in context: context['custom'].update(custom) else: context['custom'] = custom # Run the data through processors for processor in self.processors: event_data = processor(self, event_data) # Make sure all data is coerced event_data = transform(event_data) if 'exception' in event_data: event_data['exception']['handled'] = bool(handled) event_data.update({ 'timestamp': date.strftime(constants.TIMESTAMP_FORMAT), }) transaction = get_transaction() if transaction: event_data['transaction'] = {'id': transaction.id} return self._build_msg({'errors': [event_data]}) 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: if exc_module: exc_name = '%s.%s' % (exc_module, exc_type) else: exc_name = exc_type self.logger.info( 'Ignored %s exception due to exception type filter', exc_name) return True return False 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 hasattr(self._transport_class, 'sync_transport') 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 self._transport_class.sync_transport(parsed_url) if parsed_url not in self._transports: self._transports[parsed_url] = self._transport_class( parsed_url, verify_server_cert=self.config.verify_server_cert) return self._transports[parsed_url] def _get_stack_info_for_trace(self, frames, library_frame_context_lines=None, in_app_frame_context_lines=None, with_locals=True, locals_processor_func=None): """Overrideable in derived clients to add frames/info, e.g. templates""" return stacks.get_stack_info( frames, library_frame_context_lines=library_frame_context_lines, in_app_frame_context_lines=in_app_frame_context_lines, with_locals=with_locals, include_paths_re=self.include_paths_re, exclude_paths_re=self.exclude_paths_re, locals_processor_func=locals_processor_func, )
class Client(object): """ The base ElasticAPM client, which handles communication over the HTTP API to the APM Server. Will read default configuration from the environment variable ``ELASTIC_APM_APP_NAME`` and ``ELASTIC_APM_SECRET_TOKEN`` if available. :: >>> from elasticapm import Client >>> # Read configuration from environment >>> client = Client() >>> # Configure the client manually >>> client = Client( >>> include_paths=['my.package'], >>> app_name='app_name', >>> 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('elasticapm') protocol_version = '1.0' environment_config_map = { 'app_name': 'ELASTIC_APM_APP_NAME', 'secret_token': 'ELASTIC_APM_SECRET_TOKEN', 'git_ref': 'ELASTIC_APM_GIT_REF', 'app_version': 'ELASTIC_APM_APP_VERSION', } def __init__(self, app_name=None, secret_token=None, transport_class=None, include_paths=None, exclude_paths=None, timeout=None, hostname=None, auto_log_stacks=None, string_max_length=None, list_max_length=None, processors=None, filter_exception_types=None, servers=None, async_mode=None, traces_send_freq_secs=None, transactions_ignore_patterns=None, git_ref=None, app_version=None, **kwargs): self.app_name = self.secret_token = self.git_ref = self.app_version = None # configure loggers first cls = self.__class__ self.logger = logging.getLogger('%s.%s' % (cls.__module__, cls.__name__)) self.error_logger = logging.getLogger('elasticapm.errors') self.state = ClientState() self._configure(app_name=app_name, secret_token=secret_token, git_ref=git_ref, app_version=app_version) self.servers = servers or defaults.SERVERS 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 (self.app_name and self.secret_token): msg = 'Missing configuration for ElasticAPM client. Please see documentation.' self.logger.info(msg) self.is_send_disabled = (os.environ.get('ELASTIC_APM_DISABLE_SEND', '').lower() in ('1', 'true')) if self.is_send_disabled: self.logger.info( 'Not sending any data to APM Server due to ELASTIC_APM_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.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.processors = [import_string(p) for p in self.processors] self.instrumentation_store = TransactionsStore( lambda: self.get_stack_info_for_trace(iter_stack_frames(), False), self.traces_send_freq_secs, transactions_ignore_patterns) atexit_register(self.close) def _configure(self, **kwargs): """ Configures this instance based on kwargs, or environment variables. The former take precedence. """ for attr_name, value in kwargs.items(): if value is None and (attr_name in self.environment_config_map and self.environment_config_map[attr_name] in os.environ): self.logger.info( "Configuring elasticapm.%s from environment variable '%s'", attr_name, self.environment_config_map[attr_name]) value = os.environ[self.environment_config_map[attr_name]] setattr(self, attr_name, six.text_type(value)) 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 import_string(name) 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 """ 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 if 'context' not in data: data['context'] = context = {} else: context = data['context'] # if '.' not in event_type: # Assume it's a builtin event_type = 'elasticapm.events.%s' % event_type handler = self.get_handler(event_type) result = handler.capture(self, data=data, **kwargs) if self._filter_exception_type(result): return # 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 log = data.get('log', {}) if stack and 'stacktrace' not in log: if stack is True: frames = iter_stack_frames() else: frames = stack frames = varmap( lambda k, v: shorten(v, string_length=self.string_max_length, list_length=self.list_max_length), stacks.get_stack_info(frames)) log['stacktrace'] = frames if 'stacktrace' in log and not culprit: culprit = get_culprit(log['stacktrace'], self.include_paths, self.exclude_paths) if 'level' in log and isinstance(log['level'], six.integer_types): log['level'] = logging.getLevelName(log['level']).lower() if log: data['log'] = log if culprit: data['culprit'] = culprit context['custom'] = extra # Run the data through processors for processor in self.processors: data = processor(self, data) # Make sure all data is coerced data = transform(data) data.update({ 'timestamp': date.strftime(defaults.TIMESTAMP_FORMAT), }) return self.build_msg({'errors': [data]}) def build_msg(self, data=None, **kwargs): data = data or {} data['app'] = self.get_app_info() data['system'] = self.get_system_info() data.update(**kwargs) return data def capture(self, event_type, data=None, date=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 data: servers = [ server + defaults.ERROR_API_PATH for server in self.servers ] self.send(servers=servers, **data) return data['errors'][0]['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: if exc_module: exc_name = '%s.%s' % (exc_module, exc_type) else: exc_name = exc_type self.logger.info( 'Ignored %s exception due to exception type filter', exc_name) 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('elasticapm 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/json', 'Content-Encoding': 'deflate', 'User-Agent': 'elasticapm-python/%s' % elasticapm.VERSION, } 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, transaction_type): """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.begin_transaction(transaction_type) def end_transaction(self, name, status_code=None): self.instrumentation_store.end_transaction(status_code, name) if self.instrumentation_store.should_collect(): self._traces_collect() def set_transaction_name(self, name): transaction = get_transaction() if not transaction: return transaction.name = name def set_transaction_extra_data(self, data, _key=None): transaction = get_transaction() if not transaction: return if not _key: _key = 'extra' transaction.extra[_key] = data 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 = self.instrumentation_store.get_all() if not transactions: return data = self.build_msg({ 'transactions': transactions, }) api_path = defaults.TRANSACTIONS_API_PATH self.send(servers=[server + api_path for server in self.servers], **data) def get_app_info(self): language_version = platform.python_version() if hasattr(sys, 'pypy_version_info'): runtime_version = '.'.join(map(str, sys.pypy_version_info[:3])) else: runtime_version = language_version return { 'name': self.app_name, 'version': self.app_version, 'agent': { 'name': 'elasticapm-python', 'version': elasticapm.VERSION, }, 'argv': sys.argv, 'framework': { 'name': getattr(self, '_framework', None), 'version': getattr(self, '_framework_version', None), }, 'git_ref': self.git_ref, 'language': { 'name': 'python', 'version': platform.python_version(), }, 'pid': os.getpid(), 'process_title': None, 'runtime': { 'name': platform.python_implementation(), 'version': runtime_version, } } def get_system_info(self): return { 'hostname': socket.gethostname(), 'architecture': platform.machine(), 'platform': platform.system().lower(), }
class RequestStoreTest(TestCase): def setUp(self): self.mock_get_frames = Mock() frames = [{ 'function': 'something_expensive', 'abs_path': '/var/parent-elasticapm/elasticapm/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/elasticapm/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/elasticapm/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/elasticapm/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-elasticapm/elasticapm/tests/contrib/django/testapp/templates/list_fish.html' }, { 'function': 'render', 'abs_path': '/home/ron/.virtualenvs/elasticapm/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/elasticapm/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/elasticapm/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/elasticapm/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/elasticapm/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/elasticapm/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/elasticapm/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/elasticapm/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/elasticapm/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/elasticapm/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/elasticapm/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/elasticapm/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-elasticapm/elasticapm/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 = TransactionsStore(self.mock_get_frames, 99999) def test_lru_get_frames_cache(self): self.requests_store.begin_transaction("transaction.test") for i in range(10): with trace("bleh", "custom"): time.sleep(0.01) self.assertEqual(self.mock_get_frames.call_count, 10) def test_leaf_tracing(self): self.requests_store.begin_transaction("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.end_transaction(None, "transaction") transactions = self.requests_store.get_all() traces = transactions[0]['traces'] self.assertEqual(len(traces), 3) signatures = ['transaction', 'root', 'child1-leaf'] self.assertEqual(set([t['name'] for t in traces]), set(signatures))