couchdb.chart.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410
  1. # -*- coding: utf-8 -*-
  2. # Description: couchdb netdata python.d module
  3. # Author: wohali <wohali@apache.org>
  4. # Thanks to l2isbad for good examples :)
  5. from collections import namedtuple, defaultdict
  6. from json import loads
  7. from threading import Thread
  8. from socket import gethostbyname, gaierror
  9. try:
  10. from queue import Queue
  11. except ImportError:
  12. from Queue import Queue
  13. from bases.FrameworkServices.UrlService import UrlService
  14. # default module values (can be overridden per job in `config`)
  15. update_every = 1
  16. priority = 60000
  17. retries = 60
  18. METHODS = namedtuple('METHODS', ['get_data', 'url', 'stats'])
  19. OVERVIEW_STATS = [
  20. 'couchdb.database_reads.value',
  21. 'couchdb.database_writes.value',
  22. 'couchdb.httpd.view_reads.value'
  23. 'couchdb.httpd_request_methods.COPY.value',
  24. 'couchdb.httpd_request_methods.DELETE.value',
  25. 'couchdb.httpd_request_methods.GET.value',
  26. 'couchdb.httpd_request_methods.HEAD.value',
  27. 'couchdb.httpd_request_methods.OPTIONS.value',
  28. 'couchdb.httpd_request_methods.POST.value',
  29. 'couchdb.httpd_request_methods.PUT.value',
  30. 'couchdb.httpd_status_codes.200.value',
  31. 'couchdb.httpd_status_codes.201.value',
  32. 'couchdb.httpd_status_codes.202.value',
  33. 'couchdb.httpd_status_codes.204.value',
  34. 'couchdb.httpd_status_codes.206.value',
  35. 'couchdb.httpd_status_codes.301.value',
  36. 'couchdb.httpd_status_codes.302.value',
  37. 'couchdb.httpd_status_codes.304.value',
  38. 'couchdb.httpd_status_codes.400.value',
  39. 'couchdb.httpd_status_codes.401.value',
  40. 'couchdb.httpd_status_codes.403.value',
  41. 'couchdb.httpd_status_codes.404.value',
  42. 'couchdb.httpd_status_codes.405.value',
  43. 'couchdb.httpd_status_codes.406.value',
  44. 'couchdb.httpd_status_codes.409.value',
  45. 'couchdb.httpd_status_codes.412.value',
  46. 'couchdb.httpd_status_codes.413.value',
  47. 'couchdb.httpd_status_codes.414.value',
  48. 'couchdb.httpd_status_codes.415.value',
  49. 'couchdb.httpd_status_codes.416.value',
  50. 'couchdb.httpd_status_codes.417.value',
  51. 'couchdb.httpd_status_codes.500.value',
  52. 'couchdb.httpd_status_codes.501.value',
  53. 'couchdb.open_os_files.value',
  54. 'couch_replicator.jobs.running.value',
  55. 'couch_replicator.jobs.pending.value',
  56. 'couch_replicator.jobs.crashed.value',
  57. ]
  58. SYSTEM_STATS = [
  59. 'context_switches',
  60. 'run_queue',
  61. 'ets_table_count',
  62. 'reductions',
  63. 'memory.atom',
  64. 'memory.atom_used',
  65. 'memory.binary',
  66. 'memory.code',
  67. 'memory.ets',
  68. 'memory.other',
  69. 'memory.processes',
  70. 'io_input',
  71. 'io_output',
  72. 'os_proc_count',
  73. 'process_count',
  74. 'internal_replication_jobs'
  75. ]
  76. DB_STATS = [
  77. 'doc_count',
  78. 'doc_del_count',
  79. 'sizes.file',
  80. 'sizes.external',
  81. 'sizes.active'
  82. ]
  83. ORDER = [
  84. 'activity',
  85. 'request_methods',
  86. 'response_codes',
  87. 'active_tasks',
  88. 'replicator_jobs',
  89. 'open_files',
  90. 'db_sizes_file',
  91. 'db_sizes_external',
  92. 'db_sizes_active',
  93. 'db_doc_counts',
  94. 'db_doc_del_counts',
  95. 'erlang_memory',
  96. 'erlang_proc_counts',
  97. 'erlang_peak_msg_queue',
  98. 'erlang_reductions'
  99. ]
  100. CHARTS = {
  101. 'activity': {
  102. 'options': [None, 'Overall Activity', 'req/s',
  103. 'dbactivity', 'couchdb.activity', 'stacked'],
  104. 'lines': [
  105. ['couchdb_database_reads', 'DB reads', 'incremental'],
  106. ['couchdb_database_writes', 'DB writes', 'incremental'],
  107. ['couchdb_httpd_view_reads', 'View reads', 'incremental']
  108. ]
  109. },
  110. 'request_methods': {
  111. 'options': [None, 'HTTP request methods', 'req/s',
  112. 'httptraffic', 'couchdb.request_methods',
  113. 'stacked'],
  114. 'lines': [
  115. ['couchdb_httpd_request_methods_COPY', 'COPY', 'incremental'],
  116. ['couchdb_httpd_request_methods_DELETE', 'DELETE', 'incremental'],
  117. ['couchdb_httpd_request_methods_GET', 'GET', 'incremental'],
  118. ['couchdb_httpd_request_methods_HEAD', 'HEAD', 'incremental'],
  119. ['couchdb_httpd_request_methods_OPTIONS', 'OPTIONS',
  120. 'incremental'],
  121. ['couchdb_httpd_request_methods_POST', 'POST', 'incremental'],
  122. ['couchdb_httpd_request_methods_PUT', 'PUT', 'incremental']
  123. ]
  124. },
  125. 'response_codes': {
  126. 'options': [None, 'HTTP response status codes', 'resp/s',
  127. 'httptraffic', 'couchdb.response_codes',
  128. 'stacked'],
  129. 'lines': [
  130. ['couchdb_httpd_status_codes_200', '200 OK', 'incremental'],
  131. ['couchdb_httpd_status_codes_201', '201 Created', 'incremental'],
  132. ['couchdb_httpd_status_codes_202', '202 Accepted', 'incremental'],
  133. ['couchdb_httpd_status_codes_2xx', 'Other 2xx Success',
  134. 'incremental'],
  135. ['couchdb_httpd_status_codes_3xx', '3xx Redirection',
  136. 'incremental'],
  137. ['couchdb_httpd_status_codes_4xx', '4xx Client error',
  138. 'incremental'],
  139. ['couchdb_httpd_status_codes_5xx', '5xx Server error',
  140. 'incremental']
  141. ]
  142. },
  143. 'open_files': {
  144. 'options': [None, 'Open files', 'files',
  145. 'ops', 'couchdb.open_files', 'line'],
  146. 'lines': [
  147. ['couchdb_open_os_files', '# files', 'absolute']
  148. ]
  149. },
  150. 'active_tasks': {
  151. 'options': [None, 'Active task breakdown', 'tasks',
  152. 'ops', 'couchdb.active_tasks', 'stacked'],
  153. 'lines': [
  154. ['activetasks_indexer', 'Indexer', 'absolute'],
  155. ['activetasks_database_compaction', 'DB Compaction', 'absolute'],
  156. ['activetasks_replication', 'Replication', 'absolute'],
  157. ['activetasks_view_compaction', 'View Compaction', 'absolute']
  158. ]
  159. },
  160. 'replicator_jobs': {
  161. 'options': [None, 'Replicator job breakdown', 'jobs',
  162. 'ops', 'couchdb.replicator_jobs', 'stacked'],
  163. 'lines': [
  164. ['couch_replicator_jobs_running', 'Running', 'absolute'],
  165. ['couch_replicator_jobs_pending', 'Pending', 'absolute'],
  166. ['couch_replicator_jobs_crashed', 'Crashed', 'absolute'],
  167. ['internal_replication_jobs', 'Internal replication jobs',
  168. 'absolute']
  169. ]
  170. },
  171. 'erlang_memory': {
  172. 'options': [None, 'Erlang VM memory usage', 'bytes',
  173. 'erlang', 'couchdb.erlang_vm_memory', 'stacked'],
  174. 'lines': [
  175. ['memory_atom', 'atom', 'absolute'],
  176. ['memory_binary', 'binaries', 'absolute'],
  177. ['memory_code', 'code', 'absolute'],
  178. ['memory_ets', 'ets', 'absolute'],
  179. ['memory_processes', 'procs', 'absolute'],
  180. ['memory_other', 'other', 'absolute']
  181. ]
  182. },
  183. 'erlang_reductions': {
  184. 'options': [None, 'Erlang reductions', 'count',
  185. 'erlang', 'couchdb.reductions', 'line'],
  186. 'lines': [
  187. ['reductions', 'reductions', 'incremental']
  188. ]
  189. },
  190. 'erlang_proc_counts': {
  191. 'options': [None, 'Process counts', 'count',
  192. 'erlang', 'couchdb.proccounts', 'line'],
  193. 'lines': [
  194. ['os_proc_count', 'OS procs', 'absolute'],
  195. ['process_count', 'erl procs', 'absolute']
  196. ]
  197. },
  198. 'erlang_peak_msg_queue': {
  199. 'options': [None, 'Peak message queue size', 'count',
  200. 'erlang', 'couchdb.peakmsgqueue',
  201. 'line'],
  202. 'lines': [
  203. ['peak_msg_queue', 'peak size', 'absolute']
  204. ]
  205. },
  206. # Lines for the following are added as part of check()
  207. 'db_sizes_file': {
  208. 'options': [None, 'Database sizes (file)', 'KB',
  209. 'perdbstats', 'couchdb.db_sizes_file', 'line'],
  210. 'lines': []
  211. },
  212. 'db_sizes_external': {
  213. 'options': [None, 'Database sizes (external)', 'KB',
  214. 'perdbstats', 'couchdb.db_sizes_external', 'line'],
  215. 'lines': []
  216. },
  217. 'db_sizes_active': {
  218. 'options': [None, 'Database sizes (active)', 'KB',
  219. 'perdbstats', 'couchdb.db_sizes_active', 'line'],
  220. 'lines': []
  221. },
  222. 'db_doc_counts': {
  223. 'options': [None, 'Database # of docs', 'docs',
  224. 'perdbstats', 'couchdb_db_doc_count', 'line'],
  225. 'lines': []
  226. },
  227. 'db_doc_del_counts': {
  228. 'options': [None, 'Database # of deleted docs', 'docs',
  229. 'perdbstats', 'couchdb_db_doc_del_count', 'line'],
  230. 'lines': []
  231. }
  232. }
  233. class Service(UrlService):
  234. def __init__(self, configuration=None, name=None):
  235. UrlService.__init__(self, configuration=configuration, name=name)
  236. self.order = ORDER
  237. self.definitions = CHARTS
  238. self.host = self.configuration.get('host', '127.0.0.1')
  239. self.port = self.configuration.get('port', 5984)
  240. self.node = self.configuration.get('node', 'couchdb@127.0.0.1')
  241. self.scheme = self.configuration.get('scheme', 'http')
  242. self.user = self.configuration.get('user')
  243. self.password = self.configuration.get('pass')
  244. try:
  245. self.dbs = self.configuration.get('databases').split(' ')
  246. except (KeyError, AttributeError):
  247. self.dbs = []
  248. def check(self):
  249. if not (self.host and self.port):
  250. self.error('Host is not defined in the module configuration file')
  251. return False
  252. try:
  253. self.host = gethostbyname(self.host)
  254. except gaierror as error:
  255. self.error(str(error))
  256. return False
  257. self.url = '{scheme}://{host}:{port}'.format(scheme=self.scheme,
  258. host=self.host,
  259. port=self.port)
  260. stats = self.url + '/_node/{node}/_stats'.format(node=self.node)
  261. active_tasks = self.url + '/_active_tasks'
  262. system = self.url + '/_node/{node}/_system'.format(node=self.node)
  263. self.methods = [METHODS(get_data=self._get_overview_stats,
  264. url=stats,
  265. stats=OVERVIEW_STATS),
  266. METHODS(get_data=self._get_active_tasks_stats,
  267. url=active_tasks,
  268. stats=None),
  269. METHODS(get_data=self._get_overview_stats,
  270. url=system,
  271. stats=SYSTEM_STATS),
  272. METHODS(get_data=self._get_dbs_stats,
  273. url=self.url,
  274. stats=DB_STATS)]
  275. # must initialise manager before using _get_raw_data
  276. self._manager = self._build_manager()
  277. self.dbs = [db for db in self.dbs
  278. if self._get_raw_data(self.url + '/' + db)]
  279. for db in self.dbs:
  280. self.definitions['db_sizes_file']['lines'].append(
  281. ['db_'+db+'_sizes_file', db, 'absolute', 1, 1000]
  282. )
  283. self.definitions['db_sizes_external']['lines'].append(
  284. ['db_'+db+'_sizes_external', db, 'absolute', 1, 1000]
  285. )
  286. self.definitions['db_sizes_active']['lines'].append(
  287. ['db_'+db+'_sizes_active', db, 'absolute', 1, 1000]
  288. )
  289. self.definitions['db_doc_counts']['lines'].append(
  290. ['db_'+db+'_doc_count', db, 'absolute']
  291. )
  292. self.definitions['db_doc_del_counts']['lines'].append(
  293. ['db_'+db+'_doc_del_count', db, 'absolute']
  294. )
  295. return UrlService.check(self)
  296. def _get_data(self):
  297. threads = list()
  298. queue = Queue()
  299. result = dict()
  300. for method in self.methods:
  301. th = Thread(target=method.get_data,
  302. args=(queue, method.url, method.stats))
  303. th.start()
  304. threads.append(th)
  305. for thread in threads:
  306. thread.join()
  307. result.update(queue.get())
  308. # self.info('couchdb result = ' + str(result))
  309. return result or None
  310. def _get_overview_stats(self, queue, url, stats):
  311. raw_data = self._get_raw_data(url)
  312. if not raw_data:
  313. return queue.put(dict())
  314. data = loads(raw_data)
  315. to_netdata = self._fetch_data(raw_data=data, metrics=stats)
  316. if 'message_queues' in data:
  317. to_netdata['peak_msg_queue'] = get_peak_msg_queue(data)
  318. return queue.put(to_netdata)
  319. def _get_active_tasks_stats(self, queue, url, _):
  320. taskdict = defaultdict(int)
  321. taskdict["activetasks_indexer"] = 0
  322. taskdict["activetasks_database_compaction"] = 0
  323. taskdict["activetasks_replication"] = 0
  324. taskdict["activetasks_view_compaction"] = 0
  325. raw_data = self._get_raw_data(url)
  326. if not raw_data:
  327. return queue.put(dict())
  328. data = loads(raw_data)
  329. for task in data:
  330. taskdict["activetasks_" + task["type"]] += 1
  331. return queue.put(dict(taskdict))
  332. def _get_dbs_stats(self, queue, url, stats):
  333. to_netdata = {}
  334. for db in self.dbs:
  335. raw_data = self._get_raw_data(url + '/' + db)
  336. if not raw_data:
  337. continue
  338. data = loads(raw_data)
  339. for metric in stats:
  340. value = data
  341. metrics_list = metric.split('.')
  342. try:
  343. for m in metrics_list:
  344. value = value[m]
  345. except KeyError as e:
  346. self.debug('cannot process ' + metric + ' for ' + db
  347. + ": " + str(e))
  348. continue
  349. metric_name = 'db_{0}_{1}'.format(db, '_'.join(metrics_list))
  350. to_netdata[metric_name] = value
  351. return queue.put(to_netdata)
  352. def _fetch_data(self, raw_data, metrics):
  353. data = dict()
  354. for metric in metrics:
  355. value = raw_data
  356. metrics_list = metric.split('.')
  357. try:
  358. for m in metrics_list:
  359. value = value[m]
  360. except KeyError as e:
  361. self.debug('cannot process ' + metric + ': ' + str(e))
  362. continue
  363. # strip off .value from end of stat
  364. if metrics_list[-1] == 'value':
  365. metrics_list = metrics_list[:-1]
  366. # sum up 3xx/4xx/5xx
  367. if metrics_list[0:2] == ['couchdb', 'httpd_status_codes'] and \
  368. int(metrics_list[2]) > 202:
  369. metrics_list[2] = '{0}xx'.format(int(metrics_list[2]) // 100)
  370. if '_'.join(metrics_list) in data:
  371. data['_'.join(metrics_list)] += value
  372. else:
  373. data['_'.join(metrics_list)] = value
  374. else:
  375. data['_'.join(metrics_list)] = value
  376. return data
  377. def get_peak_msg_queue(data):
  378. maxsize = 0
  379. queues = data['message_queues']
  380. for queue in iter(queues.values()):
  381. if isinstance(queue, dict) and 'count' in queue:
  382. value = queue['count']
  383. elif isinstance(queue, int):
  384. value = queue
  385. else:
  386. continue
  387. maxsize = max(maxsize, value)
  388. return maxsize