Example #1
0
        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)
Example #2
0
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
Example #3
0
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
Example #4
0
    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))))
Example #5
0
    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))))
Example #6
0
    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
Example #7
0
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
Example #9
0
    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
Example #10
0
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))
Example #11
0
    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
Example #12
0
    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
Example #16
0
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)
Example #17
0
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 html_to_be_inserted():
     return six.b(header) + six.b(transaction.browser_timing_footer())
    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
Example #20
0
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
Example #22
0
    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()
Example #23
0
    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
Example #25
0
    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
Example #26
0
    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