diff --git a/newrelic.py b/newrelic.py index 02eb17f..9ca2b86 100644 --- a/newrelic.py +++ b/newrelic.py @@ -1,5 +1,6 @@ import requests import logging +import sys from time import time from urllib import urlencode @@ -14,10 +15,9 @@ logger = logging.getLogger(__name__) - class Client(object): """A Client for interacting with New Relic resources""" - def __init__(self, account_id=None, api_key=None, proxy=None, retries=3, retry_delay=1, timeout=1.000): + def __init__(self, account_id=None, api_key=None, proxy=None, retries=3, retry_delay=1, timeout=1.000, debug=False): """ Create a NewRelic REST API client TODO: implement proxy support @@ -41,41 +41,48 @@ def __init__(self, account_id=None, api_key=None, proxy=None, retries=3, retry_d self.retries = retries self.retry_delay = retry_delay self.timeout = timeout + self.debug = debug + if self.debug is True: + self.config = {'verbose': sys.stderr} + else: + self.config = {} def _make_request(self, request, uri, **kwargs): - attempts = 0 + attempts = 1 response = None - while attempts < self.retries: + while attempts <= self.retries: try: - response = request(uri, **kwargs) + response = request(uri, config=self.config, **kwargs) except (requests.ConnectionError, requests.HTTPError) as ce: logger.error('Error connecting to New Relic API: {}'.format(ce)) sleep(self.retry_delay) attempts += 1 else: break + if not response and attempts > 1: + raise NewRelicApiException('Unable to connect to the NewRelic API after {} attempts'.format(attempts)) if not response: - raise NewRelicApiException - if not str(response.status_code).startswith('2') : + raise NewRelicApiException('No response received from NewRelic API') + if not str(response.status_code).startswith('2'): self._handle_api_error(response.status_code) return self._parse_xml(response.text) def _parse_xml(self, response): parser = etree.XMLParser(remove_blank_text=True, strip_cdata=False, ns_clean=True, recover=True) if response.startswith(''): - response = ''.join(response.split('\n')[1:]) + response = '\n'.join(response.split('\n')[1:]) tree = etree.XML(response, parser) return tree - def _handle_api_error(self, status_code): + def _handle_api_error(self, status_code, error_message): if 403 == status_code: - raise NewRelicInvalidApiKeyException + raise NewRelicInvalidApiKeyException(error_message) elif 404 == status_code: - raise NewRelicUnknownApplicationException + raise NewRelicUnknownApplicationException(error_message) elif 422 == status_code: - raise NewRelicInvalidParameterException + raise NewRelicInvalidParameterException(error_message) else: - raise NewRelicApiException + raise NewRelicApiException(error_message) def _make_get_request(self, uri, parameters=None, timeout=None): @@ -98,7 +105,8 @@ def _api_rate_limit_exceeded(self, api_call, window=60): """ We want to keep track of the last time we sent a request to the NewRelic API, but only for certain operations. This method will dynamically add an attribute to the Client class with a unix timestamp with the name of the API api_call - we make so that we can check it later. + we make so that we can check it later. We return the amount of time until we can perform another API call so that appropriate waiting + can be implemented. """ current_call_time = int(time()) try: @@ -111,7 +119,7 @@ def _api_rate_limit_exceeded(self, api_call, window=60): setattr(self, api_call.__name__ + ".window", current_call_time) return False else: - return True + return window - (current_call_time - previous_call_time) def view_applications(self): @@ -178,8 +186,8 @@ def get_metric_names(self, app_id, re=None, limit=5000): Errors: 403 Invalid API Key, 422 Invalid Parameters Endpoint: api.newrelic.com """ - if self._api_rate_limit_exceeded(self.get_metric_names): - raise NewRelicApiRateLimitException + if self._api_rate_limit_exceeded(self.get_metric_data): + raise NewRelicApiRateLimitException(str(self._api_rate_limit_exceeded(self.get_metric_data))) parameters = {'re': re, 'limit': limit} @@ -195,22 +203,25 @@ def get_metric_names(self, app_id, re=None, limit=5000): metrics[metric.get('name')] = fields return metrics - def get_metric_data(self, applications, metrics, fields, start_time, end_time, summary=False): + def get_metric_data(self, applications, metrics, field, begin, end, summary=False): """ - Requires: account ID, list of application IDs, list of metrics, metric fields, begin_time, end_time + Requires: account ID, list of application IDs, list of metrics, metric fields, begin, end Method: Get Endpoint: api.newrelic.com Restrictions: Rate limit to 1x per minute Errors: 403 Invalid API key, 422 Invalid Parameters - Returns: A list of tuples, (app name, begin_time, end_time, metric_name, [(field name, value),...]) + Returns: A list of metric objects, each will have information about its start/end time, application, metric name and + any associated values """ + # Make sure we aren't going to hit an API timeout if self._api_rate_limit_exceeded(self.get_metric_data): - raise NewRelicApiRateLimitException + raise NewRelicApiRateLimitException(str(self._api_rate_limit_exceeded(self.get_metric_data))) - parameters = {} + # Just in case the API needs parameters to be in order + parameters = OrderedDict() - # Figure out what we were passed and set out parameter correctly + # Figure out what we were passed and set our parameter correctly try: int(applications[0]) except ValueError: @@ -222,54 +233,57 @@ def get_metric_data(self, applications, metrics, fields, start_time, end_time, s app_string = app_string + "[]" # Set our parameters - for app in applications: - parameters[app_string] = app + parameters[app_string] = applications - for metric in metrics: - parameters['metrics[]'] = metric + parameters['metrics[]'] = metrics - for field in fields: - parameters['field'] = field + parameters['field'] = field - parameters['begin'] = begin_time - parameters['end'] = end_time + parameters['begin'] = begin + parameters['end'] = end parameters['summary'] = int(summary) - uri = "https://api.newrelic.com/api/v1/accounts/{0}/metrics/data.xml".format(str(app_id)) + uri = "https://api.newrelic.com/api/v1/accounts/{0}/metrics/data.xml".format(str(self.account_id)) # A longer timeout is needed due to the amount of data that can be returned response = self._make_get_request(uri, parameters=parameters, timeout=5.000) + # Parsing our response + returned_metrics = [] + for metric in response.xpath('/metrics/metric'): + m = Metric(metric) + returned_metrics.append(m) + return returned_metrics # Exceptions class NewRelicApiException(Exception): - def __init__(self): + def __init__(self, message): super(NewRelicApiException, self).__init__() - pass + print message class NewRelicInvalidApiKeyException(NewRelicApiException): - def __init__(self): - super(NewRelicInvalidApiKeyException, self).__init__() + def __init__(self, message): + super(NewRelicInvalidApiKeyException, self).__init__(message) pass class NewRelicCredentialException(NewRelicApiException): - def __init__(self): - super(NewRelicCredentialException, self).__init__() + def __init__(self, message): + super(NewRelicCredentialException, self).__init__(message) pass class NewRelicInvalidParameterException(NewRelicApiException): - def __init__(self): - super(NewRelicInvalidParameterException, self).__init__() + def __init__(self, message): + super(NewRelicInvalidParameterException, self).__init__(message) pass class NewRelicUnknownApplicationException(NewRelicApiException): - def __init__(self): - super(NewRelicUnknownApplicationException, self).__init__() + def __init__(self, message): + super(NewRelicUnknownApplicationException, self).__init__(message) pass class NewRelicApiRateLimitException(NewRelicApiException): - def __init__(self, arg): - super(NewRelicApiRateLimitException, self).__init__() + def __init__(self, message): + super(NewRelicApiRateLimitException, self).__init__(message) pass # Data Classes @@ -280,3 +294,15 @@ def __init__(self, properties): self.name = properties['name'] self.app_id = properties['id'] self.url = properties['overview-url'] + +class Metric(object): + def __init__(self, metric): + super(Metric, self).__init__() + for k,v in metric.items(): + setattr(self, k, v) + for field in metric.xpath('field'): + # Each field has a 'name=metric_type' section. We want to have this accessible in the object by calling the + # metric_type property of the object directly + setattr(self, field.values()[0], field.text) + +