varnish.chart.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385
  1. # -*- coding: utf-8 -*-
  2. # Description: varnish netdata python.d module
  3. # Author: ilyam8
  4. # SPDX-License-Identifier: GPL-3.0-or-later
  5. import re
  6. from bases.FrameworkServices.ExecutableService import ExecutableService
  7. from bases.collection import find_binary
  8. ORDER = [
  9. 'session_connections',
  10. 'client_requests',
  11. 'all_time_hit_rate',
  12. 'current_poll_hit_rate',
  13. 'cached_objects_expired',
  14. 'cached_objects_nuked',
  15. 'threads_total',
  16. 'threads_statistics',
  17. 'threads_queue_len',
  18. 'backend_connections',
  19. 'backend_requests',
  20. 'esi_statistics',
  21. 'memory_usage',
  22. 'uptime'
  23. ]
  24. CHARTS = {
  25. 'session_connections': {
  26. 'options': [None, 'Connections Statistics', 'connections/s',
  27. 'client metrics', 'varnish.session_connection', 'line'],
  28. 'lines': [
  29. ['sess_conn', 'accepted', 'incremental'],
  30. ['sess_dropped', 'dropped', 'incremental']
  31. ]
  32. },
  33. 'client_requests': {
  34. 'options': [None, 'Client Requests', 'requests/s',
  35. 'client metrics', 'varnish.client_requests', 'line'],
  36. 'lines': [
  37. ['client_req', 'received', 'incremental']
  38. ]
  39. },
  40. 'all_time_hit_rate': {
  41. 'options': [None, 'All History Hit Rate Ratio', 'percentage', 'cache performance',
  42. 'varnish.all_time_hit_rate', 'stacked'],
  43. 'lines': [
  44. ['cache_hit', 'hit', 'percentage-of-absolute-row'],
  45. ['cache_miss', 'miss', 'percentage-of-absolute-row'],
  46. ['cache_hitpass', 'hitpass', 'percentage-of-absolute-row']]
  47. },
  48. 'current_poll_hit_rate': {
  49. 'options': [None, 'Current Poll Hit Rate Ratio', 'percentage', 'cache performance',
  50. 'varnish.current_poll_hit_rate', 'stacked'],
  51. 'lines': [
  52. ['cache_hit', 'hit', 'percentage-of-incremental-row'],
  53. ['cache_miss', 'miss', 'percentage-of-incremental-row'],
  54. ['cache_hitpass', 'hitpass', 'percentage-of-incremental-row']
  55. ]
  56. },
  57. 'cached_objects_expired': {
  58. 'options': [None, 'Expired Objects', 'expired/s', 'cache performance',
  59. 'varnish.cached_objects_expired', 'line'],
  60. 'lines': [
  61. ['n_expired', 'objects', 'incremental']
  62. ]
  63. },
  64. 'cached_objects_nuked': {
  65. 'options': [None, 'Least Recently Used Nuked Objects', 'nuked/s', 'cache performance',
  66. 'varnish.cached_objects_nuked', 'line'],
  67. 'lines': [
  68. ['n_lru_nuked', 'objects', 'incremental']
  69. ]
  70. },
  71. 'threads_total': {
  72. 'options': [None, 'Number Of Threads In All Pools', 'number', 'thread related metrics',
  73. 'varnish.threads_total', 'line'],
  74. 'lines': [
  75. ['threads', None, 'absolute']
  76. ]
  77. },
  78. 'threads_statistics': {
  79. 'options': [None, 'Threads Statistics', 'threads/s', 'thread related metrics',
  80. 'varnish.threads_statistics', 'line'],
  81. 'lines': [
  82. ['threads_created', 'created', 'incremental'],
  83. ['threads_failed', 'failed', 'incremental'],
  84. ['threads_limited', 'limited', 'incremental']
  85. ]
  86. },
  87. 'threads_queue_len': {
  88. 'options': [None, 'Current Queue Length', 'requests', 'thread related metrics',
  89. 'varnish.threads_queue_len', 'line'],
  90. 'lines': [
  91. ['thread_queue_len', 'in queue']
  92. ]
  93. },
  94. 'backend_connections': {
  95. 'options': [None, 'Backend Connections Statistics', 'connections/s', 'backend metrics',
  96. 'varnish.backend_connections', 'line'],
  97. 'lines': [
  98. ['backend_conn', 'successful', 'incremental'],
  99. ['backend_unhealthy', 'unhealthy', 'incremental'],
  100. ['backend_reuse', 'reused', 'incremental'],
  101. ['backend_toolate', 'closed', 'incremental'],
  102. ['backend_recycle', 'recycled', 'incremental'],
  103. ['backend_fail', 'failed', 'incremental']
  104. ]
  105. },
  106. 'backend_requests': {
  107. 'options': [None, 'Requests To The Backend', 'requests/s', 'backend metrics',
  108. 'varnish.backend_requests', 'line'],
  109. 'lines': [
  110. ['backend_req', 'sent', 'incremental']
  111. ]
  112. },
  113. 'esi_statistics': {
  114. 'options': [None, 'ESI Statistics', 'problems/s', 'esi related metrics', 'varnish.esi_statistics', 'line'],
  115. 'lines': [
  116. ['esi_errors', 'errors', 'incremental'],
  117. ['esi_warnings', 'warnings', 'incremental']
  118. ]
  119. },
  120. 'memory_usage': {
  121. 'options': [None, 'Memory Usage', 'MiB', 'memory usage', 'varnish.memory_usage', 'stacked'],
  122. 'lines': [
  123. ['memory_free', 'free', 'absolute', 1, 1 << 20],
  124. ['memory_allocated', 'allocated', 'absolute', 1, 1 << 20]]
  125. },
  126. 'uptime': {
  127. 'lines': [
  128. ['uptime', None, 'absolute']
  129. ],
  130. 'options': [None, 'Uptime', 'seconds', 'uptime', 'varnish.uptime', 'line']
  131. }
  132. }
  133. def backend_charts_template(name):
  134. order = [
  135. '{0}_response_statistics'.format(name),
  136. ]
  137. charts = {
  138. order[0]: {
  139. 'options': [None, 'Backend "{0}"'.format(name), 'kilobits/s', 'backend response statistics',
  140. 'varnish.backend', 'area'],
  141. 'lines': [
  142. ['{0}_beresp_hdrbytes'.format(name), 'header', 'incremental', 8, 1000],
  143. ['{0}_beresp_bodybytes'.format(name), 'body', 'incremental', -8, 1000]
  144. ]
  145. },
  146. }
  147. return order, charts
  148. def storage_charts_template(name):
  149. order = [
  150. 'storage_{0}_usage'.format(name),
  151. 'storage_{0}_alloc_objs'.format(name)
  152. ]
  153. charts = {
  154. order[0]: {
  155. 'options': [None, 'Storage "{0}" Usage'.format(name), 'KiB', 'storage usage', 'varnish.storage_usage', 'stacked'],
  156. 'lines': [
  157. ['{0}.g_space'.format(name), 'free', 'absolute', 1, 1 << 10],
  158. ['{0}.g_bytes'.format(name), 'allocated', 'absolute', 1, 1 << 10]
  159. ]
  160. },
  161. order[1]: {
  162. 'options': [None, 'Storage "{0}" Allocated Objects'.format(name), 'objects', 'storage usage', 'varnish.storage_alloc_objs', 'line'],
  163. 'lines': [
  164. ['{0}.g_alloc'.format(name), 'allocated', 'absolute']
  165. ]
  166. }
  167. }
  168. return order, charts
  169. VARNISHSTAT = 'varnishstat'
  170. re_version = re.compile(r'varnish-(?:plus-)?(?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)')
  171. class VarnishVersion:
  172. def __init__(self, major, minor, patch):
  173. self.major = major
  174. self.minor = minor
  175. self.patch = patch
  176. def __str__(self):
  177. return '{0}.{1}.{2}'.format(self.major, self.minor, self.patch)
  178. class Parser:
  179. _backend_new = re.compile(r'VBE.([\d\w_.]+)\(.*?\).(beresp[\w_]+)\s+(\d+)')
  180. _backend_old = re.compile(r'VBE\.[\d\w-]+\.([\w\d_-]+).(beresp[\w_]+)\s+(\d+)')
  181. _default = re.compile(r'([A-Z]+\.)?([\d\w_.]+)\s+(\d+)')
  182. def __init__(self):
  183. self.re_default = None
  184. self.re_backend = None
  185. def init(self, data):
  186. data = ''.join(data)
  187. parsed_main = Parser._default.findall(data)
  188. if parsed_main:
  189. self.re_default = Parser._default
  190. parsed_backend = Parser._backend_new.findall(data)
  191. if parsed_backend:
  192. self.re_backend = Parser._backend_new
  193. else:
  194. parsed_backend = Parser._backend_old.findall(data)
  195. if parsed_backend:
  196. self.re_backend = Parser._backend_old
  197. def server_stats(self, data):
  198. return self.re_default.findall(''.join(data))
  199. def backend_stats(self, data):
  200. return self.re_backend.findall(''.join(data))
  201. class Service(ExecutableService):
  202. def __init__(self, configuration=None, name=None):
  203. ExecutableService.__init__(self, configuration=configuration, name=name)
  204. self.order = ORDER
  205. self.definitions = CHARTS
  206. self.instance_name = configuration.get('instance_name')
  207. self.parser = Parser()
  208. self.command = None
  209. self.collected_vbe = set()
  210. self.collected_storages = set()
  211. def create_command(self):
  212. varnishstat = find_binary(VARNISHSTAT)
  213. if not varnishstat:
  214. self.error("can't locate '{0}' binary or binary is not executable by user netdata".format(VARNISHSTAT))
  215. return False
  216. command = [varnishstat, '-V']
  217. reply = self._get_raw_data(stderr=True, command=command)
  218. if not reply:
  219. self.error(
  220. "no output from '{0}'. Is varnish running? Not enough privileges?".format(' '.join(self.command)))
  221. return False
  222. ver = parse_varnish_version(reply)
  223. if not ver:
  224. self.error("failed to parse reply from '{0}', used regex :'{1}', reply : {2}".format(
  225. ' '.join(command), re_version.pattern, reply))
  226. return False
  227. if self.instance_name:
  228. self.command = [varnishstat, '-1', '-n', self.instance_name]
  229. else:
  230. self.command = [varnishstat, '-1']
  231. if ver.major > 4:
  232. self.command.extend(['-t', '1'])
  233. self.info("varnish version: {0}, will use command: '{1}'".format(ver, ' '.join(self.command)))
  234. return True
  235. def check(self):
  236. if not self.create_command():
  237. return False
  238. # STDOUT is not empty
  239. reply = self._get_raw_data()
  240. if not reply:
  241. self.error("no output from '{0}'. Is it running? Not enough privileges?".format(' '.join(self.command)))
  242. return False
  243. self.parser.init(reply)
  244. # Output is parsable
  245. if not self.parser.re_default:
  246. self.error('cant parse the output...')
  247. return False
  248. return True
  249. def get_data(self):
  250. """
  251. Format data received from shell command
  252. :return: dict
  253. """
  254. raw = self._get_raw_data()
  255. if not raw:
  256. return None
  257. data = dict()
  258. server_stats = self.parser.server_stats(raw)
  259. if not server_stats:
  260. return None
  261. stats = dict((param, value) for _, param, value in server_stats)
  262. data.update(stats)
  263. self.get_vbe_backends(data, raw)
  264. self.get_storages(server_stats)
  265. # varnish 5 uses default.g_bytes and default.g_space
  266. data['memory_allocated'] = data.get('s0.g_bytes') or data.get('default.g_bytes')
  267. data['memory_free'] = data.get('s0.g_space') or data.get('default.g_space')
  268. return data
  269. def get_vbe_backends(self, data, raw):
  270. if not self.parser.re_backend:
  271. return
  272. stats = self.parser.backend_stats(raw)
  273. if not stats:
  274. return
  275. for (name, param, value) in stats:
  276. data['_'.join([name, param])] = value
  277. if name in self.collected_vbe:
  278. continue
  279. self.collected_vbe.add(name)
  280. self.add_backend_charts(name)
  281. def get_storages(self, server_stats):
  282. # Storage types:
  283. # - SMF: File Storage
  284. # - SMA: Malloc Storage
  285. # - MSE: Massive Storage Engine (Varnish-Plus only)
  286. #
  287. # Stats example:
  288. # [('SMF.', 'ssdStorage.c_req', '47686'),
  289. # ('SMF.', 'ssdStorage.c_fail', '0'),
  290. # ('SMF.', 'ssdStorage.c_bytes', '668102656'),
  291. # ('SMF.', 'ssdStorage.c_freed', '140980224'),
  292. # ('SMF.', 'ssdStorage.g_alloc', '39753'),
  293. # ('SMF.', 'ssdStorage.g_bytes', '527122432'),
  294. # ('SMF.', 'ssdStorage.g_space', '53159968768'),
  295. # ('SMF.', 'ssdStorage.g_smf', '40130'),
  296. # ('SMF.', 'ssdStorage.g_smf_frag', '311'),
  297. # ('SMF.', 'ssdStorage.g_smf_large', '66')]
  298. storages = [name for typ, name, _ in server_stats if typ.startswith(('SMF', 'SMA', 'MSE')) and name.endswith('g_space')]
  299. if not storages:
  300. return
  301. for storage in storages:
  302. storage = storage.split('.')[0]
  303. if storage in self.collected_storages:
  304. continue
  305. self.collected_storages.add(storage)
  306. self.add_storage_charts(storage)
  307. def add_backend_charts(self, backend_name):
  308. self.add_charts(backend_name, backend_charts_template)
  309. def add_storage_charts(self, storage_name):
  310. self.add_charts(storage_name, storage_charts_template)
  311. def add_charts(self, name, charts_template):
  312. order, charts = charts_template(name)
  313. for chart_name in order:
  314. params = [chart_name] + charts[chart_name]['options']
  315. dimensions = charts[chart_name]['lines']
  316. new_chart = self.charts.add_chart(params)
  317. for dimension in dimensions:
  318. new_chart.add_dimension(dimension)
  319. def parse_varnish_version(lines):
  320. m = re_version.search(lines[0])
  321. if not m:
  322. return None
  323. m = m.groupdict()
  324. return VarnishVersion(
  325. int(m['major']),
  326. int(m['minor']),
  327. int(m['patch']),
  328. )