nginx_plus.chart.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487
  1. # -*- coding: utf-8 -*-
  2. # Description: nginx_plus netdata python.d module
  3. # Author: Ilya Mashchenko (ilyam8)
  4. # SPDX-License-Identifier: GPL-3.0-or-later
  5. import re
  6. from collections import defaultdict
  7. from copy import deepcopy
  8. from json import loads
  9. try:
  10. from collections import OrderedDict
  11. except ImportError:
  12. from third_party.ordereddict import OrderedDict
  13. from bases.FrameworkServices.UrlService import UrlService
  14. ORDER = [
  15. 'requests_total',
  16. 'requests_current',
  17. 'connections_statistics',
  18. 'connections_workers',
  19. 'ssl_handshakes',
  20. 'ssl_session_reuses',
  21. 'ssl_memory_usage',
  22. 'processes'
  23. ]
  24. CHARTS = {
  25. 'requests_total': {
  26. 'options': [None, 'Requests Total', 'requests/s', 'requests', 'nginx_plus.requests_total', 'line'],
  27. 'lines': [
  28. ['requests_total', 'total', 'incremental']
  29. ]
  30. },
  31. 'requests_current': {
  32. 'options': [None, 'Requests Current', 'requests', 'requests', 'nginx_plus.requests_current', 'line'],
  33. 'lines': [
  34. ['requests_current', 'current']
  35. ]
  36. },
  37. 'connections_statistics': {
  38. 'options': [None, 'Connections Statistics', 'connections/s',
  39. 'connections', 'nginx_plus.connections_statistics', 'stacked'],
  40. 'lines': [
  41. ['connections_accepted', 'accepted', 'incremental'],
  42. ['connections_dropped', 'dropped', 'incremental']
  43. ]
  44. },
  45. 'connections_workers': {
  46. 'options': [None, 'Workers Statistics', 'workers',
  47. 'connections', 'nginx_plus.connections_workers', 'stacked'],
  48. 'lines': [
  49. ['connections_idle', 'idle'],
  50. ['connections_active', 'active']
  51. ]
  52. },
  53. 'ssl_handshakes': {
  54. 'options': [None, 'SSL Handshakes', 'handshakes/s', 'ssl', 'nginx_plus.ssl_handshakes', 'stacked'],
  55. 'lines': [
  56. ['ssl_handshakes', 'successful', 'incremental'],
  57. ['ssl_handshakes_failed', 'failed', 'incremental']
  58. ]
  59. },
  60. 'ssl_session_reuses': {
  61. 'options': [None, 'Session Reuses', 'sessions/s', 'ssl', 'nginx_plus.ssl_session_reuses', 'line'],
  62. 'lines': [
  63. ['ssl_session_reuses', 'reused', 'incremental']
  64. ]
  65. },
  66. 'ssl_memory_usage': {
  67. 'options': [None, 'Memory Usage', 'percentage', 'ssl', 'nginx_plus.ssl_memory_usage', 'area'],
  68. 'lines': [
  69. ['ssl_memory_usage', 'usage', 'absolute', 1, 100]
  70. ]
  71. },
  72. 'processes': {
  73. 'options': [None, 'Processes', 'processes', 'processes', 'nginx_plus.processes', 'line'],
  74. 'lines': [
  75. ['processes_respawned', 'respawned']
  76. ]
  77. }
  78. }
  79. def cache_charts(cache):
  80. family = 'cache {0}'.format(cache.real_name)
  81. charts = OrderedDict()
  82. charts['{0}_traffic'.format(cache.name)] = {
  83. 'options': [None, 'Traffic', 'KiB', family, 'nginx_plus.cache_traffic', 'stacked'],
  84. 'lines': [
  85. ['_'.join([cache.name, 'hit_bytes']), 'served', 'absolute', 1, 1024],
  86. ['_'.join([cache.name, 'miss_bytes_written']), 'written', 'absolute', 1, 1024],
  87. ['_'.join([cache.name, 'miss_bytes']), 'bypass', 'absolute', 1, 1024]
  88. ]
  89. }
  90. charts['{0}_memory_usage'.format(cache.name)] = {
  91. 'options': [None, 'Memory Usage', 'percentage', family, 'nginx_plus.cache_memory_usage', 'area'],
  92. 'lines': [
  93. ['_'.join([cache.name, 'memory_usage']), 'usage', 'absolute', 1, 100],
  94. ]
  95. }
  96. return charts
  97. def web_zone_charts(wz):
  98. charts = OrderedDict()
  99. family = 'web zone {name}'.format(name=wz.real_name)
  100. # Processing
  101. charts['zone_{name}_processing'.format(name=wz.name)] = {
  102. 'options': [None, 'Zone "{name}" Processing'.format(name=wz.name), 'requests', family,
  103. 'nginx_plus.web_zone_processing', 'line'],
  104. 'lines': [
  105. ['_'.join([wz.name, 'processing']), 'processing']
  106. ]
  107. }
  108. # Requests
  109. charts['zone_{name}_requests'.format(name=wz.name)] = {
  110. 'options': [None, 'Zone "{name}" Requests'.format(name=wz.name), 'requests/s', family,
  111. 'nginx_plus.web_zone_requests', 'line'],
  112. 'lines': [
  113. ['_'.join([wz.name, 'requests']), 'requests', 'incremental']
  114. ]
  115. }
  116. # Response Codes
  117. charts['zone_{name}_responses'.format(name=wz.name)] = {
  118. 'options': [None, 'Zone "{name}" Responses'.format(name=wz.name), 'requests/s', family,
  119. 'nginx_plus.web_zone_responses', 'stacked'],
  120. 'lines': [
  121. ['_'.join([wz.name, 'responses_2xx']), '2xx', 'incremental'],
  122. ['_'.join([wz.name, 'responses_5xx']), '5xx', 'incremental'],
  123. ['_'.join([wz.name, 'responses_3xx']), '3xx', 'incremental'],
  124. ['_'.join([wz.name, 'responses_4xx']), '4xx', 'incremental'],
  125. ['_'.join([wz.name, 'responses_1xx']), '1xx', 'incremental']
  126. ]
  127. }
  128. # Traffic
  129. charts['zone_{name}_net'.format(name=wz.name)] = {
  130. 'options': [None, 'Zone "{name}" Traffic'.format(name=wz.name), 'kilobits/s', family,
  131. 'nginx_plus.zone_net', 'area'],
  132. 'lines': [
  133. ['_'.join([wz.name, 'received']), 'received', 'incremental', 1, 1000],
  134. ['_'.join([wz.name, 'sent']), 'sent', 'incremental', -1, 1000]
  135. ]
  136. }
  137. return charts
  138. def web_upstream_charts(wu):
  139. def dimensions(value, a='absolute', m=1, d=1):
  140. dims = list()
  141. for p in wu:
  142. dims.append(['_'.join([wu.name, p.server, value]), p.real_server, a, m, d])
  143. return dims
  144. charts = OrderedDict()
  145. family = 'web upstream {name}'.format(name=wu.real_name)
  146. # Requests
  147. charts['web_upstream_{name}_requests'.format(name=wu.name)] = {
  148. 'options': [None, 'Peers Requests', 'requests/s', family, 'nginx_plus.web_upstream_requests', 'line'],
  149. 'lines': dimensions('requests', 'incremental')
  150. }
  151. # Responses Codes
  152. charts['web_upstream_{name}_all_responses'.format(name=wu.name)] = {
  153. 'options': [None, 'All Peers Responses', 'responses/s', family,
  154. 'nginx_plus.web_upstream_all_responses', 'stacked'],
  155. 'lines': [
  156. ['_'.join([wu.name, 'responses_2xx']), '2xx', 'incremental'],
  157. ['_'.join([wu.name, 'responses_5xx']), '5xx', 'incremental'],
  158. ['_'.join([wu.name, 'responses_3xx']), '3xx', 'incremental'],
  159. ['_'.join([wu.name, 'responses_4xx']), '4xx', 'incremental'],
  160. ['_'.join([wu.name, 'responses_1xx']), '1xx', 'incremental'],
  161. ]
  162. }
  163. for peer in wu:
  164. charts['web_upstream_{0}_{1}_responses'.format(wu.name, peer.server)] = {
  165. 'options': [None, 'Peer "{0}" Responses'.format(peer.real_server), 'responses/s', family,
  166. 'nginx_plus.web_upstream_peer_responses', 'stacked'],
  167. 'lines': [
  168. ['_'.join([wu.name, peer.server, 'responses_2xx']), '2xx', 'incremental'],
  169. ['_'.join([wu.name, peer.server, 'responses_5xx']), '5xx', 'incremental'],
  170. ['_'.join([wu.name, peer.server, 'responses_3xx']), '3xx', 'incremental'],
  171. ['_'.join([wu.name, peer.server, 'responses_4xx']), '4xx', 'incremental'],
  172. ['_'.join([wu.name, peer.server, 'responses_1xx']), '1xx', 'incremental']
  173. ]
  174. }
  175. # Connections
  176. charts['web_upstream_{name}_connections'.format(name=wu.name)] = {
  177. 'options': [None, 'Peers Connections', 'active', family, 'nginx_plus.web_upstream_connections', 'line'],
  178. 'lines': dimensions('active')
  179. }
  180. charts['web_upstream_{name}_connections_usage'.format(name=wu.name)] = {
  181. 'options': [None, 'Peers Connections Usage', 'percentage', family,
  182. 'nginx_plus.web_upstream_connections_usage', 'line'],
  183. 'lines': dimensions('connections_usage', d=100)
  184. }
  185. # Traffic
  186. charts['web_upstream_{0}_all_net'.format(wu.name)] = {
  187. 'options': [None, 'All Peers Traffic', 'kilobits/s', family, 'nginx_plus.web_upstream_all_net', 'area'],
  188. 'lines': [
  189. ['{0}_received'.format(wu.name), 'received', 'incremental', 1, 1000],
  190. ['{0}_sent'.format(wu.name), 'sent', 'incremental', -1, 1000]
  191. ]
  192. }
  193. for peer in wu:
  194. charts['web_upstream_{0}_{1}_net'.format(wu.name, peer.server)] = {
  195. 'options': [None, 'Peer "{0}" Traffic'.format(peer.real_server), 'kilobits/s', family,
  196. 'nginx_plus.web_upstream_peer_traffic', 'area'],
  197. 'lines': [
  198. ['{0}_{1}_received'.format(wu.name, peer.server), 'received', 'incremental', 1, 1000],
  199. ['{0}_{1}_sent'.format(wu.name, peer.server), 'sent', 'incremental', -1, 1000]
  200. ]
  201. }
  202. # Response Time
  203. for peer in wu:
  204. charts['web_upstream_{0}_{1}_timings'.format(wu.name, peer.server)] = {
  205. 'options': [None, 'Peer "{0}" Timings'.format(peer.real_server), 'milliseconds', family,
  206. 'nginx_plus.web_upstream_peer_timings', 'line'],
  207. 'lines': [
  208. ['_'.join([wu.name, peer.server, 'header_time']), 'header'],
  209. ['_'.join([wu.name, peer.server, 'response_time']), 'response']
  210. ]
  211. }
  212. # Memory Usage
  213. charts['web_upstream_{name}_memory_usage'.format(name=wu.name)] = {
  214. 'options': [None, 'Memory Usage', 'percentage', family, 'nginx_plus.web_upstream_memory_usage', 'area'],
  215. 'lines': [
  216. ['_'.join([wu.name, 'memory_usage']), 'usage', 'absolute', 1, 100]
  217. ]
  218. }
  219. # State
  220. charts['web_upstream_{name}_status'.format(name=wu.name)] = {
  221. 'options': [None, 'Peers Status', 'state', family, 'nginx_plus.web_upstream_status', 'line'],
  222. 'lines': dimensions('state')
  223. }
  224. # Downtime
  225. charts['web_upstream_{name}_downtime'.format(name=wu.name)] = {
  226. 'options': [None, 'Peers Downtime', 'seconds', family, 'nginx_plus.web_upstream_peer_downtime', 'line'],
  227. 'lines': dimensions('downtime', d=1000)
  228. }
  229. return charts
  230. METRICS = {
  231. 'SERVER': [
  232. 'processes.respawned',
  233. 'connections.accepted',
  234. 'connections.dropped',
  235. 'connections.active',
  236. 'connections.idle',
  237. 'ssl.handshakes',
  238. 'ssl.handshakes_failed',
  239. 'ssl.session_reuses',
  240. 'requests.total',
  241. 'requests.current',
  242. 'slabs.SSL.pages.free',
  243. 'slabs.SSL.pages.used'
  244. ],
  245. 'WEB_ZONE': [
  246. 'processing',
  247. 'requests',
  248. 'responses.1xx',
  249. 'responses.2xx',
  250. 'responses.3xx',
  251. 'responses.4xx',
  252. 'responses.5xx',
  253. 'discarded',
  254. 'received',
  255. 'sent'
  256. ],
  257. 'WEB_UPSTREAM_PEER': [
  258. 'id',
  259. 'server',
  260. 'name',
  261. 'state',
  262. 'active',
  263. 'max_conns',
  264. 'requests',
  265. 'header_time', # alive only
  266. 'response_time', # alive only
  267. 'responses.1xx',
  268. 'responses.2xx',
  269. 'responses.3xx',
  270. 'responses.4xx',
  271. 'responses.5xx',
  272. 'sent',
  273. 'received',
  274. 'downtime'
  275. ],
  276. 'WEB_UPSTREAM_SUMMARY': [
  277. 'responses.1xx',
  278. 'responses.2xx',
  279. 'responses.3xx',
  280. 'responses.4xx',
  281. 'responses.5xx',
  282. 'sent',
  283. 'received'
  284. ],
  285. 'CACHE': [
  286. 'hit.bytes', # served
  287. 'miss.bytes_written', # written
  288. 'miss.bytes' # bypass
  289. ]
  290. }
  291. BAD_SYMBOLS = re.compile(r'[:/.-]+')
  292. class Cache:
  293. key = 'caches'
  294. charts = cache_charts
  295. def __init__(self, **kw):
  296. self.real_name = kw['name']
  297. self.name = BAD_SYMBOLS.sub('_', self.real_name)
  298. def memory_usage(self, data):
  299. used = data['slabs'][self.real_name]['pages']['used']
  300. free = data['slabs'][self.real_name]['pages']['free']
  301. return used / float(free + used) * 1e4
  302. def get_data(self, raw_data):
  303. zone_data = raw_data['caches'][self.real_name]
  304. data = parse_json(zone_data, METRICS['CACHE'])
  305. data['memory_usage'] = self.memory_usage(raw_data)
  306. return dict(('_'.join([self.name, k]), v) for k, v in data.items())
  307. class WebZone:
  308. key = 'server_zones'
  309. charts = web_zone_charts
  310. def __init__(self, **kw):
  311. self.real_name = kw['name']
  312. self.name = BAD_SYMBOLS.sub('_', self.real_name)
  313. def get_data(self, raw_data):
  314. zone_data = raw_data['server_zones'][self.real_name]
  315. data = parse_json(zone_data, METRICS['WEB_ZONE'])
  316. return dict(('_'.join([self.name, k]), v) for k, v in data.items())
  317. class WebUpstream:
  318. key = 'upstreams'
  319. charts = web_upstream_charts
  320. def __init__(self, **kw):
  321. self.real_name = kw['name']
  322. self.name = BAD_SYMBOLS.sub('_', self.real_name)
  323. self.peers = OrderedDict()
  324. peers = kw['response']['upstreams'][self.real_name]['peers']
  325. for peer in peers:
  326. self.add_peer(peer['id'], peer['server'])
  327. def __iter__(self):
  328. return iter(self.peers.values())
  329. def add_peer(self, idx, server):
  330. peer = WebUpstreamPeer(idx, server)
  331. self.peers[peer.real_server] = peer
  332. return peer
  333. def peers_stats(self, peers):
  334. peers = {int(peer['id']): peer for peer in peers}
  335. data = dict()
  336. for peer in self.peers.values():
  337. if not peer.active:
  338. continue
  339. try:
  340. data.update(peer.get_data(peers[peer.id]))
  341. except KeyError:
  342. peer.active = False
  343. return data
  344. def memory_usage(self, data):
  345. used = data['slabs'][self.real_name]['pages']['used']
  346. free = data['slabs'][self.real_name]['pages']['free']
  347. return used / float(free + used) * 1e4
  348. def summary_stats(self, data):
  349. rv = defaultdict(int)
  350. for metric in METRICS['WEB_UPSTREAM_SUMMARY']:
  351. for peer in self.peers.values():
  352. if peer.active:
  353. metric = '_'.join(metric.split('.'))
  354. rv[metric] += data['_'.join([peer.server, metric])]
  355. return rv
  356. def get_data(self, raw_data):
  357. data = dict()
  358. peers = raw_data['upstreams'][self.real_name]['peers']
  359. data.update(self.peers_stats(peers))
  360. data.update(self.summary_stats(data))
  361. data['memory_usage'] = self.memory_usage(raw_data)
  362. return dict(('_'.join([self.name, k]), v) for k, v in data.items())
  363. class WebUpstreamPeer:
  364. def __init__(self, idx, server):
  365. self.id = idx
  366. self.real_server = server
  367. self.server = BAD_SYMBOLS.sub('_', self.real_server)
  368. self.active = True
  369. def get_data(self, raw):
  370. data = dict(header_time=0, response_time=0, max_conns=0)
  371. data.update(parse_json(raw, METRICS['WEB_UPSTREAM_PEER']))
  372. data['connections_usage'] = 0 if not data['max_conns'] else data['active'] / float(data['max_conns']) * 1e4
  373. data['state'] = int(data['state'] == 'up')
  374. return dict(('_'.join([self.server, k]), v) for k, v in data.items())
  375. class Service(UrlService):
  376. def __init__(self, configuration=None, name=None):
  377. UrlService.__init__(self, configuration=configuration, name=name)
  378. self.order = list(ORDER)
  379. self.definitions = deepcopy(CHARTS)
  380. self.objects = dict()
  381. def check(self):
  382. if not self.url:
  383. self.error('URL is not defined')
  384. return None
  385. self._manager = self._build_manager()
  386. if not self._manager:
  387. return None
  388. raw_data = self._get_raw_data()
  389. if not raw_data:
  390. return None
  391. try:
  392. response = loads(raw_data)
  393. except ValueError:
  394. return None
  395. for obj_cls in [WebZone, WebUpstream, Cache]:
  396. for obj_name in response.get(obj_cls.key, list()):
  397. obj = obj_cls(name=obj_name, response=response)
  398. self.objects[obj.real_name] = obj
  399. charts = obj_cls.charts(obj)
  400. for chart in charts:
  401. self.order.append(chart)
  402. self.definitions[chart] = charts[chart]
  403. return bool(self.objects)
  404. def _get_data(self):
  405. """
  406. Format data received from http request
  407. :return: dict
  408. """
  409. raw_data = self._get_raw_data()
  410. if not raw_data:
  411. return None
  412. response = loads(raw_data)
  413. data = parse_json(response, METRICS['SERVER'])
  414. data['ssl_memory_usage'] = data['slabs_SSL_pages_used'] / float(data['slabs_SSL_pages_free']) * 1e4
  415. for obj in self.objects.values():
  416. if obj.real_name in response[obj.key]:
  417. data.update(obj.get_data(response))
  418. return data
  419. def parse_json(raw_data, metrics):
  420. data = dict()
  421. for metric in metrics:
  422. value = raw_data
  423. metrics_list = metric.split('.')
  424. try:
  425. for m in metrics_list:
  426. value = value[m]
  427. except KeyError:
  428. continue
  429. data['_'.join(metrics_list)] = value
  430. return data