def html_to_be_inserted(): header = self.transaction.browser_timing_header() if not header: return b'' footer = self.transaction.browser_timing_footer() return six.b(header) + six.b(footer)
def _encode(name, key): s = [] # Convert name and key into bytes which are treated as integers. key = list(six.iterbytes(six.b(key))) for i, c in enumerate(six.iterbytes(six.b(name))): s.append(chr(c ^ key[i % len(key)])) return s
def obfuscate(name, key): if not (name and key): return '' # Always pass name and key as str to _encode() return base64.b64encode(six.b(''.join(_encode(name, key))))
def slow_transaction_data(self): """Returns a list containing any slow transaction data collected during the reporting period. NOTE Currently only the slowest transaction for the reporting period is retained. """ if not self.__settings: return [] if not self.__slow_transaction: return [] maximum = self.__settings.agent_limits.transaction_traces_nodes transaction_trace = self.__slow_transaction.transaction_trace( self, maximum) internal_metric('Supportability/StatsEngine/Counts/' 'transaction_sample_data', self.__slow_transaction.trace_node_count) data = [transaction_trace, list(self.__slow_transaction.string_table.values())] if self.__settings.debug.log_transaction_trace_payload: _logger.debug('Encoding slow transaction data where ' 'payload=%r.', data) with InternalTrace('Supportability/StatsEngine/JSON/Encode/' 'transaction_sample_data'): json_data = json_encode(data) internal_metric('Supportability/StatsEngine/ZLIB/Bytes/' 'transaction_sample_data', len(json_data)) with InternalTrace('Supportability/StatsEngine/ZLIB/Compress/' 'transaction_sample_data'): zlib_data = zlib.compress(six.b(json_data)) with InternalTrace('Supportability/StatsEngine/BASE64/Encode/' 'transaction_sample_data'): pack_data = base64.standard_b64encode(zlib_data) if six.PY3: pack_data = pack_data.decode('Latin-1') root = transaction_trace.root trace_data = [[root.start_time, root.end_time - root.start_time, self.__slow_transaction.path, self.__slow_transaction.request_uri, pack_data]] return trace_data
def deobfuscate(name, key): if not (name and key): return '' # Always pass name and key as str to _encode() return ''.join(_encode(six.text_type(base64.b64decode(six.b(name)), encoding='Latin-1'), key))
def slow_transaction_data(self): """Returns a list containing any slow transaction data collected during the reporting period. NOTE Currently only the slowest transaction for the reporting period is retained. """ if not self.__settings: return [] if not self.__slow_transaction: return [] maximum = self.__settings.agent_limits.transaction_traces_nodes transaction_trace = self.__slow_transaction.transaction_trace( self, maximum) internal_metric('Supportability/StatsEngine/Counts/' 'transaction_sample_data', self.__slow_transaction.trace_node_count) data = [transaction_trace, list(self.__slow_transaction.string_table.values())] if self.__settings.debug.log_transaction_trace_payload: _logger.debug('Encoding slow transaction data where ' 'payload=%r.', data) with InternalTrace('Supportability/StatsEngine/JSON/Encode/' 'transaction_sample_data'): json_data = simplejson.dumps(data, ensure_ascii=True, encoding='Latin-1', namedtuple_as_object=False, default=lambda o: list(iter(o))) internal_metric('Supportability/StatsEngine/ZLIB/Bytes/' 'transaction_sample_data', len(json_data)) with InternalTrace('Supportability/StatsEngine/ZLIB/Compress/' 'transaction_sample_data'): zlib_data = zlib.compress(six.b(json_data)) with InternalTrace('Supportability/StatsEngine/BASE64/Encode/' 'transaction_sample_data'): pack_data = base64.standard_b64encode(zlib_data) root = transaction_trace.root trace_data = [[root.start_time, root.end_time - root.start_time, self.__slow_transaction.path, self.__slow_transaction.request_uri, pack_data]] return trace_data
def profile_data(self): # Generic profiling sessions have to wait for completion before # reporting data. if self.state == SessionState.RUNNING: return None # We prune the number of nodes sent if we are over the specified # limit. This is just to avoid having the response be too large # and get rejected by the data collector. settings = global_settings() self._prune_call_trees(settings.agent_limits.thread_profiler_nodes) flat_tree = {} thread_count = 0 for category, bucket in six.iteritems(self.call_buckets): # Only flatten buckets that have data in them. No need to send # empty buckets. if bucket: flat_tree[category] = [x.flatten() for x in bucket.values()] thread_count += len(bucket) # Construct the actual final data for sending. The actual call # data is turned into JSON, compressed and then base64 encoded at # this point to cut its size. if settings.debug.log_thread_profile_payload: _logger.debug('Encoding thread profile data where ' 'payload=%r.', flat_tree) json_call_tree = json_encode(flat_tree) level = settings.agent_limits.data_compression_level level = level or zlib.Z_DEFAULT_COMPRESSION encoded_tree = base64.standard_b64encode( zlib.compress(six.b(json_call_tree), level)) if six.PY3: encoded_tree = encoded_tree.decode('Latin-1') profile = [[ self.profile_id, self.start_time_s * 1000, (self.actual_stop_time_s or time.time()) * 1000, self.sample_count, encoded_tree, thread_count, 0, None ]] # Reset the data structures to default. self.reset_profile_data() return profile
def deobfuscate(name, key): if not (name and key): return '' # Always pass name and key as str to _encode() return ''.join( _encode( six.text_type(base64.b64decode(six.b(name)), encoding='Latin-1'), key))
def slow_sql_data(self): if not self.__settings: return [] if not self.__sql_stats_table: return [] maximum = self.__settings.agent_limits.slow_sql_data slow_sql_nodes = sorted(six.itervalues(self.__sql_stats_table), key=lambda x: x.max_call_time)[-maximum:] result = [] for node in slow_sql_nodes: params = {} if node.slow_sql_node.stack_trace: params['backtrace'] = node.slow_sql_node.stack_trace explain_plan = node.slow_sql_node.explain_plan if explain_plan: params['explain_plan'] = explain_plan json_data = json_encode(params) params_data = base64.standard_b64encode( zlib.compress(six.b(json_data))) if six.PY3: params_data = params_data.decode('Latin-1') # Limit the length of any SQL that is reported back. limit = self.__settings.agent_limits.sql_query_length_maximum sql = node.slow_sql_node.formatted[:limit] data = [node.slow_sql_node.path, node.slow_sql_node.request_uri, node.slow_sql_node.identifier, sql, node.slow_sql_node.metric, node.call_count, node.total_call_time * 1000, node.min_call_time * 1000, node.max_call_time * 1000, params_data] result.append(data) return result
def slow_sql_data(self): if not self.__settings: return [] if not self.__sql_stats_table: return [] maximum = self.__settings.agent_limits.slow_sql_data slow_sql_nodes = sorted(six.itervalues(self.__sql_stats_table), key=lambda x: x.max_call_time)[-maximum:] result = [] for node in slow_sql_nodes: params = {} if node.slow_sql_node.stack_trace: params['backtrace'] = node.slow_sql_node.stack_trace explain_plan = node.slow_sql_node.explain_plan if explain_plan: params['explain_plan'] = explain_plan json_data = json_encode(params) params_data = base64.standard_b64encode( zlib.compress(six.b(json_data))) if six.PY3: params_data = params_data.decode('Latin-1') # Limit the length of any SQL that is reported back. limit = self.__settings.agent_limits.sql_query_length_maximum sql = node.slow_sql_node.formatted[:limit] data = [ node.slow_sql_node.path, node.slow_sql_node.request_uri, node.slow_sql_node.identifier, sql, node.slow_sql_node.metric, node.call_count, node.total_call_time * 1000, node.min_call_time * 1000, node.max_call_time * 1000, params_data ] result.append(data) return result
def slow_sql_data(self): if not self.__settings: return [] if not self.__sql_stats_table: return [] maximum = self.__settings.agent_limits.slow_sql_data slow_sql_nodes = sorted(six.itervalues(self.__sql_stats_table), key=lambda x: x.max_call_time)[-maximum:] result = [] for node in slow_sql_nodes: params = {} if node.slow_sql_node.stack_trace: params['backtrace'] = node.slow_sql_node.stack_trace explain_plan = node.slow_sql_node.explain_plan if explain_plan: params['explain_plan'] = explain_plan json_data = simplejson.dumps(params, ensure_ascii=True, encoding='Latin-1', namedtuple_as_object=False, default=lambda o: list(iter(o))) params_data = base64.standard_b64encode( zlib.compress(six.b(json_data))) data = [node.slow_sql_node.path, node.slow_sql_node.request_uri, node.slow_sql_node.identifier, node.slow_sql_node.formatted, node.slow_sql_node.metric, node.call_count, node.total_call_time * 1000, node.min_call_time * 1000, node.max_call_time * 1000, params_data] result.append(data) return result
def html_to_be_inserted(): return six.b(header) + six.b(transaction.browser_timing_footer())
def browser_timing_middleware(request, response): # Don't do anything if receive a streaming response which # was introduced in Django 1.5. Need to avoid this as there # will be no 'content' attribute. Alternatively there may be # a 'content' attribute which flattens the stream, which if # we access, will break the streaming and/or buffer what is # potentially a very large response in memory contrary to # what user wanted by explicitly using a streaming response # object in the first place. To preserve streaming but still # do RUM insertion, need to move to a WSGI middleware and # deal with how to update the content length. if hasattr(response, 'streaming_content'): return response # Need to be running within a valid web transaction. transaction = current_transaction() if not transaction: return response # Only insert RUM JavaScript headers and footers if enabled # in configuration. if not transaction.settings.rum.enabled: return response if transaction.autorum_disabled: return response # Only possible if the content type is text/html. ctype = response.get('Content-Type', '').lower() if ctype != 'text/html' and not ctype.startswith('text/html;'): return response # Don't risk it if content encoding already set. if response.has_header('Content-Encoding'): return response # No point continuing if header is empty. This can occur if # RUM is not enabled within the UI. It is assumed at this # point that if header is not empty, then footer will be not # empty. We don't want to generate the footer just yet as # want to do that as late as possible so that application # server time in footer is as accurate as possible. In # particular, if the response content is generated on demand # then the flattening of the response could take some time # and we want to track that. We thus generate footer below # at point of insertion. header = transaction.browser_timing_header() if not header: return response header = six.b(header) # Make sure we flatten any content first as it could be # stored as a list of strings in the response object. We # assign it back to the response object to avoid having # multiple copies of the string in memory at the same time # as we progress through steps below. content = response.content response.content = content # Insert the JavaScript. If there is no <head> element we # insert our own containing the JavaScript. If we detect # possibility of IE compatibility mode then we insert # JavaScript at end of the <head> element. In other cases # insert at the start of the <head> element. # # When updating response content we null the original first # to avoid multiple copies in memory when we recompose the # actual response from list of strings. start = content.find(b'<head') end = content.rfind(b'</body>', -1024) if start != -1 and end != -1: offset = content.find(b'</head>', start) if content.find(b'X-UA-Compatible', start, offset) == -1: start = content.find(b'>', start, start + 1024) elif offset != -1: start = offset - 1 if start != -1 and start < end: parts = [] parts.append(content[0:start + 1]) parts.append(header) parts.append(content[start + 1:end]) footer = transaction.browser_timing_footer() footer = six.b(footer) parts.append(footer) parts.append(content[end:]) response.content = b'' content = b''.join(parts) response.content = content elif start == -1 and end != -1: start = content.find(b'<body') if start != -1 and start < end: parts = [] parts.append(content[0:start]) parts.append(b'<head>') parts.append(header) parts.append(b'</head>') parts.append(content[start:end]) footer = transaction.browser_timing_footer() footer = six.b(footer) parts.append(footer) parts.append(content[end:]) response.content = '' content = b''.join(parts) response.content = content response['Content-Length'] = str(len(response.content)) return response
def send_request(session, url, method, license_key, agent_run_id=None, payload=()): """Constructs and sends a request to the data collector.""" params = {} headers = {} config = {} settings = global_settings() start = time.time() # Validate that the license key was actually set and if not replace # it with a string which makes it more obvious it was not set. if not license_key: license_key = 'NO LICENSE KEY WAS SET IN AGENT CONFIGURATION' # The agent formats requests and is able to handle responses for # protocol version 12. params['method'] = method params['license_key'] = license_key params['protocol_version'] = '12' params['marshal_format'] = 'json' if agent_run_id: params['run_id'] = str(agent_run_id) headers['User-Agent'] = USER_AGENT headers['Content-Encoding'] = 'identity' # Set up definitions for proxy server in case that has been set. proxies = proxy_server() # At this time we use JSON content encoding for the data being sent. # If an error does occur when encoding the JSON, then it isn't # likely going to work later on in a subsequent request with same # data, even if aggregated with other data, so we need to log the # details and then flag that data should be thrown away. Don't mind # being noisy in the the log in this situation as it would indicate # a problem with the implementation of the agent. try: with InternalTrace('Supportability/Collector/JSON/Encode/%s' % method): data = json_encode(payload) except Exception: _logger.exception( 'Error encoding data for JSON payload for ' 'method %r with payload of %r. Please report this problem ' 'to New Relic support.', method, payload) raise DiscardDataForRequest(str(sys.exc_info()[1])) # Log details of call and/or payload for debugging. Use the JSON # encoded value so know that what is encoded is correct. if settings.debug.log_data_collector_payloads: _logger.debug( 'Calling data collector with url=%r, method=%r and ' 'payload=%r.', url, method, data) elif settings.debug.log_data_collector_calls: _logger.debug('Calling data collector with url=%r and method=%r.', url, method) # Compress the serialized JSON being sent as content if over 64KiB # in size. If less than 2MB in size compress for speed. If over # 2MB then compress for smallest size. This parallels what the Ruby # agent does. if len(data) > 64 * 1024: headers['Content-Encoding'] = 'deflate' level = (len(data) < 2000000) and 1 or 9 internal_metric('Supportability/Collector/ZLIB/Bytes/%s' % method, len(data)) with InternalTrace('Supportability/Collector/ZLIB/Compress/' '%s' % method): data = zlib.compress(six.b(data), level) # If there is no requests session object provided for making # requests create one now. We want to close this as soon as we # are done with it. auto_close_session = False if not session: session = requests.session() auto_close_session = True # The 'requests' library can raise a number of exception derived # from 'RequestException' before we even manage to get a connection # to the data collector. # # The data collector can the generate a number of different types of # HTTP errors for requests. These are: # # 400 Bad Request - For incorrect method type or incorrectly # construct parameters. We should not get this and if we do it would # likely indicate a problem with the implementation of the agent. # # 413 Request Entity Too Large - Where the request content was too # large. The limits on number of nodes in slow transaction traces # should in general prevent this, but not everything has size limits # and so rogue data could still blow things out. Same data is not # going to work later on in a subsequent request, even if aggregated # with other data, so we need to log the details and then flag that # data should be thrown away. # # 415 Unsupported Media Type - This occurs when the JSON which was # sent can't be decoded by the data collector. If this is a true # problem with the JSON formatting, then sending again, even if # aggregated with other data, may not work, so we need to log the # details and then flag that data should be thrown away. # # 503 Service Unavailable - This occurs when data collector, or core # application is being restarted and not in state to be able to # accept requests. It should be a transient issue so should be able # to retain data and try again. internal_metric('Supportability/Collector/Output/Bytes/%s' % method, len(data)) try: # The timeout value in the requests module is only on # the initial connection and doesn't apply to how long # it takes to get back a response. timeout = settings.agent_limits.data_collector_timeout r = session.post(url, params=params, headers=headers, proxies=proxies, timeout=timeout, data=data) # Read the content now so we can force close the socket # connection if this is a transient session as quickly # as possible. content = r.content except requests.RequestException: if not settings.proxy_host or not settings.proxy_port: _logger.warning( 'Data collector is not contactable. This can be ' 'because of a network issue or because of the data ' 'collector being restarted. In the event that contact ' 'cannot be made after a period of time then please ' 'report this problem to New Relic support for further ' 'investigation. The error raised was %r.', sys.exc_info()[1]) else: _logger.warning( 'Data collector is not contactable via the proxy ' 'host %r on port %r with proxy user of %r. This can be ' 'because of a network issue or because of the data ' 'collector being restarted. In the event that contact ' 'cannot be made after a period of time then please ' 'report this problem to New Relic support for further ' 'investigation. The error raised was %r.', settings.proxy_host, settings.proxy_port, settings.proxy_user, sys.exc_info()[1]) raise RetryDataForRequest(str(sys.exc_info()[1])) finally: if auto_close_session: session.close() session = None if r.status_code != 200: _logger.debug( 'Received a non 200 HTTP response from the data ' 'collector where url=%r, method=%r, license_key=%r, ' 'agent_run_id=%r, params=%r, headers=%r, status_code=%r ' 'and content=%r.', url, method, license_key, agent_run_id, params, headers, r.status_code, content) if r.status_code == 400: _logger.error( 'Data collector is indicating that a bad ' 'request has been submitted for url %r, headers of %r, ' 'params of %r and payload of %r. Please report this ' 'problem to New Relic support.', url, headers, params, payload) raise DiscardDataForRequest() elif r.status_code == 413: _logger.warning( 'Data collector is indicating that a request for ' 'method %r was received where the request content size ' 'was over the maximum allowed size limit. The length of ' 'the request content was %d. If this keeps occurring on a ' 'regular basis, please report this problem to New Relic ' 'support for further investigation.', method, len(data)) raise DiscardDataForRequest() elif r.status_code == 415: _logger.warning( 'Data collector is indicating that it was sent ' 'malformed JSON data for method %r. If this keeps occurring ' 'on a regular basis, please report this problem to New ' 'Relic support for further investigation.', method) if settings.debug.log_malformed_json_data: if headers['Content-Encoding'] == 'deflate': data = zlib.decompress(data) _logger.info( 'JSON data which was rejected by the data ' 'collector was %r.', data) raise DiscardDataForRequest(content) elif r.status_code == 503: _logger.warning( 'Data collector is unavailable. This can be a ' 'transient issue because of the data collector or our ' 'core application being restarted. If the issue persists ' 'it can also be indicative of a problem with our servers. ' 'In the event that availability of our servers is not ' 'restored after a period of time then please report this ' 'problem to New Relic support for further investigation.') raise ServerIsUnavailable() elif r.status_code != 200: if not settings.proxy_host or not settings.proxy_port: _logger.warning( 'An unexpected HTTP response was received from ' 'the data collector of %r for method %r. The payload for ' 'the request was %r. If this issue persists then please ' 'report this problem to New Relic support for further ' 'investigation.', r.status_code, method, payload) else: _logger.warning( 'An unexpected HTTP response was received from ' 'the data collector of %r for method %r while connecting ' 'via proxy host %r on port %r with proxy user of %r. ' 'The payload for the request was %r. If this issue ' 'persists then please report this problem to New Relic ' 'support for further investigation.', r.status_code, method, settings.proxy_host, settings.proxy_port, settings.proxy_user, payload) raise DiscardDataForRequest() # Log details of response payload for debugging. Use the JSON # encoded value so know that what original encoded value was. duration = time.time() - start if settings.debug.log_data_collector_payloads: _logger.debug( 'Valid response from data collector after %.2f ' 'seconds with content=%r.', duration, content) elif settings.debug.log_data_collector_calls: _logger.debug( 'Valid response from data collector after %.2f ' 'seconds.', duration) # If we got this far we should have a legitimate response from the # data collector. The response is JSON so need to decode it. internal_metric('Supportability/Collector/Input/Bytes/%s' % method, len(content)) try: with InternalTrace('Supportability/Collector/JSON/Decode/%s' % method): if six.PY3: content = content.decode('UTF-8') result = json_decode(content) except Exception: _logger.exception( 'Error decoding data for JSON payload for ' 'method %r with payload of %r. Please report this problem ' 'to New Relic support.', method, content) if settings.debug.log_malformed_json_data: _logger.info( 'JSON data received from data collector which ' 'could not be decoded was %r.', content) raise DiscardDataForRequest(str(sys.exc_info()[1])) # The decoded JSON can be either for a successful response or an # error. A successful response has a 'return_value' element and an # error an 'exception' element. if 'return_value' in result: return result['return_value'] error_type = result['exception']['error_type'] message = result['exception']['message'] # Now need to check for server side exceptions. The following # exceptions can occur for abnormal events. _logger.debug( 'Received an exception from the data collector where ' 'url=%r, method=%r, license_key=%r, agent_run_id=%r, params=%r, ' 'headers=%r, error_type=%r and message=%r', url, method, license_key, agent_run_id, params, headers, error_type, message) if error_type == 'NewRelic::Agent::LicenseException': _logger.error( 'Data collector is indicating that an incorrect ' 'license key has been supplied by the agent. The value ' 'which was used by the agent is %r. Please correct any ' 'problem with the license key or report this problem to ' 'New Relic support.', license_key) raise DiscardDataForRequest(message) elif error_type == 'NewRelic::Agent::PostTooBigException': _logger.warning( 'Core application is indicating that a request for ' 'method %r was received where the request content size ' 'was over the maximum allowed size limit. The length of ' 'the request content was %d. If this keeps occurring on a ' 'regular basis, please report this problem to New Relic ' 'support for further investigation.', method, len(data)) raise DiscardDataForRequest(message) # Server side exceptions are also used to inform the agent to # perform certain actions such as restart when server side # configuration has changed for this application or when agent is # being disabled remotely for some reason. if error_type == 'NewRelic::Agent::ForceRestartException': _logger.info( 'An automatic internal agent restart has been ' 'requested by the data collector for the application ' 'where the agent run was %r. The reason given for the ' 'forced restart is %r.', agent_run_id, message) raise ForceAgentRestart(message) elif error_type == 'NewRelic::Agent::ForceDisconnectException': _logger.critical( 'Disconnection of the agent has been requested by ' 'the data collector for the application where the ' 'agent run was %r. The reason given for the forced ' 'disconnection is %r. Please contact New Relic support ' 'for further information.', agent_run_id, message) raise ForceAgentDisconnect(message) # We received an unexpected server side error we don't know what # to do with. _logger.warning( 'An unexpected server error was received from the ' 'data collector for method %r with payload of %r. The error ' 'was of type %r with message %r. If this issue persists ' 'then please report this problem to New Relic support for ' 'further investigation.', method, payload, error_type, message) raise DiscardDataForRequest(message)
def send_request(session, url, method, license_key, agent_run_id=None, payload=()): """Constructs and sends a request to the data collector.""" params = {} headers = {} config = {} settings = global_settings() start = time.time() # Validate that the license key was actually set and if not replace # it with a string which makes it more obvious it was not set. if not license_key: license_key = 'NO LICENSE KEY WAS SET IN AGENT CONFIGURATION' # The agent formats requests and is able to handle responses for # protocol version 14. params['method'] = method params['license_key'] = license_key params['protocol_version'] = '14' params['marshal_format'] = 'json' if agent_run_id: params['run_id'] = str(agent_run_id) headers['User-Agent'] = USER_AGENT headers['Content-Encoding'] = 'identity' # Set up definitions for proxy server in case that has been set. proxies = proxy_server() # At this time we use JSON content encoding for the data being sent. # If an error does occur when encoding the JSON, then it isn't # likely going to work later on in a subsequent request with same # data, even if aggregated with other data, so we need to log the # details and then flag that data should be thrown away. Don't mind # being noisy in the the log in this situation as it would indicate # a problem with the implementation of the agent. try: with InternalTrace('Supportability/Collector/JSON/Encode/%s' % method): data = json_encode(payload) except Exception: _logger.exception('Error encoding data for JSON payload for ' 'method %r with payload of %r. Please report this problem ' 'to New Relic support.', method, payload) raise DiscardDataForRequest(str(sys.exc_info()[1])) # Log details of call and/or payload for debugging. Use the JSON # encoded value so know that what is encoded is correct. if settings.debug.log_data_collector_payloads: _logger.debug('Calling data collector with url=%r, method=%r and ' 'payload=%r.', url, method, data) elif settings.debug.log_data_collector_calls: _logger.debug('Calling data collector with url=%r and method=%r.', url, method) # Compress the serialized JSON being sent as content if over 64KiB # in size. If less than 2MB in size compress for speed. If over # 2MB then compress for smallest size. This parallels what the Ruby # agent does. if len(data) > 64*1024: headers['Content-Encoding'] = 'deflate' level = (len(data) < 2000000) and 1 or 9 internal_metric('Supportability/Collector/ZLIB/Bytes/%s' % method, len(data)) with InternalTrace('Supportability/Collector/ZLIB/Compress/' '%s' % method): data = zlib.compress(six.b(data), level) # If there is no requests session object provided for making # requests create one now. We want to close this as soon as we # are done with it. auto_close_session = False if not session: session = requests.session() auto_close_session = True # The 'requests' library can raise a number of exception derived # from 'RequestException' before we even manage to get a connection # to the data collector. # # The data collector can the generate a number of different types of # HTTP errors for requests. These are: # # 400 Bad Request - For incorrect method type or incorrectly # construct parameters. We should not get this and if we do it would # likely indicate a problem with the implementation of the agent. # # 413 Request Entity Too Large - Where the request content was too # large. The limits on number of nodes in slow transaction traces # should in general prevent this, but not everything has size limits # and so rogue data could still blow things out. Same data is not # going to work later on in a subsequent request, even if aggregated # with other data, so we need to log the details and then flag that # data should be thrown away. # # 415 Unsupported Media Type - This occurs when the JSON which was # sent can't be decoded by the data collector. If this is a true # problem with the JSON formatting, then sending again, even if # aggregated with other data, may not work, so we need to log the # details and then flag that data should be thrown away. # # 503 Service Unavailable - This occurs when data collector, or core # application is being restarted and not in state to be able to # accept requests. It should be a transient issue so should be able # to retain data and try again. internal_metric('Supportability/Collector/Output/Bytes/%s' % method, len(data)) # If audit logging is enabled, log the requests details. log_id = _log_request(url, params, headers, data) try: # The timeout value in the requests module is only on # the initial connection and doesn't apply to how long # it takes to get back a response. cert_loc = certs.where() if settings.debug.disable_certificate_validation: cert_loc = False timeout = settings.agent_limits.data_collector_timeout with warnings.catch_warnings(): warnings.simplefilter("ignore") r = session.post(url, params=params, headers=headers, proxies=proxies, timeout=timeout, data=data, verify=cert_loc) # Read the content now so we can force close the socket # connection if this is a transient session as quickly # as possible. content = r.content except requests.RequestException: if not settings.proxy_host or not settings.proxy_port: _logger.warning('Data collector is not contactable. This can be ' 'because of a network issue or because of the data ' 'collector being restarted. In the event that contact ' 'cannot be made after a period of time then please ' 'report this problem to New Relic support for further ' 'investigation. The error raised was %r.', sys.exc_info()[1]) else: _logger.warning('Data collector is not contactable via the proxy ' 'host %r on port %r with proxy user of %r. This can be ' 'because of a network issue or because of the data ' 'collector being restarted. In the event that contact ' 'cannot be made after a period of time then please ' 'report this problem to New Relic support for further ' 'investigation. The error raised was %r.', settings.proxy_host, settings.proxy_port, settings.proxy_user, sys.exc_info()[1]) raise RetryDataForRequest(str(sys.exc_info()[1])) finally: if auto_close_session: session.close() session = None if r.status_code != 200: _logger.debug('Received a non 200 HTTP response from the data ' 'collector where url=%r, method=%r, license_key=%r, ' 'agent_run_id=%r, params=%r, headers=%r, status_code=%r ' 'and content=%r.', url, method, license_key, agent_run_id, params, headers, r.status_code, content) if r.status_code == 400: _logger.error('Data collector is indicating that a bad ' 'request has been submitted for url %r, headers of %r, ' 'params of %r and payload of %r. Please report this ' 'problem to New Relic support.', url, headers, params, payload) raise DiscardDataForRequest() elif r.status_code == 413: _logger.warning('Data collector is indicating that a request for ' 'method %r was received where the request content size ' 'was over the maximum allowed size limit. The length of ' 'the request content was %d. If this keeps occurring on a ' 'regular basis, please report this problem to New Relic ' 'support for further investigation.', method, len(data)) raise DiscardDataForRequest() elif r.status_code == 415: _logger.warning('Data collector is indicating that it was sent ' 'malformed JSON data for method %r. If this keeps occurring ' 'on a regular basis, please report this problem to New ' 'Relic support for further investigation.', method) if settings.debug.log_malformed_json_data: if headers['Content-Encoding'] == 'deflate': data = zlib.decompress(data) _logger.info('JSON data which was rejected by the data ' 'collector was %r.', data) raise DiscardDataForRequest(content) elif r.status_code == 503: _logger.warning('Data collector is unavailable. This can be a ' 'transient issue because of the data collector or our ' 'core application being restarted. If the issue persists ' 'it can also be indicative of a problem with our servers. ' 'In the event that availability of our servers is not ' 'restored after a period of time then please report this ' 'problem to New Relic support for further investigation.') raise ServerIsUnavailable() elif r.status_code != 200: if not settings.proxy_host or not settings.proxy_port: _logger.warning('An unexpected HTTP response was received from ' 'the data collector of %r for method %r. The payload for ' 'the request was %r. If this issue persists then please ' 'report this problem to New Relic support for further ' 'investigation.', r.status_code, method, payload) else: _logger.warning('An unexpected HTTP response was received from ' 'the data collector of %r for method %r while connecting ' 'via proxy host %r on port %r with proxy user of %r. ' 'The payload for the request was %r. If this issue ' 'persists then please report this problem to New Relic ' 'support for further investigation.', r.status_code, method, settings.proxy_host, settings.proxy_port, settings.proxy_user, payload) raise DiscardDataForRequest() # Log details of response payload for debugging. Use the JSON # encoded value so know that what original encoded value was. duration = time.time() - start if settings.debug.log_data_collector_payloads: _logger.debug('Valid response from data collector after %.2f ' 'seconds with content=%r.', duration, content) elif settings.debug.log_data_collector_calls: _logger.debug('Valid response from data collector after %.2f ' 'seconds.', duration) # If we got this far we should have a legitimate response from the # data collector. The response is JSON so need to decode it. internal_metric('Supportability/Collector/Input/Bytes/%s' % method, len(content)) try: with InternalTrace('Supportability/Collector/JSON/Decode/%s' % method): if six.PY3: content = content.decode('UTF-8') result = json_decode(content) except Exception: _logger.exception('Error decoding data for JSON payload for ' 'method %r with payload of %r. Please report this problem ' 'to New Relic support.', method, content) if settings.debug.log_malformed_json_data: _logger.info('JSON data received from data collector which ' 'could not be decoded was %r.', content) raise DiscardDataForRequest(str(sys.exc_info()[1])) # The decoded JSON can be either for a successful response or an # error. A successful response has a 'return_value' element and on # error an 'exception' element. if log_id is not None: _log_response(log_id, result) if 'return_value' in result: return result['return_value'] error_type = result['exception']['error_type'] message = result['exception']['message'] # Now need to check for server side exceptions. The following # exceptions can occur for abnormal events. _logger.debug('Received an exception from the data collector where ' 'url=%r, method=%r, license_key=%r, agent_run_id=%r, params=%r, ' 'headers=%r, error_type=%r and message=%r', url, method, license_key, agent_run_id, params, headers, error_type, message) if error_type == 'NewRelic::Agent::LicenseException': _logger.error('Data collector is indicating that an incorrect ' 'license key has been supplied by the agent. The value ' 'which was used by the agent is %r. Please correct any ' 'problem with the license key or report this problem to ' 'New Relic support.', license_key) raise DiscardDataForRequest(message) elif error_type == 'NewRelic::Agent::PostTooBigException': _logger.warning('Core application is indicating that a request for ' 'method %r was received where the request content size ' 'was over the maximum allowed size limit. The length of ' 'the request content was %d. If this keeps occurring on a ' 'regular basis, please report this problem to New Relic ' 'support for further investigation.', method, len(data)) raise DiscardDataForRequest(message) # Server side exceptions are also used to inform the agent to # perform certain actions such as restart when server side # configuration has changed for this application or when agent is # being disabled remotely for some reason. if error_type == 'NewRelic::Agent::ForceRestartException': _logger.info('An automatic internal agent restart has been ' 'requested by the data collector for the application ' 'where the agent run was %r. The reason given for the ' 'forced restart is %r.', agent_run_id, message) raise ForceAgentRestart(message) elif error_type == 'NewRelic::Agent::ForceDisconnectException': _logger.critical('Disconnection of the agent has been requested by ' 'the data collector for the application where the ' 'agent run was %r. The reason given for the forced ' 'disconnection is %r. Please contact New Relic support ' 'for further information.', agent_run_id, message) raise ForceAgentDisconnect(message) # We received an unexpected server side error we don't know what # to do with. _logger.warning('An unexpected server error was received from the ' 'data collector for method %r with payload of %r. The error ' 'was of type %r with message %r. If this issue persists ' 'then please report this problem to New Relic support for ' 'further investigation.', method, payload, error_type, message) raise DiscardDataForRequest(message)
def profile_data(self): # Generic profiling sessions have to wait for completion before # reporting data. # # X-ray profile session can send partial profile data on every harvest. if ((self.profiler_type == SessionType.GENERIC) and (self.state == SessionState.RUNNING)): return None # We prune the number of nodes sent if we are over the specified # limit. This is just to avoid having the response be too large # and get rejected by the data collector. settings = global_settings() self._prune_call_trees(settings.agent_limits.thread_profiler_nodes) flat_tree = {} thread_count = 0 for category, bucket in six.iteritems(self.call_buckets): # Only flatten buckets that have data in them. No need to send # empty buckets. if bucket: flat_tree[category] = [x.flatten() for x in bucket.values()] thread_count += len(bucket) # If no profile data was captured for an x-ray session return None # instead of sending an encoded empty data-structure. For a generic # profiler continue to send an empty tree. This can happen on a system # that uses green threads (coroutines), so sending an empty tree marks # the end of a profile session. If we don't send anything then the UI # times out after a very long time (~15mins) which is frustrating for # the customer. if (thread_count == 0) and (self.profiler_type == SessionType.XRAY): return None # Construct the actual final data for sending. The actual call # data is turned into JSON, compressed and then base64 encoded at # this point to cut its size. if settings.debug.log_thread_profile_payload: _logger.debug('Encoding thread profile data where ' 'payload=%r.', flat_tree) json_call_tree = json_encode(flat_tree) level = settings.agent_limits.data_compression_level level = level or zlib.Z_DEFAULT_COMPRESSION encoded_tree = base64.standard_b64encode( zlib.compress(six.b(json_call_tree), level)) if six.PY3: encoded_tree = encoded_tree.decode('Latin-1') profile = [[ self.profile_id, self.start_time_s * 1000, (self.actual_stop_time_s or time.time()) * 1000, self.sample_count, encoded_tree, thread_count, 0, self.xray_id ]] # Reset the data structures to default. For x-ray profile sessions we # report the partial call tree at every harvest cycle. It is required # to reset the data structures to avoid aggregating the call trees # across harvest cycles. self.reset_profile_data() return profile
def browser_timing_middleware(request, response): # Don't do anything if receive a streaming response which # was introduced in Django 1.5. Need to avoid this as there # will be no 'content' attribute. Alternatively there may be # a 'content' attribute which flattens the stream, which if # we access, will break the streaming and/or buffer what is # potentially a very large response in memory contrary to # what user wanted by explicitly using a streaming response # object in the first place. To preserve streaming but still # do RUM insertion, need to move to a WSGI middleware and # deal with how to update the content length. if hasattr(response, 'streaming_content'): return response # Need to be running within a valid web transaction. transaction = current_transaction() if not transaction: return response # Only insert RUM JavaScript headers and footers if enabled # in configuration. if not transaction.settings.rum.enabled: return response if transaction.autorum_disabled: return response # Only possible if the content type is text/html. ctype = response.get('Content-Type', '').lower() if ctype != 'text/html' and not ctype.startswith('text/html;'): return response # Don't risk it if content encoding already set. if response.has_header('Content-Encoding'): return response # No point continuing if header is empty. This can occur if # RUM is not enabled within the UI. It is assumed at this # point that if header is not empty, then footer will be not # empty. We don't want to generate the footer just yet as # want to do that as late as possible so that application # server time in footer is as accurate as possible. In # particular, if the response content is generated on demand # then the flattening of the response could take some time # and we want to track that. We thus generate footer below # at point of insertion. header = transaction.browser_timing_header() if not header: return response header = six.b(header) # Make sure we flatten any content first as it could be # stored as a list of strings in the response object. We # assign it back to the response object to avoid having # multiple copies of the string in memory at the same time # as we progress through steps below. content = response.content response.content = content # Insert the JavaScript. If there is no <head> element we # insert our own containing the JavaScript. If we detect # possibility of IE compatibility mode then we insert # JavaScript at end of the <head> element. In other cases # insert at the start of the <head> element. # # When updating response content we null the original first # to avoid multiple copies in memory when we recompose the # actual response from list of strings. start = content.find(b'<head') end = content.rfind(b'</body>', -1024) if start != -1 and end != -1: offset = content.find(b'</head>', start) if content.find(b'X-UA-Compatible', start, offset) == -1: start = content.find(b'>', start, start+1024) elif offset != -1: start = offset - 1 if start != -1 and start < end: parts = [] parts.append(content[0:start+1]) parts.append(header) parts.append(content[start+1:end]) footer = transaction.browser_timing_footer() footer = six.b(footer) parts.append(footer) parts.append(content[end:]) response.content = b'' content = b''.join(parts) response.content = content elif start == -1 and end != -1: start = content.find(b'<body') if start != -1 and start < end: parts = [] parts.append(content[0:start]) parts.append(b'<head>') parts.append(header) parts.append(b'</head>') parts.append(content[start:end]) footer = transaction.browser_timing_footer() footer = six.b(footer) parts.append(footer) parts.append(content[end:]) response.content = '' content = b''.join(parts) response.content = content response['Content-Length'] = str(len(response.content)) return response
def profile_data(self): # Generic profiling sessions have to wait for completion before # reporting data. # # Xray profile session can send partial profile data on every harvest. if ((self.profiler_type == SessionType.GENERIC) and (self.state == SessionState.RUNNING)): return None # We prune the number of nodes sent if we are over the specified # limit. This is just to avoid having the response be too large # and get rejected by the data collector. settings = global_settings() self._prune_call_trees(settings.agent_limits.thread_profiler_nodes) flat_tree = {} thread_count = 0 for category, bucket in six.iteritems(self.call_buckets): # Only flatten buckets that have data in them. No need to send # empty buckets. if bucket: flat_tree[category] = [x.flatten() for x in bucket.values()] thread_count += len(bucket) # If no profile data was captured for an x-ray session return None # instead of sending an encoded empty data-structure. For a generic # profiler continue to send an empty tree. This can happen on a system # that uses green threads (coroutines), so sending an empty tree marks # the end of a profile session. If we don't send anything then the UI # timesout after a very long time (~15mins) which is frustrating for # the customer. if (thread_count == 0) and (self.profiler_type == SessionType.XRAY): return None # Construct the actual final data for sending. The actual call # data is turned into JSON, compessed and then base64 encoded at # this point to cut its size. if settings.debug.log_thread_profile_payload: _logger.debug('Encoding thread profile data where ' 'payload=%r.', flat_tree) json_call_tree = json_encode(flat_tree) level = settings.agent_limits.data_compression_level level = level or zlib.Z_DEFAULT_COMPRESSION encoded_tree = base64.standard_b64encode( zlib.compress(six.b(json_call_tree), level)) if six.PY3: encoded_tree = encoded_tree.decode('Latin-1') profile = [[self.profile_id, self.start_time_s * 1000, (self.actual_stop_time_s or time.time()) * 1000, self.sample_count, encoded_tree, thread_count, 0, self.xray_id]] # Reset the datastructures to default. For xray profile sessions we # report the partial call tree at every harvest cycle. It is required # to reset the datastructures to avoid aggregating the call trees # across harvest cycles. self.reset_profile_data() return profile
async def send_inject_browser_agent(self, message): if self.pass_through: return await self.send(message) # Store messages in case of an abort self.messages.append(message) message_type = message["type"] if message_type == "http.response.start" and not self.initial_message: headers = list(message.get("headers", ())) if not self.should_insert_html(headers): await self.abort() return message["headers"] = headers self.initial_message = message elif message_type == "http.response.body" and self.initial_message: body = message.get("body", b"") self.more_body = message.get("more_body", False) # Add this message to the current body self.body += body # if there's a valid body string, attempt to insert the HTML if verify_body_exists(self.body): header = self.transaction.browser_timing_header() if not header: # If there's no header, abort browser monitoring injection await self.send_buffered() return footer = self.transaction.browser_timing_footer() browser_agent_data = six.b(header) + six.b(footer) body = insert_html_snippet(self.body, lambda: browser_agent_data, self.search_maximum) # If we have inserted the browser agent if len(body) != len(self.body): # check to see if we have to modify the content-length # header headers = self.initial_message["headers"] for header_index, header_data in enumerate(headers): header_name, header_value = header_data if header_name.lower() == b"content-length": break else: header_value = None try: content_length = int(header_value) except ValueError: # Invalid content length results in an abort await self.send_buffered() return if content_length is not None: delta = len(body) - len(self.body) headers[header_index] = ( b"content-length", str(content_length + delta).encode("utf-8"), ) # Body is found and modified so we can now send the # modified data and stop searching self.body = body await self.send_buffered() return # 1. Body is found but not modified # 2. Body is not found # No more body if not self.more_body: await self.send_buffered() # We have hit our search limit elif len(self.body) >= self.search_maximum: await self.send_buffered() # Protocol error, unexpected message: abort else: await self.abort()
def transaction_trace_data(self, connections): """Returns a list of slow transaction data collected during the reporting period. """ _logger.debug('Generating transaction trace data.') if not self.__settings: return [] # Create a set 'traces' that is a union of slow transaction, # browser_transactions and xray_transactions. This ensures we don't # send duplicates of a transaction. traces = set() if self.__slow_transaction: traces.add(self.__slow_transaction) traces.update(self.__browser_transactions) traces.update(self.__xray_transactions) # Return an empty list if no transactions were captured. if not traces: return [] # We want to limit the number of explain plans we do across # these. So work out what were the slowest and tag them. # Later the explain plan will only be run on those which are # tagged. agent_limits = self.__settings.agent_limits explain_plan_limit = agent_limits.sql_explain_plans_per_harvest maximum_nodes = agent_limits.transaction_traces_nodes database_nodes = [] if explain_plan_limit != 0: for trace in traces: for node in trace.slow_sql: # Make sure we clear any flag for explain plans on # the nodes in case a transaction trace was merged # in from previous harvest period. node.generate_explain_plan = False # Node should be excluded if not for an operation # that we can't do an explain plan on. Also should # not be one which would not be included in the # transaction trace because limit was reached. if (node.node_count < maximum_nodes and node.connect_params and node.statement.operation in node.statement.database.explain_stmts): database_nodes.append(node) database_nodes = sorted(database_nodes, key=lambda x: x.duration)[-explain_plan_limit:] for node in database_nodes: node.generate_explain_plan = True else: for trace in traces: for node in trace.slow_sql: node.generate_explain_plan = True database_nodes.append(node) # Now generate the transaction traces. We need to cap the # number of nodes capture to the specified limit. trace_data = [] for trace in traces: transaction_trace = trace.transaction_trace( self, maximum_nodes, connections) internal_metric('Supportability/StatsEngine/Counts/' 'transaction_sample_data', trace.trace_node_count) data = [transaction_trace, list(trace.string_table.values())] if self.__settings.debug.log_transaction_trace_payload: _logger.debug('Encoding slow transaction data where ' 'payload=%r.', data) with InternalTrace('Supportability/StatsEngine/JSON/Encode/' 'transaction_sample_data'): json_data = json_encode(data) internal_metric('Supportability/StatsEngine/ZLIB/Bytes/' 'transaction_sample_data', len(json_data)) with InternalTrace('Supportability/StatsEngine/ZLIB/Compress/' 'transaction_sample_data'): zlib_data = zlib.compress(six.b(json_data)) with InternalTrace('Supportability/StatsEngine/BASE64/Encode/' 'transaction_sample_data'): pack_data = base64.standard_b64encode(zlib_data) if six.PY3: pack_data = pack_data.decode('Latin-1') root = transaction_trace.root xray_id = getattr(trace, 'xray_id', None) if (xray_id or trace.rum_trace or trace.record_tt): force_persist = True else: force_persist = False trace_data.append([root.start_time, root.end_time - root.start_time, trace.path, trace.request_uri, pack_data, trace.guid, None, force_persist, xray_id,]) return trace_data
def transaction_trace_data(self): """Returns a list of slow transaction data collected during the reporting period. """ if not self.__settings: return [] # Create a set 'traces' that is a union of slow transaction, # browser_transactions and xray_transactions. This ensures we don't # send duplicates of a transaction. traces = set() if self.__slow_transaction: traces.add(self.__slow_transaction) traces.update(self.__browser_transactions) traces.update(self.__xray_transactions) # Return an empty list if no transactions were captured. if not traces: return [] trace_data = [] maximum = self.__settings.agent_limits.transaction_traces_nodes for trace in traces: transaction_trace = trace.transaction_trace(self, maximum) internal_metric('Supportability/StatsEngine/Counts/' 'transaction_sample_data', trace.trace_node_count) data = [transaction_trace, list(trace.string_table.values())] if self.__settings.debug.log_transaction_trace_payload: _logger.debug('Encoding slow transaction data where ' 'payload=%r.', data) with InternalTrace('Supportability/StatsEngine/JSON/Encode/' 'transaction_sample_data'): json_data = simplejson.dumps(data, ensure_ascii=True, encoding='Latin-1', namedtuple_as_object=False, default=lambda o: list(iter(o))) internal_metric('Supportability/StatsEngine/ZLIB/Bytes/' 'transaction_sample_data', len(json_data)) with InternalTrace('Supportability/StatsEngine/ZLIB/Compress/' 'transaction_sample_data'): zlib_data = zlib.compress(six.b(json_data)) with InternalTrace('Supportability/StatsEngine/BASE64/Encode/' 'transaction_sample_data'): pack_data = base64.standard_b64encode(zlib_data) root = transaction_trace.root xray_id = getattr(trace, 'xray_id', None) if (xray_id or trace.rum_trace or trace.record_tt): force_persist = True else: force_persist = False trace_data.append([root.start_time, root.end_time - root.start_time, trace.path, trace.request_uri, pack_data, trace.guid, None, force_persist, xray_id,]) return trace_data
def slow_sql_data(self, connections): _logger.debug('Generating slow SQL data.') if not self.__settings: return [] if not self.__sql_stats_table: return [] if not self.__settings.slow_sql.enabled: return [] maximum = self.__settings.agent_limits.slow_sql_data slow_sql_nodes = sorted(six.itervalues(self.__sql_stats_table), key=lambda x: x.max_call_time)[-maximum:] result = [] for stats_node in slow_sql_nodes: params = {} slow_sql_node = stats_node.slow_sql_node if slow_sql_node.stack_trace: params['backtrace'] = slow_sql_node.stack_trace explain_plan_data = explain_plan(connections, slow_sql_node.statement, slow_sql_node.connect_params, slow_sql_node.cursor_params, slow_sql_node.sql_parameters, slow_sql_node.execute_params, slow_sql_node.sql_format) if explain_plan_data: params['explain_plan'] = explain_plan_data json_data = json_encode(params) params_data = base64.standard_b64encode( zlib.compress(six.b(json_data))) if six.PY3: params_data = params_data.decode('Latin-1') # Limit the length of any SQL that is reported back. limit = self.__settings.agent_limits.sql_query_length_maximum sql = slow_sql_node.formatted[:limit] data = [slow_sql_node.path, slow_sql_node.request_uri, slow_sql_node.identifier, sql, slow_sql_node.metric, stats_node.call_count, stats_node.total_call_time * 1000, stats_node.min_call_time * 1000, stats_node.max_call_time * 1000, params_data] result.append(data) return result
def profile_data(self): # Generic profiling sessions have to wait for completion before # reporting data. # # Xray profile session can send partial profile data on every harvest. if ((self.profiler_type == SessionType.GENERIC) and (self.state == SessionState.RUNNING)): return None # We prune the number of nodes sent if we are over the specified # limit. This is just to avoid having the response be too large # and get rejected by the data collector. settings = global_settings() self._prune_call_trees(settings.agent_limits.thread_profiler_nodes) flat_tree = {} thread_count = 0 for category, bucket in six.iteritems(self.call_buckets): # Only flatten buckets that have data in them. No need to send # empty buckets. if bucket: flat_tree[category] = [x.flatten() for x in bucket.values()] thread_count += len(bucket) # If no profile data was captured return None instead of sending an # encoded empty data-structure if thread_count == 0: return None # Construct the actual final data for sending. The actual call # data is turned into JSON, compessed and then base64 encoded at # this point to cut its size. _logger.debug('Returning partial thread profiling data ' 'for %d transactions with name %r and xray ID of ' '%r over a period of %.2f seconds and %d samples.', self.transaction_count, self.key_txn, self.xray_id, time.time()-self.start_time_s, self.sample_count) if settings.debug.log_thread_profile_payload: _logger.debug('Encoding thread profile data where ' 'payload=%r.', flat_tree) json_call_tree = simplejson.dumps(flat_tree, ensure_ascii=True, encoding='Latin-1', namedtuple_as_object=False) encoded_tree = base64.standard_b64encode( zlib.compress(six.b(json_call_tree))) profile = [[self.profile_id, self.start_time_s*1000, (self.actual_stop_time_s or time.time()) * 1000, self.sample_count, encoded_tree, thread_count, 0, self.xray_id]] # Reset the datastructures to default. For xray profile sessions we # report the partial call tree at every harvest cycle. It is required # to reset the datastructures to avoid aggregating the call trees # across harvest cycles. self.reset_profile_data() return profile
def profile_data(self): # Generic profiling sessions have to wait for completion before # reporting data. # # Xray profile session can send partial profile data on every harvest. if ((self.profiler_type == SessionType.GENERIC) and (self.state == SessionState.RUNNING)): return None # We prune the number of nodes sent if we are over the specified # limit. This is just to avoid having the response be too large # and get rejected by the data collector. settings = global_settings() self._prune_call_trees(settings.agent_limits.thread_profiler_nodes) flat_tree = {} thread_count = 0 for category, bucket in six.iteritems(self.call_buckets): # Only flatten buckets that have data in them. No need to send # empty buckets. if bucket: flat_tree[category] = [x.flatten() for x in bucket.values()] thread_count += len(bucket) # If no profile data was captured return None instead of sending an # encoded empty data-structure if thread_count == 0: return None # Construct the actual final data for sending. The actual call # data is turned into JSON, compessed and then base64 encoded at # this point to cut its size. _logger.debug( 'Returning partial thread profiling data ' 'for %d transactions with name %r and xray ID of ' '%r over a period of %.2f seconds and %d samples.', self.transaction_count, self.key_txn, self.xray_id, time.time() - self.start_time_s, self.sample_count) if settings.debug.log_thread_profile_payload: _logger.debug('Encoding thread profile data where ' 'payload=%r.', flat_tree) json_call_tree = simplejson.dumps(flat_tree, ensure_ascii=True, encoding='Latin-1', namedtuple_as_object=False) encoded_tree = base64.standard_b64encode( zlib.compress(six.b(json_call_tree))) profile = [[ self.profile_id, self.start_time_s * 1000, (self.actual_stop_time_s or time.time()) * 1000, self.sample_count, encoded_tree, thread_count, 0, self.xray_id ]] # Reset the datastructures to default. For xray profile sessions we # report the partial call tree at every harvest cycle. It is required # to reset the datastructures to avoid aggregating the call trees # across harvest cycles. self.reset_profile_data() return profile