PageRenderTime 53ms CodeModel.GetById 16ms RepoModel.GetById 0ms app.codeStats 0ms

/newrelic_plugin_agent/agent.py

https://gitlab.com/Blueprint-Marketing/newrelic-plugin-agent
Python | 339 lines | 209 code | 42 blank | 88 comment | 39 complexity | b69745639067d52d72ca648e5d4eba7a MD5 | raw file
  1. """
  2. Multiple Plugin Agent for the New Relic Platform
  3. """
  4. import helper
  5. import importlib
  6. import json
  7. import logging
  8. import os
  9. import requests
  10. import socket
  11. import sys
  12. import Queue as queue
  13. import threading
  14. import time
  15. from newrelic_plugin_agent import __version__
  16. from newrelic_plugin_agent import plugins
  17. LOGGER = logging.getLogger(__name__)
  18. class NewRelicPluginAgent(helper.Controller):
  19. """The NewRelicPluginAgent class implements a agent that polls plugins
  20. every minute and reports the state to NewRelic.
  21. """
  22. IGNORE_KEYS = ['license_key', 'proxy', 'endpoint',
  23. 'poll_interval', 'wake_interval']
  24. MAX_METRICS_PER_REQUEST = 10000
  25. PLATFORM_URL = 'https://platform-api.newrelic.com/platform/v1/metrics'
  26. WAKE_INTERVAL = 60
  27. def __init__(self, args, operating_system):
  28. """Initialize the NewRelicPluginAgent object.
  29. :param argparse.Namespace args: Command line arguments
  30. :param str operating_system: The operating_system name
  31. """
  32. super(NewRelicPluginAgent, self).__init__(args, operating_system)
  33. self.derive_last_interval = dict()
  34. self.endpoint = self.PLATFORM_URL
  35. self.http_headers = {'Accept': 'application/json',
  36. 'Content-Type': 'application/json'}
  37. self.last_interval_start = None
  38. self.min_max_values = dict()
  39. self._wake_interval = (self.config.application.get('wake_interval') or
  40. self.config.application.get('poll_interval') or
  41. self.WAKE_INTERVAL)
  42. self.next_wake_interval = int(self._wake_interval)
  43. self.publish_queue = queue.Queue()
  44. self.threads = list()
  45. info = tuple([__version__] + list(self.system_platform))
  46. LOGGER.info('Agent v%s initialized, %s %s v%s', *info)
  47. def setup(self):
  48. """Setup the internal state for the controller class. This is invoked
  49. on Controller.run().
  50. Items requiring the configuration object should be assigned here due to
  51. startup order of operations.
  52. """
  53. if hasattr(self.config.application, 'endpoint'):
  54. self.endpoint = self.config.application.endpoint
  55. self.http_headers['X-License-Key'] = self.license_key
  56. self.last_interval_start = time.time()
  57. @property
  58. def agent_data(self):
  59. """Return the agent data section of the NewRelic Platform data payload
  60. :rtype: dict
  61. """
  62. return {'host': socket.gethostname(),
  63. 'pid': os.getpid(),
  64. 'version': __version__}
  65. @property
  66. def license_key(self):
  67. """Return the NewRelic license key from the configuration values.
  68. :rtype: str
  69. """
  70. return self.config.application.license_key
  71. def poll_plugin(self, plugin_name, plugin, config):
  72. """Kick off a background thread to run the processing task.
  73. :param newrelic_plugin_agent.plugins.base.Plugin plugin: The plugin
  74. :param dict config: The config for the plugin
  75. """
  76. if not isinstance(config, (list, tuple)):
  77. config = [config]
  78. for instance in config:
  79. thread = threading.Thread(target=self.thread_process,
  80. kwargs={'config': instance,
  81. 'name': plugin_name,
  82. 'plugin': plugin,
  83. 'poll_interval':
  84. int(self._wake_interval)})
  85. thread.run()
  86. self.threads.append(thread)
  87. def process(self):
  88. """This method is called after every sleep interval. If the intention
  89. is to use an IOLoop instead of sleep interval based daemon, override
  90. the run method.
  91. """
  92. start_time = time.time()
  93. self.start_plugin_polling()
  94. # Sleep for a second while threads are running
  95. while self.threads_running:
  96. time.sleep(1)
  97. self.threads = list()
  98. self.send_data_to_newrelic()
  99. duration = time.time() - start_time
  100. self.next_wake_interval = self._wake_interval - duration
  101. if self.next_wake_interval < 1:
  102. LOGGER.warning('Poll interval took greater than %i seconds',
  103. duration)
  104. self.next_wake_interval = int(self._wake_interval)
  105. LOGGER.info('Stats processed in %.2f seconds, next wake in %i seconds',
  106. duration, self.next_wake_interval)
  107. def process_min_max_values(self, component):
  108. """Agent keeps track of previous values, so compute the differences for
  109. min/max values.
  110. :param dict component: The component to calc min/max values for
  111. """
  112. guid = component['guid']
  113. name = component['name']
  114. if guid not in self.min_max_values.keys():
  115. self.min_max_values[guid] = dict()
  116. if name not in self.min_max_values[guid].keys():
  117. self.min_max_values[guid][name] = dict()
  118. for metric in component['metrics']:
  119. min_val, max_val = self.min_max_values[guid][name].get(metric,
  120. (None, None))
  121. value = component['metrics'][metric]['total']
  122. if min_val is not None and min_val > value:
  123. min_val = value
  124. if max_val is None or max_val < value:
  125. max_val = value
  126. if component['metrics'][metric]['min'] is None:
  127. component['metrics'][metric]['min'] = min_val or value
  128. if component['metrics'][metric]['max'] is None:
  129. component['metrics'][metric]['max'] = max_val
  130. self.min_max_values[guid][name][metric] = min_val, max_val
  131. @property
  132. def proxies(self):
  133. """Return the proxy used to access NewRelic.
  134. :rtype: dict
  135. """
  136. if 'proxy' in self.config.application:
  137. return {
  138. 'http': self.config.application['proxy'],
  139. 'https': self.config.application['proxy']
  140. }
  141. return None
  142. def send_data_to_newrelic(self):
  143. metrics = 0
  144. components = list()
  145. while self.publish_queue.qsize():
  146. (name, data, last_values) = self.publish_queue.get()
  147. self.derive_last_interval[name] = last_values
  148. if isinstance(data, list):
  149. for component in data:
  150. self.process_min_max_values(component)
  151. metrics += len(component['metrics'].keys())
  152. if metrics >= self.MAX_METRICS_PER_REQUEST:
  153. self.send_components(components, metrics)
  154. components = list()
  155. metrics = 0
  156. components.append(component)
  157. elif isinstance(data, dict):
  158. self.process_min_max_values(data)
  159. metrics += len(data['metrics'].keys())
  160. if metrics >= self.MAX_METRICS_PER_REQUEST:
  161. self.send_components(components, metrics)
  162. components = list()
  163. metrics = 0
  164. components.append(data)
  165. LOGGER.debug('Done, will send remainder of %i metrics', metrics)
  166. self.send_components(components, metrics)
  167. def send_components(self, components, metrics):
  168. """Create the headers and payload to send to NewRelic platform as a
  169. JSON encoded POST body.
  170. """
  171. if not metrics:
  172. LOGGER.warning('No metrics to send to NewRelic this interval')
  173. return
  174. LOGGER.info('Sending %i metrics to NewRelic', metrics)
  175. body = {'agent': self.agent_data, 'components': components}
  176. LOGGER.debug(body)
  177. try:
  178. response = requests.post(self.endpoint,
  179. headers=self.http_headers,
  180. proxies=self.proxies,
  181. data=json.dumps(body, ensure_ascii=False),
  182. verify=self.config.get('verify_ssl_cert',
  183. True))
  184. LOGGER.debug('Response: %s: %r',
  185. response.status_code,
  186. response.content.strip())
  187. except requests.ConnectionError as error:
  188. LOGGER.error('Error reporting stats: %s', error)
  189. @staticmethod
  190. def _get_plugin(plugin_path):
  191. """Given a qualified class name (eg. foo.bar.Foo), return the class
  192. :rtype: object
  193. """
  194. try:
  195. package, class_name = plugin_path.rsplit('.', 1)
  196. except ValueError:
  197. return None
  198. try:
  199. module_handle = importlib.import_module(package)
  200. class_handle = getattr(module_handle, class_name)
  201. return class_handle
  202. except ImportError:
  203. LOGGER.exception('Attempting to import %s', plugin_path)
  204. return None
  205. def start_plugin_polling(self):
  206. """Iterate through each plugin and start the polling process."""
  207. for plugin in [key for key in self.config.application.keys()
  208. if key not in self.IGNORE_KEYS]:
  209. LOGGER.info('Enabling plugin: %s', plugin)
  210. plugin_class = None
  211. # If plugin is part of the core agent plugin list
  212. if plugin in plugins.available:
  213. plugin_class = self._get_plugin(plugins.available[plugin])
  214. # If plugin is in config and a qualified class name
  215. elif '.' in plugin:
  216. plugin_class = self._get_plugin(plugin)
  217. # If plugin class could not be imported
  218. if not plugin_class:
  219. LOGGER.error('Enabled plugin %s not available', plugin)
  220. continue
  221. self.poll_plugin(plugin, plugin_class,
  222. self.config.application.get(plugin))
  223. @property
  224. def threads_running(self):
  225. """Return True if any of the child threads are alive
  226. :rtype: bool
  227. """
  228. for thread in self.threads:
  229. if thread.is_alive():
  230. return True
  231. return False
  232. def thread_process(self, name, plugin, config, poll_interval):
  233. """Created a thread process for the given name, plugin class,
  234. config and poll interval. Process is added to a Queue object which
  235. used to maintain the stack of running plugins.
  236. :param str name: The name of the plugin
  237. :param newrelic_plugin_agent.plugin.Plugin plugin: The plugin class
  238. :param dict config: The plugin configuration
  239. :param int poll_interval: How often the plugin is invoked
  240. """
  241. instance_name = "%s:%s" % (name, config.get('name', 'unnamed'))
  242. obj = plugin(config, poll_interval,
  243. self.derive_last_interval.get(instance_name))
  244. obj.poll()
  245. self.publish_queue.put((instance_name, obj.values(),
  246. obj.derive_last_interval))
  247. @property
  248. def wake_interval(self):
  249. """Return the wake interval in seconds as the number of seconds
  250. until the next minute.
  251. :rtype: int
  252. """
  253. return self.next_wake_interval
  254. def main():
  255. helper.parser.description('The NewRelic Plugin Agent polls various '
  256. 'services and sends the data to the NewRelic '
  257. 'Platform')
  258. helper.parser.name('newrelic_plugin_agent')
  259. argparse = helper.parser.get()
  260. argparse.add_argument('-C',
  261. action='store_true',
  262. dest='configure',
  263. help='Run interactive configuration')
  264. args = helper.parser.parse()
  265. if args.configure:
  266. print('Configuration')
  267. sys.exit(0)
  268. helper.start(NewRelicPluginAgent)
  269. if __name__ == '__main__':
  270. logging.basicConfig(level=logging.DEBUG)
  271. main()