Source code for ruxit_mongodb

"""
OneAgent MongoDB plugin
=======================

Gathers MongoDB stats using two MongoDB commands:

* dbStats -  https://docs.mongodb.com/manual/reference/command/dbStats/
* serverStatus - https://docs.mongodb.com/manual/reference/command/serverStatus/

Running the plugin requires specifying:

* user credentials - *clusterMonitor* role is required. If authorization is disabled on MongoDB then leave *user* and *password* fields empty
* port - access to the MongoDB via localhost interface is required

This is a singleton type plugin - one instance will be created even if multiple MongoDB process
groups instances are detected.

"""

import pymongo
from ruxit.api.exceptions import AuthException, ConfigException
from ruxit.api.data import PluginMeasurement, PluginProperty, MEAttribute
from ruxit.api.base_plugin import BasePlugin
from ruxit.api.selectors import ListenPortSelector
import re
from collections import namedtuple
from contextlib import closing
from ruxit.api.snapshot import parse_port_bindings


import logging
log = logging.getLogger(__name__)

QueryDesc = namedtuple("QueryDesc", ["timeseries_key", "per_second_result_flag"])
"""
Namedtuple used for storing informations about timeseries metric, including corresponding timeseries key and information about metric type.
"""

[docs]class MongodbPlugin(BasePlugin): """ MongoDB plugin class. This plugin is stateful, however, the state is limited to processing of JSON and received configuration together with maintaining database connection. """
[docs] def initialize(self, **kwargs): config = kwargs['config'] self.timeseries = kwargs['json_config']['metrics'] self.process_types = kwargs['activation_context'].value.process_types self.process_snapshot = kwargs['process_snapshot'] if config['port'] == '': raise ConfigException("port is empty") self.port = config['port'] self.auth = None if 'auth_user' in config and 'auth_password' in config: self.user = config['auth_user'] self.password = config['auth_password'] else: raise ConfigException("user or password is empty") self.server_status_metrics = {} self.db_stats_metrics = {} self.regex_pattern = "\[\'([^\']*)\'\]" self.mongodb_auth_error_codes = [13, 18] self.initialize_metrics()
[docs] def initialize_metrics(self): """ On plugin creation corresponding plugin.json file is processed in order to extract statistics that are related to server status and particular database. """ server_status_metrics = {} db_stats_metrics = {} try: for metric in self.timeseries: query = metric['source']['query'] timeseries_key = metric['timeseries']['key'] per_second_result_flag = metric.get('source', {}).get('persecond', False) query_start_index = query.find('[') if query.startswith('server_status'): if query[query_start_index:] in server_status_metrics and not query[query_start_index:].endswith('2'): server_status_metrics[query[query_start_index:]] = QueryDesc(timeseries_key, per_second_result_flag) else: server_status_metrics[query[query_start_index:]] = QueryDesc(timeseries_key, per_second_result_flag) elif query.startswith('db_stats'): if query[query_start_index:] in db_stats_metrics and not query[query_start_index:].endswith('2'): db_stats_metrics[query[query_start_index:]] = QueryDesc(timeseries_key, per_second_result_flag) else: db_stats_metrics[query[query_start_index:]] = QueryDesc(timeseries_key, per_second_result_flag) except KeyError: log.info(self, exc_info=1) self.server_status_metrics = server_status_metrics self.db_stats_metrics = db_stats_metrics
def _createpm(self, *args, **kwargs): return PluginMeasurement(*args, entity_selector=ListenPortSelector(self.port, self.process_types), **kwargs) def _add_result(self, query_desc, value, dim=None): if value is not None: if query_desc.per_second_result_flag == 1: self.results_builder.add_per_second_result(self._createpm( key=query_desc.timeseries_key, value=value, dimensions=dim) ) else: self.results_builder.add_absolute_result(self._createpm( key=query_desc.timeseries_key, value=value, dimensions=dim) ) else: log.info('Metric ' + query_desc.timeseries_key + ' is not available')
[docs] def query(self, **kwargs): """ Main method that creates connection to MongoDB engine based on provided credatials and controls plugin execution by invoking methods responsible for data collection. Raises: ruxit.api.exceptions.AuthException: MongoDB server returned either 13 or 18 error indicating auth issues ruxit.api.exceptions.ConfigException: unable to connect for other reasons """ uri = self._create_uri() try: with closing(pymongo.MongoClient(uri)) as client: databases = client.database_names() self.get_server_metrics(client) self.get_database_metrics(client, databases) except pymongo.errors.OperationFailure as ex: if ex.code in self.mongodb_auth_error_codes: raise AuthException from ex except pymongo.errors.ServerSelectionTimeoutError as ex: raise ConfigException(ex.args[0]) from ex
def _create_uri(self): for ps_entry in self.process_snapshot.entries: if ps_entry.process_type == 21: port_bindings = parse_port_bindings(ps_entry) for binding in port_bindings: if str(binding[1]) == self.port: return self._url(binding[0]) log.info('PortBindings data: %s', port_bindings) raise ConfigException('Unable to detect MongoDB binding') def _url(self, ip): if not self.user or not self.password: return 'mongodb://{address}:{port}'.format(address=ip, port=self.port) else: return 'mongodb://{user}:{pwd}@{address}:{port}'.format(user=self.user, pwd=self.password, address=ip, port=self.port)
[docs] def get_server_metrics(self, client): """ Collect MongoDB server level metrics and sends results. """ server_status = client.db.command("serverStatus") for query, query_desc in self.server_status_metrics.items(): indices = re.findall(self.regex_pattern, query) value = self.get_value_of_indices_chain(indices, server_status) self._add_result(query_desc, value)
[docs] def get_database_metrics(self, client, databases): """ Iterates over a list of database received in main query, collects and sends database level metrics. """ for database in databases: db = client[database] db_stats = db.command('dbstats') dim = {'database': database} for query, query_desc in self.db_stats_metrics.items(): indices = re.findall(self.regex_pattern, query) value = self.get_value_of_indices_chain(indices, db_stats) self._add_result(query_desc, value, dim)
[docs] def get_value_of_indices_chain(self, indices, dict): for index in indices: if index not in dict: return None dict = dict[index] return dict