]> arthur.barton.de Git - netdata.git/blob - python.d/mongodb.chart.py
66135e51df3b8852a5ca9ad5c7b446321976a1f2
[netdata.git] / python.d / mongodb.chart.py
1 # -*- coding: utf-8 -*-
2 # Description: mongodb netdata python.d module
3 # Author: l2isbad
4
5 from base import SimpleService
6 from copy import deepcopy
7 try:
8     from pymongo import MongoClient
9     from pymongo.errors import PyMongoError
10     PYMONGO = True
11 except ImportError:
12     PYMONGO = False
13
14 # default module values (can be overridden per job in `config`)
15 # update_every = 2
16 priority = 60000
17 retries = 60
18
19 REPLSET_STATES = [
20         ('1', 'primary'),
21         ('8', 'down'),
22         ('2', 'secondary'),
23         ('3', 'recovering'),
24         ('5', 'startup2'),
25         ('4', 'fatal'),
26         ('7', 'arbiter'),
27         ('6', 'unknown'),
28         ('9', 'rollback'),
29         ('10', 'removed'),
30         ('0', 'startup')]
31
32 # charts order (can be overridden if you want less charts, or different order)
33 ORDER = ['read_operations', 'write_operations', 'active_clients', 'journaling_transactions',
34          'journaling_volume', 'background_flush_average', 'background_flush_last', 'background_flush_rate',
35          'wiredtiger_read', 'wiredtiger_write', 'cursors', 'connections', 'memory', 'page_faults',
36          'queued_requests', 'record_moves', 'wiredtiger_cache', 'wiredtiger_pages_evicted', 'asserts',
37          'dbstats_objects', 'tcmalloc_generic', 'tcmalloc_metrics', 'command_total_rate', 'command_failed_rate']
38
39 CHARTS = {
40     'read_operations': {
41         'options': [None, "Received read requests", "requests/s", 'throughput metrics',
42                     'mongodb.read_operations', 'line'],
43         'lines': [
44             ['readWriteOper_query', 'query', 'incremental'],
45             ['readWriteOper_getmore', 'getmore', 'incremental']
46         ]},
47     'write_operations': {
48         'options': [None, "Received write requests", "requests/s", 'throughput metrics',
49                     'mongodb.write_operations', 'line'],
50         'lines': [
51             ['readWriteOper_insert', 'insert', 'incremental'],
52             ['readWriteOper_update', 'update', 'incremental'],
53             ['readWriteOper_delete', 'delete', 'incremental']
54         ]},
55     'active_clients': {
56         'options': [None, "Clients with read or write operations in progress or queued", "clients",
57                     'throughput metrics', 'mongodb.active_clients', 'line'],
58         'lines': [
59             ['activeClients_readers', 'readers', 'absolute'],
60             ['activeClients_writers', 'writers', 'absolute']
61             ]},
62     'journaling_transactions': {
63         'options': [None, "Transactions that have been written to the journal", "commits",
64                     'database performance', 'mongodb.journaling_transactions', 'line'],
65         'lines': [
66             ['journalTrans_commits', 'commits', 'absolute']
67             ]},
68     'journaling_volume': {
69         'options': [None, "Volume of data written to the journal", "MB", 'database performance',
70                     'mongodb.journaling_volume', 'line'],
71         'lines': [
72             ['journalTrans_journaled', 'volume', 'absolute', 1, 100]
73             ]},
74     'background_flush_average': {
75         'options': [None, "Average time taken by flushes to execute", "ms", 'database performance',
76                     'mongodb.background_flush_average', 'line'],
77         'lines': [
78             ['background_flush_average', 'time', 'absolute', 1, 100]
79             ]},
80     'background_flush_last': {
81         'options': [None, "Time taken by the last flush operation to execute", "ms", 'database performance',
82                     'mongodb.background_flush_last', 'line'],
83         'lines': [
84             ['background_flush_last', 'time', 'absolute', 1, 100]
85             ]},
86     'background_flush_rate': {
87         'options': [None, "Flushes rate", "flushes", 'database performance', 'mongodb.background_flush_rate', 'line'],
88         'lines': [
89             ['background_flush_rate', 'flushes', 'incremental', 1, 1]
90             ]},
91     'wiredtiger_read': {
92         'options': [None, "Read tickets in use and remaining", "tickets", 'database performance',
93                     'mongodb.wiredtiger_read', 'stacked'],
94         'lines': [
95             ['wiredTigerRead_available', 'available', 'absolute', 1, 1],
96             ['wiredTigerRead_out', 'inuse', 'absolute', 1, 1]
97             ]},
98     'wiredtiger_write': {
99         'options': [None, "Write tickets in use and remaining", "tickets", 'database performance',
100                     'mongodb.wiredtiger_write', 'stacked'],
101         'lines': [
102             ['wiredTigerWrite_available', 'available', 'absolute', 1, 1],
103             ['wiredTigerWrite_out', 'inuse', 'absolute', 1, 1]
104             ]},
105     'cursors': {
106         'options': [None, "Currently openned cursors, cursors with timeout disabled and timed out cursors",
107                     "cursors", 'database performance', 'mongodb.cursors', 'stacked'],
108         'lines': [
109             ['cursor_total', 'openned', 'absolute', 1, 1],
110             ['cursor_noTimeout', 'notimeout', 'absolute', 1, 1],
111             ['cursor_timedOut', 'timedout', 'incremental', 1, 1]
112             ]},
113     'connections': {
114         'options': [None, "Currently connected clients and unused connections", "connections",
115                     'resource utilization', 'mongodb.connections', 'stacked'],
116         'lines': [
117             ['connections_available', 'unused', 'absolute', 1, 1],
118             ['connections_current', 'connected', 'absolute', 1, 1]
119             ]},
120     'memory': {
121         'options': [None, "Memory metrics", "MB", 'resource utilization', 'mongodb.memory', 'stacked'],
122         'lines': [
123             ['memory_virtual', 'virtual', 'absolute', 1, 1],
124             ['memory_resident', 'resident', 'absolute', 1, 1],
125             ['memory_mapped', 'mapped', 'absolute', 1, 1]
126             ]},
127     'page_faults': {
128         'options': [None, "Number of times MongoDB had to fetch data from disk", "request/s",
129                     'resource utilization', 'mongodb.page_faults', 'line'],
130         'lines': [
131             ['page_faults', 'page_faults', 'incremental', 1, 1]
132             ]},
133     'queued_requests': {
134         'options': [None, "Currently queued read and wrire requests", "requests", 'resource saturation',
135                     'mongodb.queued_requests', 'line'],
136         'lines': [
137             ['currentQueue_readers', 'readers', 'absolute', 1, 1],
138             ['currentQueue_writers', 'writers', 'absolute', 1, 1]
139             ]},
140     'record_moves': {
141         'options': [None, "Number of times documents had to be moved on-disk", "number",
142                     'resource saturation', 'mongodb.record_moves', 'line'],
143         'lines': [
144             ['record_moves', 'moves', 'incremental', 1, 1]
145             ]},
146     'asserts': {
147         'options': [None, "Number of message, warning, regular, corresponding to errors generated"
148                           " by users assertions raised", "number", 'errors (asserts)', 'mongodb.asserts', 'line'],
149         'lines': [
150             ['errors_msg', 'msg', 'incremental', 1, 1],
151             ['errors_warning', 'warning', 'incremental', 1, 1],
152             ['errors_regular', 'regular', 'incremental', 1, 1],
153             ['errors_user', 'user', 'incremental', 1, 1]
154             ]},
155     'wiredtiger_cache': {
156         'options': [None, "Amount of space taken by cached data and by dirty data in the cache",
157                     "KB", 'resource utilization', 'mongodb.wiredtiger_cache', 'stacked'],
158         'lines': [
159             ['wiredTiger_bytes_in_cache', 'cached', 'absolute', 1, 1024],
160             ['wiredTiger_dirty_in_cache', 'dirty', 'absolute', 1, 1024]
161             ]},
162     'wiredtiger_pages_evicted': {
163         'options': [None, "Pages evicted from the cache",
164                     "pages", 'resource utilization', 'mongodb.wiredtiger_pages_evicted', 'stacked'],
165         'lines': [
166             ['wiredTiger_unmodified_pages_evicted', 'unmodified', 'absolute', 1, 1],
167             ['wiredTiger_modified_pages_evicted', 'modified', 'absolute', 1, 1]
168             ]},
169     'dbstats_objects': {
170         'options': [None, "Number of documents in the database among all the collections", "documents",
171                     'storage size metrics', 'mongodb.dbstats_objects', 'stacked'],
172         'lines': [
173             ]},
174     'tcmalloc_generic': {
175         'options': [None, "Tcmalloc generic metrics", "MB", 'tcmalloc', 'mongodb.tcmalloc_generic', 'stacked'],
176         'lines': [
177             ['current_allocated_bytes', 'allocated', 'absolute', 1, 1048576],
178             ['heap_size', 'heap_size', 'absolute', 1, 1048576]
179             ]},
180     'tcmalloc_metrics': {
181         'options': [None, "Tcmalloc metrics", "KB", 'tcmalloc', 'mongodb.tcmalloc_metrics', 'stacked'],
182         'lines': [
183             ['central_cache_free_bytes', 'central_cache_free', 'absolute', 1, 1024],
184             ['current_total_thread_cache_bytes', 'current_total_thread_cache', 'absolute', 1, 1024],
185             ['pageheap_free_bytes', 'pageheap_free', 'absolute', 1, 1024],
186             ['pageheap_unmapped_bytes', 'pageheap_unmapped', 'absolute', 1, 1024],
187             ['thread_cache_free_bytes', 'thread_cache_free', 'absolute', 1, 1024],
188             ['transfer_cache_free_bytes', 'transfer_cache_free', 'absolute', 1, 1024]
189             ]},
190     'command_total_rate': {
191         'options': [None, "Commands total rate", "commands/s", 'commands', 'mongodb.command_total_rate', 'stacked'],
192         'lines': [
193             ['count_total', 'count', 'incremental', 1, 1],
194             ['createIndexes_total', 'createIndexes', 'incremental', 1, 1],
195             ['delete_total', 'delete', 'incremental', 1, 1],
196             ['eval_total', 'eval', 'incremental', 1, 1],
197             ['findAndModify_total', 'findAndModify', 'incremental', 1, 1],
198             ['insert_total', 'insert', 'incremental', 1, 1],
199             ['update_total', 'update', 'incremental', 1, 1]
200             ]},
201     'command_failed_rate': {
202         'options': [None, "Commands failed rate", "commands/s", 'commands', 'mongodb.command_failed_rate', 'stacked'],
203         'lines': [
204             ['count_failed', 'count', 'incremental', 1, 1],
205             ['createIndexes_failed', 'createIndexes', 'incremental', 1, 1],
206             ['delete_dailed', 'delete', 'incremental', 1, 1],
207             ['eval_failed', 'eval', 'incremental', 1, 1],
208             ['findAndModify_failed', 'findAndModify', 'incremental', 1, 1],
209             ['insert_failed', 'insert', 'incremental', 1, 1],
210             ['update_failed', 'update', 'incremental', 1, 1]
211             ]}
212 }
213
214
215 class Service(SimpleService):
216     def __init__(self, configuration=None, name=None):
217         SimpleService.__init__(self, configuration=configuration, name=name)
218         self.user = self.configuration.get('user')
219         self.password = self.configuration.get('pass')
220         self.host = self.configuration.get('host', '127.0.0.1')
221         self.port = self.configuration.get('port', 27017)
222         self.timeout = self.configuration.get('timeout', 100)
223
224     def check(self):
225         if not PYMONGO:
226             self.error('Pymongo module is needed to use mongodb.chart.py')
227             return False
228
229         self.connection, server_status, error = self._create_connection()
230         if error:
231             self.error(error)
232             return False
233
234         self.repl = 'repl' in server_status
235         self.databases = self.connection.database_names()
236         self._create_charts(server_status)
237
238         return True
239
240     def _create_charts(self, server_status):
241
242         self.order = ORDER[:]
243         self.definitions = deepcopy(CHARTS)
244         self.ss = dict()
245
246         for elem in ['dur', 'backgroundFlushing', 'wiredTiger', 'tcmalloc', 'cursor', 'commands']:
247             self.ss[elem] = in_server_status(elem, server_status)
248
249         if not self.ss['dur']:
250             self.order.remove('journaling_transactions')
251             self.order.remove('journaling_volume')
252
253         if not self.ss['backgroundFlushing']:
254             self.order.remove('background_flush_average')
255             self.order.remove('background_flush_last')
256
257         if not self.ss['cursor']:
258             self.order.remove('cursors')
259
260         if not self.ss['wiredTiger']:
261             self.order.remove('wiredtiger_write')
262             self.order.remove('wiredtiger_read')
263             self.order.remove('wiredtiger_cache')
264
265         if not self.ss['tcmalloc']:
266             self.order.remove('tcmalloc_generic')
267             self.order.remove('tcmalloc_metrics')
268
269         if not self.ss['commands']:
270             self.order.remove('command_total_rate')
271             self.order.remove('command_failed_rate')
272
273         for dbase in self.databases:
274             self.order.append('_'.join([dbase, 'dbstats']))
275             self.definitions['_'.join([dbase, 'dbstats'])] = {
276                     'options': [None, "%s: size of all documents, indexes, extents" % dbase, "KB",
277                                 'storage size metrics', 'mongodb.dbstats', 'line'],
278                     'lines': [
279                              ['_'.join([dbase, 'dataSize']), 'documents', 'absolute', 1, 1024],
280                              ['_'.join([dbase, 'indexSize']), 'indexes', 'absolute', 1, 1024],
281                              ['_'.join([dbase, 'storageSize']), 'extents', 'absolute', 1, 1024]
282                       ]}
283             self.definitions['dbstats_objects']['lines'].append(['_'.join([dbase, 'objects']), dbase, 'absolute'])
284
285         if server_status.get('repl'):
286             hosts = server_status['repl']['hosts']
287             for host in hosts:
288                 chart_name = '_'.join([host, 'state'])
289                 self.order.append(chart_name)
290                 self.definitions[chart_name] = {
291                        'options': [None, "%s state" % host, "state",
292                                    'replication', 'mongodb.replication_state', 'line'],
293                        'lines': [
294                          ]}
295                 for state, description in REPLSET_STATES:
296                     self.definitions[chart_name]['lines'].append(['_'.join([host, 'state', state]), description, 'absolute', 1, 1])
297
298
299
300     def _get_raw_data(self):
301         raw_data = dict()
302
303         raw_data.update(self.get_serverstatus_() or dict())
304         raw_data.update(self.get_dbstats_() or dict())
305         raw_data.update(self.get_replsetgetstatus_() or dict())
306
307         return raw_data or None
308
309     def get_serverstatus_(self):
310         raw_data = dict()
311         try:
312             raw_data['serverStatus'] = self.connection.admin.command('serverStatus')
313         except PyMongoError:
314             return None
315         else:
316             return raw_data
317
318     def get_dbstats_(self):
319         raw_data = dict()
320         raw_data['dbStats'] = dict()
321         try:
322             for dbase in self.databases:
323                 raw_data['dbStats'][dbase] = self.connection[dbase].command('dbStats')
324         except PyMongoError:
325             return None
326         else:
327             return raw_data
328
329     def get_replsetgetstatus_(self):
330         if not self.repl:
331             return None
332
333         raw_data = dict()
334         try:
335             raw_data['replSetGetStatus'] = self.connection.admin.command('replSetGetStatus')
336         except PyMongoError:
337             return None
338         else:
339             return raw_data
340
341     def _get_data(self):
342         """
343         :return: dict
344         """
345         raw_data = self._get_raw_data()
346
347         if not raw_data:
348             return None
349
350         to_netdata = dict()
351         serverStatus = raw_data['serverStatus']
352         dbStats = raw_data['dbStats']
353         replSetGetStatus = raw_data.get('replSetGetStatus')
354
355         # serverStatus
356         to_netdata.update(update_dict_key(serverStatus['opcounters'], 'readWriteOper'))
357         to_netdata.update(update_dict_key(serverStatus['globalLock']['activeClients'], 'activeClients'))
358         to_netdata.update(update_dict_key(serverStatus['connections'], 'connections'))
359         to_netdata.update(update_dict_key(serverStatus['mem'], 'memory'))
360         to_netdata.update(update_dict_key(serverStatus['globalLock']['currentQueue'], 'currentQueue'))
361         to_netdata.update(update_dict_key(serverStatus['asserts'], 'errors'))
362         to_netdata['page_faults'] = serverStatus['extra_info']['page_faults']
363         to_netdata['record_moves'] = serverStatus['metrics']['record']['moves']
364
365         if self.ss['dur']:
366             to_netdata['journalTrans_commits'] = serverStatus['dur']['commits']
367             to_netdata['journalTrans_journaled'] = int(serverStatus['dur']['journaledMB'] * 100)
368
369         if self.ss['backgroundFlushing']:
370             to_netdata['background_flush_average'] = int(serverStatus['backgroundFlushing']['average_ms'] * 100)
371             to_netdata['background_flush_last'] = int(serverStatus['backgroundFlushing']['last_ms'] * 100)
372             to_netdata['background_flush_rate'] = serverStatus['backgroundFlushing']['flushes']
373
374         if self.ss['cursor']:
375             to_netdata['cursor_timedOut'] = serverStatus['metrics']['cursor']['timedOut']
376             to_netdata.update(update_dict_key(serverStatus['metrics']['cursor']['open'], 'cursor'))
377
378         if self.ss['wiredTiger']:
379             wired_tiger = serverStatus['wiredTiger']
380             to_netdata.update(update_dict_key(serverStatus['wiredTiger']['concurrentTransactions']['read'],
381                                               'wiredTigerRead'))
382             to_netdata.update(update_dict_key(serverStatus['wiredTiger']['concurrentTransactions']['write'],
383                                               'wiredTigerWrite'))
384             to_netdata['wiredTiger_bytes_in_cache'] = wired_tiger['cache']['bytes currently in the cache']
385             to_netdata['wiredTiger_dirty_in_cache'] = wired_tiger['cache']['tracked dirty bytes in the cache']
386             to_netdata['wiredTiger_unmodified_pages_evicted'] = wired_tiger['cache']['unmodified pages evicted']
387             to_netdata['wiredTiger_modified_pages_evicted'] = wired_tiger['cache']['modified pages evicted']
388
389         if self.ss['tcmalloc']:
390             to_netdata.update(serverStatus['tcmalloc']['generic'])
391             to_netdata.update(dict([(k, v) for k, v in serverStatus['tcmalloc']['tcmalloc'].items()
392                                     if int_or_float(v)]))
393
394         if self.ss['commands']:
395             for elem in ['count', 'createIndexes', 'delete', 'eval', 'findAndModify', 'insert', 'update']:
396                 to_netdata.update(update_dict_key(serverStatus['metrics']['commands'][elem], elem))
397
398         # dbStats
399         for dbase in dbStats:
400             to_netdata.update(update_dict_key(dbStats[dbase], dbase))
401
402         # replSetGetStatus
403         if replSetGetStatus:
404             members = replSetGetStatus['members']
405             for member in members:
406                 for elem in REPLSET_STATES:
407                     state = elem[0]
408                     to_netdata.update({'_'.join([member['name'], 'state', state]): 0})
409                 to_netdata.update({'_'.join([member['name'], 'state', str(member['state'])]): member['state']})
410
411         return to_netdata
412
413     def _create_connection(self):
414         conn_vars = {'host': self.host, 'port': self.port}
415         if hasattr(MongoClient, 'server_selection_timeout'):
416             conn_vars.update({'serverselectiontimeoutms': self.timeout})
417         try:
418             connection = MongoClient(**conn_vars)
419             if self.user and self.password:
420                 connection.admin.authenticate(name=self.user, password=self.password)
421           #  elif self.user:
422           #      connection.admin.authenticate(name=self.user, mechanism='MONGODB-X509')
423             server_status = connection.admin.command('serverStatus')
424         except PyMongoError as error:
425             return None, None, str(error)
426         else:
427             return connection, server_status, None
428
429
430 def update_dict_key(collection, string):
431     return dict([('_'.join([string, k]), int(round(v))) for k, v in collection.items() if int_or_float(v)])
432
433
434 def int_or_float(value):
435     return isinstance(value, (int, float))
436
437
438 def in_server_status(elem, server_status):
439     return elem in server_status or elem in server_status['metrics']