haproxy.chart.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368
  1. # -*- coding: utf-8 -*-
  2. # Description: haproxy netdata python.d module
  3. # Author: ilyam8, ktarasz
  4. # SPDX-License-Identifier: GPL-3.0-or-later
  5. from collections import defaultdict
  6. from re import compile as re_compile
  7. try:
  8. from urlparse import urlparse
  9. except ImportError:
  10. from urllib.parse import urlparse
  11. from bases.FrameworkServices.SocketService import SocketService
  12. from bases.FrameworkServices.UrlService import UrlService
  13. # charts order (can be overridden if you want less charts, or different order)
  14. ORDER = [
  15. 'fbin',
  16. 'fbout',
  17. 'fscur',
  18. 'fqcur',
  19. 'fhrsp_1xx',
  20. 'fhrsp_2xx',
  21. 'fhrsp_3xx',
  22. 'fhrsp_4xx',
  23. 'fhrsp_5xx',
  24. 'fhrsp_other',
  25. 'fhrsp_total',
  26. 'bbin',
  27. 'bbout',
  28. 'bscur',
  29. 'bqcur',
  30. 'bhrsp_1xx',
  31. 'bhrsp_2xx',
  32. 'bhrsp_3xx',
  33. 'bhrsp_4xx',
  34. 'bhrsp_5xx',
  35. 'bhrsp_other',
  36. 'bhrsp_total',
  37. 'bqtime',
  38. 'bttime',
  39. 'brtime',
  40. 'bctime',
  41. 'health_sup',
  42. 'health_sdown',
  43. 'health_smaint',
  44. 'health_bdown',
  45. 'health_idle'
  46. ]
  47. CHARTS = {
  48. 'fbin': {
  49. 'options': [None, 'Kilobytes In', 'KiB/s', 'frontend', 'haproxy_f.bin', 'line'],
  50. 'lines': []
  51. },
  52. 'fbout': {
  53. 'options': [None, 'Kilobytes Out', 'KiB/s', 'frontend', 'haproxy_f.bout', 'line'],
  54. 'lines': []
  55. },
  56. 'fscur': {
  57. 'options': [None, 'Sessions Active', 'sessions', 'frontend', 'haproxy_f.scur', 'line'],
  58. 'lines': []
  59. },
  60. 'fqcur': {
  61. 'options': [None, 'Session In Queue', 'sessions', 'frontend', 'haproxy_f.qcur', 'line'],
  62. 'lines': []
  63. },
  64. 'fhrsp_1xx': {
  65. 'options': [None, 'HTTP responses with 1xx code', 'responses/s', 'frontend', 'haproxy_f.hrsp_1xx', 'line'],
  66. 'lines': []
  67. },
  68. 'fhrsp_2xx': {
  69. 'options': [None, 'HTTP responses with 2xx code', 'responses/s', 'frontend', 'haproxy_f.hrsp_2xx', 'line'],
  70. 'lines': []
  71. },
  72. 'fhrsp_3xx': {
  73. 'options': [None, 'HTTP responses with 3xx code', 'responses/s', 'frontend', 'haproxy_f.hrsp_3xx', 'line'],
  74. 'lines': []
  75. },
  76. 'fhrsp_4xx': {
  77. 'options': [None, 'HTTP responses with 4xx code', 'responses/s', 'frontend', 'haproxy_f.hrsp_4xx', 'line'],
  78. 'lines': []
  79. },
  80. 'fhrsp_5xx': {
  81. 'options': [None, 'HTTP responses with 5xx code', 'responses/s', 'frontend', 'haproxy_f.hrsp_5xx', 'line'],
  82. 'lines': []
  83. },
  84. 'fhrsp_other': {
  85. 'options': [None, 'HTTP responses with other codes (protocol error)', 'responses/s', 'frontend',
  86. 'haproxy_f.hrsp_other', 'line'],
  87. 'lines': []
  88. },
  89. 'fhrsp_total': {
  90. 'options': [None, 'HTTP responses', 'responses', 'frontend', 'haproxy_f.hrsp_total', 'line'],
  91. 'lines': []
  92. },
  93. 'bbin': {
  94. 'options': [None, 'Kilobytes In', 'KiB/s', 'backend', 'haproxy_b.bin', 'line'],
  95. 'lines': []
  96. },
  97. 'bbout': {
  98. 'options': [None, 'Kilobytes Out', 'KiB/s', 'backend', 'haproxy_b.bout', 'line'],
  99. 'lines': []
  100. },
  101. 'bscur': {
  102. 'options': [None, 'Sessions Active', 'sessions', 'backend', 'haproxy_b.scur', 'line'],
  103. 'lines': []
  104. },
  105. 'bqcur': {
  106. 'options': [None, 'Sessions In Queue', 'sessions', 'backend', 'haproxy_b.qcur', 'line'],
  107. 'lines': []
  108. },
  109. 'bhrsp_1xx': {
  110. 'options': [None, 'HTTP responses with 1xx code', 'responses/s', 'backend', 'haproxy_b.hrsp_1xx', 'line'],
  111. 'lines': []
  112. },
  113. 'bhrsp_2xx': {
  114. 'options': [None, 'HTTP responses with 2xx code', 'responses/s', 'backend', 'haproxy_b.hrsp_2xx', 'line'],
  115. 'lines': []
  116. },
  117. 'bhrsp_3xx': {
  118. 'options': [None, 'HTTP responses with 3xx code', 'responses/s', 'backend', 'haproxy_b.hrsp_3xx', 'line'],
  119. 'lines': []
  120. },
  121. 'bhrsp_4xx': {
  122. 'options': [None, 'HTTP responses with 4xx code', 'responses/s', 'backend', 'haproxy_b.hrsp_4xx', 'line'],
  123. 'lines': []
  124. },
  125. 'bhrsp_5xx': {
  126. 'options': [None, 'HTTP responses with 5xx code', 'responses/s', 'backend', 'haproxy_b.hrsp_5xx', 'line'],
  127. 'lines': []
  128. },
  129. 'bhrsp_other': {
  130. 'options': [None, 'HTTP responses with other codes (protocol error)', 'responses/s', 'backend',
  131. 'haproxy_b.hrsp_other', 'line'],
  132. 'lines': []
  133. },
  134. 'bhrsp_total': {
  135. 'options': [None, 'HTTP responses (total)', 'responses/s', 'backend', 'haproxy_b.hrsp_total', 'line'],
  136. 'lines': []
  137. },
  138. 'bqtime': {
  139. 'options': [None, 'The average queue time over the 1024 last requests', 'milliseconds', 'backend',
  140. 'haproxy_b.qtime', 'line'],
  141. 'lines': []
  142. },
  143. 'bctime': {
  144. 'options': [None, 'The average connect time over the 1024 last requests', 'milliseconds', 'backend',
  145. 'haproxy_b.ctime', 'line'],
  146. 'lines': []
  147. },
  148. 'brtime': {
  149. 'options': [None, 'The average response time over the 1024 last requests', 'milliseconds', 'backend',
  150. 'haproxy_b.rtime', 'line'],
  151. 'lines': []
  152. },
  153. 'bttime': {
  154. 'options': [None, 'The average total session time over the 1024 last requests', 'milliseconds', 'backend',
  155. 'haproxy_b.ttime', 'line'],
  156. 'lines': []
  157. },
  158. 'health_sdown': {
  159. 'options': [None, 'Backend Servers In DOWN State', 'failed servers', 'health', 'haproxy_hs.down', 'line'],
  160. 'lines': []
  161. },
  162. 'health_sup': {
  163. 'options': [None, 'Backend Servers In UP State', 'health servers', 'health', 'haproxy_hs.up', 'line'],
  164. 'lines': []
  165. },
  166. 'health_smaint': {
  167. 'options': [None, 'Backend Servers In MAINT State', 'maintenance servers', 'health', 'haproxy_hs.maint', 'line'],
  168. 'lines': []
  169. },
  170. 'health_bdown': {
  171. 'options': [None, 'Is Backend Failed?', 'boolean', 'health', 'haproxy_hb.down', 'line'],
  172. 'lines': []
  173. },
  174. 'health_idle': {
  175. 'options': [None, 'The Ratio Of Polling Time Vs Total Time', 'percentage', 'health', 'haproxy.idle', 'line'],
  176. 'lines': [
  177. ['idle', None, 'absolute']
  178. ]
  179. }
  180. }
  181. METRICS = {
  182. 'bin': {'algorithm': 'incremental', 'divisor': 1024},
  183. 'bout': {'algorithm': 'incremental', 'divisor': 1024},
  184. 'scur': {'algorithm': 'absolute', 'divisor': 1},
  185. 'qcur': {'algorithm': 'absolute', 'divisor': 1},
  186. 'hrsp_1xx': {'algorithm': 'incremental', 'divisor': 1},
  187. 'hrsp_2xx': {'algorithm': 'incremental', 'divisor': 1},
  188. 'hrsp_3xx': {'algorithm': 'incremental', 'divisor': 1},
  189. 'hrsp_4xx': {'algorithm': 'incremental', 'divisor': 1},
  190. 'hrsp_5xx': {'algorithm': 'incremental', 'divisor': 1},
  191. 'hrsp_other': {'algorithm': 'incremental', 'divisor': 1}
  192. }
  193. BACKEND_METRICS = {
  194. 'qtime': {'algorithm': 'absolute', 'divisor': 1},
  195. 'ctime': {'algorithm': 'absolute', 'divisor': 1},
  196. 'rtime': {'algorithm': 'absolute', 'divisor': 1},
  197. 'ttime': {'algorithm': 'absolute', 'divisor': 1}
  198. }
  199. REGEX = dict(url=re_compile(r'idle = (?P<idle>[0-9]+)'),
  200. socket=re_compile(r'Idle_pct: (?P<idle>[0-9]+)'))
  201. # TODO: the code is unreadable
  202. class Service(UrlService, SocketService):
  203. def __init__(self, configuration=None, name=None):
  204. if 'socket' in configuration:
  205. SocketService.__init__(self, configuration=configuration, name=name)
  206. self.poll = SocketService
  207. self.options_ = dict(regex=REGEX['socket'],
  208. stat='show stat\n'.encode(),
  209. info='show info\n'.encode())
  210. else:
  211. UrlService.__init__(self, configuration=configuration, name=name)
  212. self.poll = UrlService
  213. self.options_ = dict(regex=REGEX['url'],
  214. stat=self.url,
  215. info=url_remove_params(self.url))
  216. self.order = ORDER
  217. self.definitions = CHARTS
  218. def check(self):
  219. if self.poll.check(self):
  220. self.create_charts()
  221. self.info('We are using %s.' % self.poll.__name__)
  222. return True
  223. return False
  224. def _get_data(self):
  225. to_netdata = dict()
  226. self.request, self.url = self.options_['stat'], self.options_['stat']
  227. stat_data = self._get_stat_data()
  228. self.request, self.url = self.options_['info'], self.options_['info']
  229. info_data = self._get_info_data(regex=self.options_['regex'])
  230. to_netdata.update(stat_data)
  231. to_netdata.update(info_data)
  232. return to_netdata or None
  233. def _get_stat_data(self):
  234. """
  235. :return: dict
  236. """
  237. raw_data = self.poll._get_raw_data(self)
  238. if not raw_data:
  239. return dict()
  240. raw_data = raw_data.splitlines()
  241. self.data = parse_data_([dict(zip(raw_data[0].split(','), raw_data[_].split(',')))
  242. for _ in range(1, len(raw_data))])
  243. if not self.data:
  244. return dict()
  245. stat_data = dict()
  246. for frontend in self.data['frontend']:
  247. for metric in METRICS:
  248. idx = frontend['# pxname'].replace('.', '_')
  249. stat_data['_'.join(['frontend', metric, idx])] = frontend.get(metric) or 0
  250. for backend in self.data['backend']:
  251. name, idx = backend['# pxname'], backend['# pxname'].replace('.', '_')
  252. stat_data['hsup_' + idx] = len([server for server in self.data['servers']
  253. if server_status(server, name, 'UP')])
  254. stat_data['hsdown_' + idx] = len([server for server in self.data['servers']
  255. if server_status(server, name, 'DOWN')])
  256. stat_data['hsmaint_' + idx] = len([server for server in self.data['servers']
  257. if server_status(server, name, 'MAINT')])
  258. stat_data['hbdown_' + idx] = 1 if backend.get('status') == 'DOWN' else 0
  259. for metric in BACKEND_METRICS:
  260. stat_data['_'.join(['backend', metric, idx])] = backend.get(metric) or 0
  261. hrsp_total = 0
  262. for metric in METRICS:
  263. stat_data['_'.join(['backend', metric, idx])] = backend.get(metric) or 0
  264. if metric.startswith('hrsp_'):
  265. hrsp_total += int(backend.get(metric) or 0)
  266. stat_data['_'.join(['backend', 'hrsp_total', idx])] = hrsp_total
  267. return stat_data
  268. def _get_info_data(self, regex):
  269. """
  270. :return: dict
  271. """
  272. raw_data = self.poll._get_raw_data(self)
  273. if not raw_data:
  274. return dict()
  275. match = regex.search(raw_data)
  276. return match.groupdict() if match else dict()
  277. @staticmethod
  278. def _check_raw_data(data):
  279. """
  280. Check if all data has been gathered from socket
  281. :param data: str
  282. :return: boolean
  283. """
  284. return not bool(data)
  285. def create_charts(self):
  286. for front in self.data['frontend']:
  287. name, idx = front['# pxname'], front['# pxname'].replace('.', '_')
  288. for metric in METRICS:
  289. self.definitions['f' + metric]['lines'].append(['_'.join(['frontend', metric, idx]),
  290. name, METRICS[metric]['algorithm'], 1,
  291. METRICS[metric]['divisor']])
  292. self.definitions['fhrsp_total']['lines'].append(['_'.join(['frontend', 'hrsp_total', idx]),
  293. name, 'incremental', 1, 1])
  294. for back in self.data['backend']:
  295. name, idx = back['# pxname'], back['# pxname'].replace('.', '_')
  296. for metric in METRICS:
  297. self.definitions['b' + metric]['lines'].append(['_'.join(['backend', metric, idx]),
  298. name, METRICS[metric]['algorithm'], 1,
  299. METRICS[metric]['divisor']])
  300. self.definitions['bhrsp_total']['lines'].append(['_'.join(['backend', 'hrsp_total', idx]),
  301. name, 'incremental', 1, 1])
  302. for metric in BACKEND_METRICS:
  303. self.definitions['b' + metric]['lines'].append(['_'.join(['backend', metric, idx]),
  304. name, BACKEND_METRICS[metric]['algorithm'], 1,
  305. BACKEND_METRICS[metric]['divisor']])
  306. self.definitions['health_sup']['lines'].append(['hsup_' + idx, name, 'absolute'])
  307. self.definitions['health_sdown']['lines'].append(['hsdown_' + idx, name, 'absolute'])
  308. self.definitions['health_smaint']['lines'].append(['hsmaint_' + idx, name, 'absolute'])
  309. self.definitions['health_bdown']['lines'].append(['hbdown_' + idx, name, 'absolute'])
  310. def parse_data_(data):
  311. def is_backend(backend):
  312. return backend.get('svname') == 'BACKEND' and backend.get('# pxname') != 'stats'
  313. def is_frontend(frontend):
  314. return frontend.get('svname') == 'FRONTEND' and frontend.get('# pxname') != 'stats'
  315. def is_server(server):
  316. return not server.get('svname', '').startswith(('FRONTEND', 'BACKEND'))
  317. if not data:
  318. return None
  319. result = defaultdict(list)
  320. for elem in data:
  321. if is_backend(elem):
  322. result['backend'].append(elem)
  323. continue
  324. elif is_frontend(elem):
  325. result['frontend'].append(elem)
  326. continue
  327. elif is_server(elem):
  328. result['servers'].append(elem)
  329. return result or None
  330. def server_status(server, backend_name, status='DOWN'):
  331. return server.get('# pxname') == backend_name and server.get('status').partition(' ')[0] == status
  332. def url_remove_params(url):
  333. parsed = urlparse(url or str())
  334. return '{scheme}://{netloc}{path}'.format(scheme=parsed.scheme, netloc=parsed.netloc, path=parsed.path)