Hello community,

here is the log from the commit of package hanadb_exporter for openSUSE:Factory 
checked in at 2019-11-04 17:15:35
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/hanadb_exporter (Old)
 and      /work/SRC/openSUSE:Factory/.hanadb_exporter.new.2990 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "hanadb_exporter"

Mon Nov  4 17:15:35 2019 rev:4 rq:745027 version:0.5.0

Changes:
--------
--- /work/SRC/openSUSE:Factory/hanadb_exporter/hanadb_exporter.changes  
2019-11-03 12:08:35.328267267 +0100
+++ 
/work/SRC/openSUSE:Factory/.hanadb_exporter.new.2990/hanadb_exporter.changes    
    2019-11-04 17:15:52.292867841 +0100
@@ -1,0 +2,12 @@
+Fri Oct 25 06:14:03 UTC 2019 - Xabier Arbulu <[email protected]>
+
+- Version 0.5.0 Add the option to export metrics from multiple
+databases/tenants 
+
+-------------------------------------------------------------------
+Thu Oct 24 03:00:45 UTC 2019 - Xabier Arbulu <[email protected]>
+
+- Version 0.4.1 Add new metadata labels to the metrics (sid, instance
+number and databse name) 
+
+-------------------------------------------------------------------

Old:
----
  hanadb_exporter-0.4.0.tar.gz

New:
----
  hanadb_exporter-0.5.0.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ hanadb_exporter.spec ++++++
--- /var/tmp/diff_new_pack.v7MBQp/_old  2019-11-04 17:15:52.916868507 +0100
+++ /var/tmp/diff_new_pack.v7MBQp/_new  2019-11-04 17:15:52.920868511 +0100
@@ -26,7 +26,7 @@
 %endif
 
 Name:           hanadb_exporter
-Version:        0.4.0
+Version:        0.5.0
 Release:        0
 Summary:        SAP HANA database metrics exporter
 License:        Apache-2.0

++++++ hanadb_exporter-0.4.0.tar.gz -> hanadb_exporter-0.5.0.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/hanadb_exporter-0.4.0/README.md 
new/hanadb_exporter-0.5.0/README.md
--- old/hanadb_exporter-0.4.0/README.md 2019-10-24 04:34:18.541251898 +0200
+++ new/hanadb_exporter-0.5.0/README.md 2019-10-29 14:44:08.123074953 +0100
@@ -7,10 +7,14 @@
 Prometheus exporter written in Python, to export SAP HANA database metrics. The
 project is based in the official prometheus exporter: 
[prometheus_client](https://github.com/prometheus/client_python).
 
+The exporter is able to export the metrics from more than 1 database/tenant if 
the `multi_tenant` option is enabled in the configuration file (enabled by 
default).
+
+The labels `sid` (system identifier), `insnr` (instance number), 
`database_name` (database name) and `host` (machine hostname) will be exported 
for all the metrics.
+
 
 ## Prerequisites
 
-1. A running and reachable SAP HANA database. Running the exporter in the
+1. A running and reachable SAP HANA database (single or multi container). 
Running the exporter in the
 same machine where the HANA database is running is recommended. Ideally each 
database
 should be monitored by one exporter.
 
@@ -53,6 +57,8 @@
 An example of `config.json` available in 
[config.json.example](config.json.example). Here the most
 important items in the configuration file:
   - `exposition_port`: Port where the prometheus exporter will be exposed 
(8001 by default).
+  - `multi_tenant`: Export the metrics from other tenants. To use this the 
connection must be done with the System Database (port 30013).
+  - `timeout`: Timeout to connect to the database. After this time the app 
will fail (even in daemon mode).
   - `hana.host`: Address of the SAP HANA database.
   - `hana.port`: Port where the SAP HANA database is exposed.
   - `hana.user`: An existing user with access right to the SAP HANA database.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/hanadb_exporter-0.4.0/config.json.example 
new/hanadb_exporter-0.5.0/config.json.example
--- old/hanadb_exporter-0.4.0/config.json.example       2019-10-24 
04:34:18.541251898 +0200
+++ new/hanadb_exporter-0.5.0/config.json.example       2019-10-29 
14:44:08.123074953 +0100
@@ -1,8 +1,10 @@
 {
   "exposition_port": 8001,
+  "multi_tenant": true,
+  "timeout": 600,
   "hana": {
     "host": "localhost",
-    "port": 30015,
+    "port": 30013,
     "user": "SYSTEM",
     "password": "PASSWORD"
   },
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/hanadb_exporter-0.4.0/hanadb_exporter/__init__.py 
new/hanadb_exporter-0.5.0/hanadb_exporter/__init__.py
--- old/hanadb_exporter-0.4.0/hanadb_exporter/__init__.py       2019-10-24 
04:34:18.541251898 +0200
+++ new/hanadb_exporter-0.5.0/hanadb_exporter/__init__.py       2019-10-29 
14:44:08.123074953 +0100
@@ -8,4 +8,4 @@
 :since: 2019-05-09
 """
 
-__version__ = "0.4.0"
+__version__ = "0.5.0"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/hanadb_exporter-0.4.0/hanadb_exporter/db_manager.py 
new/hanadb_exporter-0.5.0/hanadb_exporter/db_manager.py
--- old/hanadb_exporter-0.4.0/hanadb_exporter/db_manager.py     1970-01-01 
01:00:00.000000000 +0100
+++ new/hanadb_exporter-0.5.0/hanadb_exporter/db_manager.py     2019-10-29 
14:44:08.123074953 +0100
@@ -0,0 +1,95 @@
+"""
+SAP HANA database manager
+
+:author: xarbulu
+:organization: SUSE Linux GmbH
+:contact: [email protected]
+
+:since: 2019-10-24
+"""
+
+import logging
+import time
+
+from shaptools import hdb_connector
+from hanadb_exporter import utils
+
+RECONNECTION_INTERVAL = 15
+
+
+class DatabaseManager(object):
+    """
+    Manage the connection to a multi container HANA system
+    """
+
+    TENANT_DATA_QUERY =\
+"""SELECT SQL_PORT FROM SYS_DATABASES.M_SERVICES
+WHERE (SERVICE_NAME='indexserver' and COORDINATOR_TYPE= 'MASTER')"""
+
+    def __init__(self):
+        self._logger = logging.getLogger(__name__)
+        self._system_db_connector = hdb_connector.HdbConnector()
+        self._db_connectors = []
+
+    def _get_tenants_port(self):
+        """
+        Get tenants port
+        """
+        data = self._system_db_connector.query(self.TENANT_DATA_QUERY)
+        formatted_data = utils.format_query_result(data)
+        for tenant_data in formatted_data:
+            yield int(tenant_data['SQL_PORT'])
+
+    def _connect_tenants(self, host, user, password):
+        """
+        Connect to the tenants
+
+        Args:
+            host (str): Host of the HANA database
+            user (str): System database user name (SYSTEM usually)
+            password (str): System database user password
+        """
+        for tenant_port in self._get_tenants_port():
+            conn = hdb_connector.HdbConnector()
+            conn.connect(
+                host, tenant_port, user=user, password=password, 
RECONNECT='FALSE')
+            self._db_connectors.append(conn)
+
+    def start(self, host, port, user, password, multi_tenant=True, 
timeout=600):
+        """
+        Start de database manager. This will open a connection with the System 
database and
+        retrieve the current environemtn tenant databases data
+
+        Args:
+            host (str): Host of the HANA database
+            port (int): Port of the System database (3XX13 when XX is the 
instance number)
+            user (str): System database user name (SYSTEM usually)
+            password (str): System database user password
+            multi_tenant (bool): Connect to all tenants checking the data in 
the System database
+            timeout (int, opt): Timeout in seconds to connect to the System 
database
+        """
+        current_time = time.time()
+        timeout = current_time + timeout
+        while current_time <= timeout:
+            try:
+                self._system_db_connector.connect(
+                    host, port, user=user, password=password, 
RECONNECT='FALSE')
+                self._db_connectors.append(self._system_db_connector)
+                break
+            except hdb_connector.connectors.base_connector.ConnectionError as 
err:
+                self._logger.error(
+                    'the connection to the system database failed. error 
message: %s', str(err))
+                time.sleep(RECONNECTION_INTERVAL)
+                current_time = time.time()
+        else:
+            raise hdb_connector.connectors.base_connector.ConnectionError(
+                'timeout reached connecting the System database')
+
+        if multi_tenant:
+            self._connect_tenants(host, user, password)
+
+    def get_connectors(self):
+        """
+        Get the connectors
+        """
+        return self._db_connectors
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/hanadb_exporter-0.4.0/hanadb_exporter/main.py 
new/hanadb_exporter-0.5.0/hanadb_exporter/main.py
--- old/hanadb_exporter-0.4.0/hanadb_exporter/main.py   2019-10-24 
04:34:18.541251898 +0200
+++ new/hanadb_exporter-0.5.0/hanadb_exporter/main.py   2019-10-29 
14:44:08.123074953 +0100
@@ -16,14 +16,12 @@
 import json
 import argparse
 
-from shaptools import hdb_connector
-
 from prometheus_client.core import REGISTRY
 from prometheus_client import start_http_server
 
 from hanadb_exporter import prometheus_exporter
+from hanadb_exporter import db_manager
 
-RECONNECTION_INTERVAL = 15
 LOGGER = logging.getLogger(__name__)
 
 
@@ -74,31 +72,6 @@
     sys.excepthook = handle_exception
 
 
-def connect(connector, config):
-    """
-    Connect to HANA. This operation is repeated until successfull connection. 
The exporter will
-    start working after that
-    """
-    hana_config = config['hana']
-    LOGGER.info(
-        'connecting to the hana database (%s:%s)',
-        hana_config['host'], hana_config.get('port', 30015))
-    while True:
-        try:
-            connector.connect(
-                hana_config['host'],
-                hana_config.get('port', 30015),
-                user=hana_config['user'],
-                password=hana_config['password'],
-                RECONNECT='FALSE'
-            )
-            break
-        except hdb_connector.connectors.base_connector.ConnectionError as err:
-            LOGGER.error(
-                'the connection to the database failed. error message: %s', 
str(err))
-            time.sleep(RECONNECTION_INTERVAL)
-
-
 # Start up the server to expose the metrics.
 def run():
     """
@@ -113,21 +86,23 @@
         logging.basicConfig(level=args.verbosity or logging.INFO)
 
     metrics = args.metrics
-    connector = hdb_connector.HdbConnector()
 
     try:
-        connect(connector, config)
+        hana_config = config['hana']
+        dbs = db_manager.DatabaseManager()
+        dbs.start(
+            hana_config['host'], hana_config.get('port', 30013),
+            hana_config['user'], hana_config['password'],
+            multi_tenant=config.get('multi_tenant', True),
+            timeout=config.get('timeout', 600))
     except KeyError as err:
         raise KeyError('Configuration file {} is malformed: {} not 
found'.format(args.config, err))
 
-    hana_version = 
prometheus_exporter.SapHanaCollector.get_hana_version(connector)
-    LOGGER.info('SAP HANA database version: %s', hana_version)
-
-    collector = prometheus_exporter.SapHanaCollector(
-        connector=connector, metrics_file=metrics, hana_version=hana_version)
-
+    connectors = dbs.get_connectors()
+    collector = prometheus_exporter.SapHanaCollectors(connectors=connectors, 
metrics_file=metrics)
     REGISTRY.register(collector)
     LOGGER.info('exporter sucessfully registered')
+
     LOGGER.info('starting to serve metrics')
     start_http_server(config.get('exposition_port', 8001), '0.0.0.0')
     while True:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/hanadb_exporter-0.4.0/hanadb_exporter/prometheus_exporter.py 
new/hanadb_exporter-0.5.0/hanadb_exporter/prometheus_exporter.py
--- old/hanadb_exporter-0.4.0/hanadb_exporter/prometheus_exporter.py    
2019-10-24 04:34:18.541251898 +0200
+++ new/hanadb_exporter-0.5.0/hanadb_exporter/prometheus_exporter.py    
2019-10-29 14:44:08.123074953 +0100
@@ -16,29 +16,76 @@
 from hanadb_exporter import utils
 
 
+class SapHanaCollectors(object):
+    """
+    SAP HANA database data exporter using multiple db connectors
+    """
+
+    def __init__(self, connectors, metrics_file):
+        self._logger = logging.getLogger(__name__)
+        self._collectors = []
+        for connector in connectors:
+            collector = SapHanaCollector(connector, metrics_file)
+            self._collectors.append(collector)
+
+    def collect(self):
+        """
+        Collect metrics for each collector
+        """
+        for collector in self._collectors:
+            for metric in collector.collect():
+                yield metric
+
+
 class SapHanaCollector(object):
     """
     SAP HANA database data exporter
     """
 
-    def __init__(self, connector, metrics_file, hana_version):
+    METADATA_LABEL_HEADERS = ['sid', 'insnr', 'database_name']
+
+    def __init__(self, connector, metrics_file):
         self._logger = logging.getLogger(__name__)
         self._hdb_connector = connector
         # metrics_config contains the configuration api/json data
         self._metrics_config = 
prometheus_metrics.PrometheusMetrics(metrics_file)
-        self._hana_version = hana_version
+        self.retrieve_metadata()
 
-    @staticmethod
-    def get_hana_version(connector):
+    @property
+    def metadata_labels(self):
         """
-        Query the SAP HANA database version
-
-        Args:
-            connector: HANA database api connector
+        Get metadata labels data
         """
-        query = 'SELECT * FROM sys.m_database;'
-        query_result = connector.query(query)
-        return utils.format_query_result(query_result)[0]['VERSION']
+        return [self._sid, self._insnr, self._database_name]
+
+    def retrieve_metadata(self):
+        """
+        Retrieve database metadata: sid, instance number, database name and 
hana version
+        """
+        query = \
+"""SELECT
+(SELECT value
+FROM M_SYSTEM_OVERVIEW
+WHERE section = 'System'
+AND name = 'Instance ID') SID,
+(SELECT value
+FROM M_SYSTEM_OVERVIEW
+WHERE section = 'System'
+AND name = 'Instance Number') INSNR,
+m.database_name,
+m.version
+FROM m_database m;"""
+
+        self._logger.info('Querying database metadata...')
+        query_result = self._hdb_connector.query(query)
+        formatted_result = utils.format_query_result(query_result)[0]
+        self._hana_version = formatted_result['VERSION']
+        self._sid = formatted_result['SID']
+        self._insnr = formatted_result['INSNR']
+        self._database_name = formatted_result['DATABASE_NAME']
+        self._logger.info(
+            'Metadata retrieved. version: %s, sid: %s, insnr: %s, database: 
%s',
+            self._hana_version, self._sid, self._insnr, self._database_name)
 
     def _manage_gauge(self, metric, formatted_query_result):
         """
@@ -50,8 +97,10 @@
             metric (dict): a dictionary containing information about the metric
             formatted_query_result (nested list): query formated by 
_format_query_result method
         """
+        # Add sid, insnr and database_name labels
+        combined_label_headers = self.METADATA_LABEL_HEADERS + metric.labels
         metric_obj = core.GaugeMetricFamily(
-            metric.name, metric.description, None, metric.labels, metric.unit)
+            metric.name, metric.description, None, combined_label_headers, 
metric.unit)
         for row in formatted_query_result:
             labels = []
             metric_value = None
@@ -73,17 +122,30 @@
                     ' for metric: "{}" is not found in the the query 
result'.format(
                         metric.name))
             else:
-                metric_obj.add_metric(labels, metric_value)
+                # Add sid, insnr and database_name labels
+                combined_labels = self.metadata_labels + labels
+                metric_obj.add_metric(combined_labels, metric_value)
         self._logger.debug('%s \n', metric_obj.samples)
         return metric_obj
 
+    def reconnect(self):
+        """
+        Reconnect if needed and retrieve new metadata
+
+        hdb_connector reconnect already checks if the connection is working, 
but we need to
+        recheck to run the retrieve_metadata method to update some possible 
changes
+        """
+        if not self._hdb_connector.isconnected():
+            self._hdb_connector.reconnect()
+            self.retrieve_metadata()
+
     def collect(self):
         """
         execute db queries defined by metrics_config/api file, and store them 
in
         a prometheus metric_object, which will be served over http for 
scraping e.g gauge, etc.
         """
         # Try to reconnect if the connection is lost. It will raise an 
exception is case of error
-        self._hdb_connector.reconnect()
+        self.reconnect()
 
         for query in self._metrics_config.queries:
             if not query.enabled:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/hanadb_exporter-0.4.0/hanadb_exporter.changes 
new/hanadb_exporter-0.5.0/hanadb_exporter.changes
--- old/hanadb_exporter-0.4.0/hanadb_exporter.changes   2019-10-24 
04:34:18.541251898 +0200
+++ new/hanadb_exporter-0.5.0/hanadb_exporter.changes   2019-10-29 
14:44:08.123074953 +0100
@@ -1,4 +1,16 @@
 -------------------------------------------------------------------
+Fri Oct 25 06:14:03 UTC 2019 - Xabier Arbulu <[email protected]>
+
+- Version 0.5.0 Add the option to export metrics from multiple
+databases/tenants 
+
+-------------------------------------------------------------------
+Thu Oct 24 03:00:45 UTC 2019 - Xabier Arbulu <[email protected]>
+
+- Version 0.4.1 Add new metadata labels to the metrics (sid, instance
+number and databse name) 
+
+-------------------------------------------------------------------
 Wed Oct 23 10:53:36 UTC 2019 - Xabier Arbulu <[email protected]>
 
 - Version 0.4.0 Remove the factory usage to gain simplicity as only 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/hanadb_exporter-0.4.0/hanadb_exporter.spec 
new/hanadb_exporter-0.5.0/hanadb_exporter.spec
--- old/hanadb_exporter-0.4.0/hanadb_exporter.spec      2019-10-24 
04:34:18.541251898 +0200
+++ new/hanadb_exporter-0.5.0/hanadb_exporter.spec      2019-10-29 
14:44:08.123074953 +0100
@@ -26,7 +26,7 @@
 %endif
 
 Name:           hanadb_exporter
-Version:        0.4.0
+Version:        0.5.0
 Release:        0
 Summary:        SAP HANA database metrics exporter
 License:        Apache-2.0
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/hanadb_exporter-0.4.0/tests/db_manager_test.py 
new/hanadb_exporter-0.5.0/tests/db_manager_test.py
--- old/hanadb_exporter-0.4.0/tests/db_manager_test.py  1970-01-01 
01:00:00.000000000 +0100
+++ new/hanadb_exporter-0.5.0/tests/db_manager_test.py  2019-10-29 
14:44:08.123074953 +0100
@@ -0,0 +1,193 @@
+"""
+Unitary tests for exporters/db_manager.py.
+
+:author: xarbulu
+:organization: SUSE Linux GmbH
+:contact: [email protected]
+
+:since: 2019-10-25
+"""
+
+# pylint:disable=C0103,C0111,W0212,W0611
+
+import os
+import sys
+import pytest
+
+sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), 
'..')))
+
+try:
+    from unittest import mock
+except ImportError:
+    import mock
+
+sys.modules['shaptools'] = mock.MagicMock()
+from hanadb_exporter import db_manager
+
+
+class TestDatabaseManager(object):
+    """
+    Unitary tests for hanadb_exporter/db_manager.py.
+    """
+
+    @mock.patch('hanadb_exporter.db_manager.hdb_connector.HdbConnector')
+    def setup(self, mock_hdb):
+        """
+        Test setUp.
+        """
+
+        self._db_manager = db_manager.DatabaseManager()
+        mock_hdb.assert_called_once_with()
+
+    @mock.patch('hanadb_exporter.db_manager.utils.format_query_result')
+    def test_get_tenants_port(self, mock_format_query):
+        self._db_manager._system_db_connector = mock.Mock()
+        self._db_manager._system_db_connector.query.return_value = 'result'
+        ports = ['30040', '30041']
+        mock_format_query.return_value = [{'SQL_PORT': ports[0]}, {'SQL_PORT': 
ports[1]}]
+
+        for i, port in enumerate(self._db_manager._get_tenants_port()):
+            assert port == int(ports[i])
+
+    @mock.patch('hanadb_exporter.db_manager.hdb_connector.HdbConnector')
+    def test_connect_tenants(self, mock_hdb):
+
+        self._db_manager._get_tenants_port = mock.Mock(return_value=[1, 2, 3])
+
+        mock_conn1 = mock.Mock()
+        mock_conn2 = mock.Mock()
+        mock_conn3 = mock.Mock()
+
+        mock_hdb.side_effect = [mock_conn1, mock_conn2, mock_conn3]
+
+        self._db_manager._connect_tenants('10.10.10.10', 'SYSTEM', 'pass')
+
+        assert mock_hdb.call_count == 3
+
+        mock_conn1.connect.assert_called_once_with(
+            '10.10.10.10', 1, user='SYSTEM', password='pass', 
RECONNECT='FALSE')
+        mock_conn2.connect.assert_called_once_with(
+            '10.10.10.10', 2, user='SYSTEM', password='pass', 
RECONNECT='FALSE')
+        mock_conn3.connect.assert_called_once_with(
+            '10.10.10.10', 3, user='SYSTEM', password='pass', 
RECONNECT='FALSE')
+
+        assert self._db_manager._db_connectors == [mock_conn1, mock_conn2, 
mock_conn3]
+
+    
@mock.patch('hanadb_exporter.db_manager.hdb_connector.connectors.base_connector')
+    @mock.patch('logging.Logger.error')
+    @mock.patch('time.sleep')
+    @mock.patch('time.time')
+    def test_start_timeout(self, mock_time, mock_sleep, mock_logger, 
mock_exception):
+        mock_exception.ConnectionError = Exception
+        mock_time.side_effect = [0, 1, 2, 3]
+        self._db_manager._system_db_connector = mock.Mock()
+
+        self._db_manager._system_db_connector.connect.side_effect = [
+            mock_exception.ConnectionError('err'),
+            mock_exception.ConnectionError('err'),
+            mock_exception.ConnectionError('err')]
+
+        with pytest.raises(mock_exception.ConnectionError) as err:
+            self._db_manager.start(
+                '10.10.10.10', 30013, 'user', 'pass', multi_tenant=False, 
timeout=2)
+
+        assert 'timeout reached connecting the System database' in 
str(err.value)
+
+        self._db_manager._system_db_connector.connect.assert_has_calls([
+            mock.call('10.10.10.10', 30013, user='user', password='pass', 
RECONNECT='FALSE'),
+            mock.call('10.10.10.10', 30013, user='user', password='pass', 
RECONNECT='FALSE'),
+            mock.call('10.10.10.10', 30013, user='user', password='pass', 
RECONNECT='FALSE')
+        ])
+
+        mock_sleep.assert_has_calls([
+            mock.call(15),
+            mock.call(15),
+            mock.call(15)
+        ])
+
+        mock_logger.assert_has_calls([
+            mock.call('the connection to the system database failed. error 
message: %s', 'err'),
+            mock.call('the connection to the system database failed. error 
message: %s', 'err'),
+            mock.call('the connection to the system database failed. error 
message: %s', 'err')
+        ])
+
+        assert self._db_manager._db_connectors == []
+
+    
@mock.patch('hanadb_exporter.db_manager.hdb_connector.connectors.base_connector')
+    @mock.patch('logging.Logger.error')
+    @mock.patch('time.sleep')
+    @mock.patch('time.time')
+    def test_start_correct(self, mock_time, mock_sleep, mock_logger, 
mock_exception):
+        mock_exception.ConnectionError = Exception
+        mock_time.side_effect = [0, 1, 2, 3]
+        self._db_manager._system_db_connector = mock.Mock()
+        self._db_manager._connect_tenants = mock.Mock()
+
+        self._db_manager._system_db_connector.connect.side_effect = [
+            mock_exception.ConnectionError('err'),
+            mock_exception.ConnectionError('err'),
+            None]
+
+        self._db_manager.start(
+            '10.10.10.10', 30013, 'user', 'pass', multi_tenant=False, 
timeout=2)
+
+        self._db_manager._system_db_connector.connect.assert_has_calls([
+            mock.call('10.10.10.10', 30013, user='user', password='pass', 
RECONNECT='FALSE'),
+            mock.call('10.10.10.10', 30013, user='user', password='pass', 
RECONNECT='FALSE'),
+            mock.call('10.10.10.10', 30013, user='user', password='pass', 
RECONNECT='FALSE')
+        ])
+
+        mock_sleep.assert_has_calls([
+            mock.call(15),
+            mock.call(15)
+        ])
+
+        mock_logger.assert_has_calls([
+            mock.call('the connection to the system database failed. error 
message: %s', 'err'),
+            mock.call('the connection to the system database failed. error 
message: %s', 'err')
+        ])
+
+        assert self._db_manager._db_connectors == 
[self._db_manager._system_db_connector]
+        self._db_manager._connect_tenants.assert_not_called()
+
+    
@mock.patch('hanadb_exporter.db_manager.hdb_connector.connectors.base_connector')
+    @mock.patch('logging.Logger.error')
+    @mock.patch('time.sleep')
+    @mock.patch('time.time')
+    def test_start_correct_multitenant(self, mock_time, mock_sleep, 
mock_logger, mock_exception):
+        mock_exception.ConnectionError = Exception
+        mock_time.side_effect = [0, 1, 2, 3]
+        self._db_manager._system_db_connector = mock.Mock()
+        self._db_manager._connect_tenants = mock.Mock()
+
+        self._db_manager._system_db_connector.connect.side_effect = [
+            mock_exception.ConnectionError('err'),
+            mock_exception.ConnectionError('err'),
+            None]
+
+        self._db_manager.start(
+            '10.10.10.10', 30013, 'user', 'pass', multi_tenant=True, timeout=2)
+
+        self._db_manager._system_db_connector.connect.assert_has_calls([
+            mock.call('10.10.10.10', 30013, user='user', password='pass', 
RECONNECT='FALSE'),
+            mock.call('10.10.10.10', 30013, user='user', password='pass', 
RECONNECT='FALSE'),
+            mock.call('10.10.10.10', 30013, user='user', password='pass', 
RECONNECT='FALSE')
+        ])
+
+        mock_sleep.assert_has_calls([
+            mock.call(15),
+            mock.call(15)
+        ])
+
+        mock_logger.assert_has_calls([
+            mock.call('the connection to the system database failed. error 
message: %s', 'err'),
+            mock.call('the connection to the system database failed. error 
message: %s', 'err')
+        ])
+
+        assert self._db_manager._db_connectors == 
[self._db_manager._system_db_connector]
+        
self._db_manager._connect_tenants.assert_called_once_with('10.10.10.10', 
'user', 'pass')
+
+
+    def test_get_connectors(self):
+        self._db_manager._db_connectors = 'conns'
+        assert 'conns' == self._db_manager.get_connectors()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/hanadb_exporter-0.4.0/tests/main_test.py 
new/hanadb_exporter-0.5.0/tests/main_test.py
--- old/hanadb_exporter-0.4.0/tests/main_test.py        2019-10-24 
04:34:18.545253898 +0200
+++ new/hanadb_exporter-0.5.0/tests/main_test.py        2019-10-29 
14:44:08.123074953 +0100
@@ -89,83 +89,30 @@
         ])
 
     @mock.patch('hanadb_exporter.main.LOGGER')
-    def test_connect(self, mock_logger):
-
-        mock_connector = mock.Mock()
-        config = {
-            'hana': {
-                'host': '123.123.123.123',
-                'port': 1234,
-                'user': 'user',
-                'password': 'pass'
-            }
-        }
-
-        main.connect(mock_connector, config)
-
-        mock_logger.info.assert_called_once_with(
-            'connecting to the hana database (%s:%s)', '123.123.123.123', 1234)
-        mock_connector.connect.assert_called_once_with(
-            '123.123.123.123', 1234, user='user', password='pass', 
RECONNECT='FALSE')
-
-    @mock.patch('hanadb_exporter.main.LOGGER')
-    @mock.patch('hanadb_exporter.main.hdb_connector.connectors.base_connector')
-    @mock.patch('time.sleep')
-    def test_connect_loop(self, mock_sleep, mock_connection_error, 
mock_logger):
-
-        mock_exception = Exception
-        mock_connection_error.ConnectionError = mock_exception
-        mock_connector = mock.Mock()
-        config = {
-            'hana': {
-                'host': '123.123.123.123',
-                'user': 'user',
-                'password': 'pass'
-            }
-        }
-
-        mock_connector.connect.side_effect = [mock_exception('err1'), 
mock_exception('err2'), True]
-
-        main.connect(mock_connector, config)
-
-        mock_logger.info.assert_called_once_with(
-            'connecting to the hana database (%s:%s)', '123.123.123.123', 
30015)
-        mock_connector.connect.assert_has_calls([
-            mock.call('123.123.123.123', 30015, user='user', password='pass', 
RECONNECT='FALSE'),
-            mock.call('123.123.123.123', 30015, user='user', password='pass', 
RECONNECT='FALSE'),
-            mock.call('123.123.123.123', 30015, user='user', password='pass', 
RECONNECT='FALSE')
-        ])
-
-        mock_logger.error.assert_has_calls([
-            mock.call('the connection to the database failed. error message: 
%s', 'err1'),
-            mock.call('the connection to the database failed. error message: 
%s', 'err2')
-        ])
-
-        mock_sleep.assert_has_calls([
-            mock.call(15),
-            mock.call(15)
-        ])
-
-    @mock.patch('hanadb_exporter.main.LOGGER')
-    @mock.patch('hanadb_exporter.main.connect')
     @mock.patch('hanadb_exporter.main.parse_arguments')
     @mock.patch('hanadb_exporter.main.parse_config')
     @mock.patch('hanadb_exporter.main.setup_logging')
-    @mock.patch('hanadb_exporter.main.hdb_connector.HdbConnector')
-    @mock.patch('hanadb_exporter.main.prometheus_exporter.SapHanaCollector')
+    @mock.patch('hanadb_exporter.main.db_manager.DatabaseManager')
+    @mock.patch('hanadb_exporter.main.prometheus_exporter.SapHanaCollectors')
     @mock.patch('hanadb_exporter.main.REGISTRY.register')
     @mock.patch('hanadb_exporter.main.start_http_server')
     @mock.patch('logging.getLogger')
     @mock.patch('time.sleep')
     def test_run(
             self, mock_sleep, mock_get_logger, mock_start_server, 
mock_registry,
-            mock_exporter, mock_hdb, mock_setup_loggin,
-            mock_parse_config, mock_parse_arguments, mock_connect, 
mock_logger):
+            mock_exporters, mock_db_manager, mock_setup_logging,
+            mock_parse_config, mock_parse_arguments, mock_logger):
 
         mock_arguments = mock.Mock(config='config', metrics='metrics')
         mock_parse_arguments.return_value = mock_arguments
 
         config = {
+            'hana': {
+                'host': '10.10.10.10',
+                'port': 1234,
+                'user': 'user',
+                'password': 'pass'
+            },
             'logging': {
                 'log_file': 'my_file',
                 'config_file': 'my_config_file'
@@ -173,12 +120,12 @@
         }
         mock_parse_config.return_value = config
 
-        mock_connector = mock.Mock()
-        mock_hdb.return_value = mock_connector
+        db_instance = mock.Mock()
+        db_instance.get_connectors.return_value = 'connectors'
+        mock_db_manager.return_value = db_instance
 
         mock_collector = mock.Mock()
-        mock_exporter.return_value = mock_collector
-        mock_exporter.get_hana_version.return_value = '1.0.0'
+        mock_exporters.return_value = mock_collector
 
         mock_sleep.side_effect = Exception
 
@@ -187,12 +134,14 @@
 
         mock_parse_arguments.assert_called_once_with()
         mock_parse_config.assert_called_once_with(mock_arguments.config)
-        mock_setup_loggin.assert_called_once_with(config)
-        mock_hdb.assert_called_once_with()
-        mock_connect.assert_called_once_with(mock_connector, config)
-        mock_exporter.get_hana_version.assert_called_once_with(mock_connector)
-        mock_exporter.assert_called_once_with(
-            connector=mock_connector, metrics_file='metrics', 
hana_version='1.0.0')
+        mock_setup_logging.assert_called_once_with(config)
+        mock_db_manager.assert_called_once_with()
+        db_instance.start.assert_called_once_with(
+            '10.10.10.10', 1234, 'user', 'pass', multi_tenant=True, 
timeout=600)
+        db_instance.get_connectors.assert_called_once_with()
+        mock_exporters.assert_called_once_with(
+            connectors='connectors', metrics_file='metrics')
+
         mock_registry.assert_called_once_with(mock_collector)
         mock_logger.info.assert_has_calls([
             mock.call('exporter sucessfully registered'),
@@ -202,31 +151,26 @@
         mock_sleep.assert_called_once_with(1)
 
     @mock.patch('hanadb_exporter.main.LOGGER')
-    @mock.patch('hanadb_exporter.main.connect')
     @mock.patch('hanadb_exporter.main.parse_arguments')
     @mock.patch('hanadb_exporter.main.parse_config')
-    @mock.patch('hanadb_exporter.main.hdb_connector.HdbConnector')
+    @mock.patch('hanadb_exporter.main.db_manager.DatabaseManager')
     @mock.patch('logging.getLogger')
     @mock.patch('logging.basicConfig')
     def test_run_malformed(
-            self, mock_logging, mock_get_logger, mock_hdb,
-            mock_parse_config, mock_parse_arguments, mock_connect, 
mock_logger):
+            self, mock_logging, mock_get_logger, mock_db_manager,
+            mock_parse_config, mock_parse_arguments, mock_logger):
 
         mock_arguments = mock.Mock(config='config', metrics='metrics', 
verbosity='DEBUG')
         mock_parse_arguments.return_value = mock_arguments
 
         config = {
             'hana': {
-                'host': '123.123.123.123',
                 'port': 1234,
                 'user': 'user',
                 'password': 'pass'
             }
         }
         mock_parse_config.return_value = config
-        mock_connect.side_effect = KeyError('error')
-        hdb_instance = mock.Mock()
-        mock_hdb.return_value = hdb_instance
 
         with pytest.raises(KeyError) as err:
             main.run()
@@ -234,7 +178,6 @@
         mock_parse_arguments.assert_called_once_with()
         mock_parse_config.assert_called_once_with(mock_arguments.config)
         mock_logging.assert_called_once_with(level='DEBUG')
-        mock_hdb.assert_called_once_with()
-        mock_connect.assert_called_once_with(hdb_instance, config)
+        mock_db_manager.assert_called_once_with()
         assert 'Configuration file {} is malformed: {} not found'.format(
-            'config', '\'error\'') in str(err.value)
+            'config', '\'host\'') in str(err.value)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/hanadb_exporter-0.4.0/tests/prometheus_exporter_test.py 
new/hanadb_exporter-0.5.0/tests/prometheus_exporter_test.py
--- old/hanadb_exporter-0.4.0/tests/prometheus_exporter_test.py 2019-10-24 
04:34:18.545253898 +0200
+++ new/hanadb_exporter-0.5.0/tests/prometheus_exporter_test.py 2019-10-29 
14:44:08.123074953 +0100
@@ -28,34 +28,119 @@
 
 from hanadb_exporter import prometheus_exporter
 
+class TestSapHanaCollectors(object):
+    """
+    Unitary tests for SapHanaCollectors.
+    """
+
+    @mock.patch('hanadb_exporter.prometheus_exporter.SapHanaCollector')
+    def test_init(self, mock_collector):
+
+        conn1 = mock.Mock()
+        conn2 = mock.Mock()
+
+        connectors = [conn1, conn2]
+
+        coll1 = mock.Mock()
+        coll2 = mock.Mock()
+        mock_collector.side_effect = [coll1, coll2]
+
+        collectors = prometheus_exporter.SapHanaCollectors(connectors, 
'metrics.json')
+
+        mock_collector.assert_has_calls([
+            mock.call(conn1, 'metrics.json'),
+            mock.call(conn2, 'metrics.json')
+        ])
+
+        assert collectors._collectors == [coll1, coll2]
+
+    @mock.patch('hanadb_exporter.prometheus_exporter.SapHanaCollector')
+    def test_collect(self, mock_collector):
+
+        conn1 = mock.Mock()
+        conn2 = mock.Mock()
+
+        connectors = [conn1, conn2]
+
+        metrics = ['metric1', 'metric2', 'metric3', 'metric4']
+        coll1 = mock.Mock()
+        coll1.collect.return_value=[metrics[0], metrics[1]]
+        coll2 = mock.Mock()
+        coll2.collect.return_value=[metrics[2], metrics[3]]
+
+        mock_collector.side_effect = [coll1, coll2]
+
+        collectors = prometheus_exporter.SapHanaCollectors(connectors, 
'metrics.json')
+
+        for i, metric in enumerate(collectors.collect()):
+            assert metric == metrics[i]
+
+        coll1.collect.assert_called_once_with()
+        coll2.collect.assert_called_once_with()
+
 
 class TestSapHanaCollector(object):
     """
     Unitary tests for SapHanaCollector.
     """
 
+    
@mock.patch('hanadb_exporter.prometheus_exporter.SapHanaCollector.retrieve_metadata')
     @mock.patch('hanadb_exporter.prometheus_metrics.PrometheusMetrics')
-    def setup(self, mock_metrics):
+    def setup(self, mock_metrics, mock_retrieve_metadata):
         """
         Test setUp.
         """
         self._mock_metrics_config = mock.Mock()
         mock_metrics.return_value = self._mock_metrics_config
         self._mock_connector = mock.Mock()
-        hana_version = '2.0'
-        self._collector = prometheus_exporter.SapHanaCollector(
-            self._mock_connector, 'metrics.json', hana_version)
+        self._collector = 
prometheus_exporter.SapHanaCollector(self._mock_connector, 'metrics.json')
+
+        self._collector._sid = 'prd'
+        self._collector._insnr = '00'
+        self._collector._database_name = 'db_name'
+        self._collector._hana_version = '2.0'
+
+        mock_retrieve_metadata.assert_called_once_with()
+
+    def test_metadata_labels(self):
+        assert ['prd', '00', 'db_name'] == self._collector.metadata_labels
 
     @mock.patch('hanadb_exporter.utils.format_query_result')
-    def test_get_hana_version(self, mock_format_query):
-        mock_connector = mock.Mock()
-        mock_connector.query.return_value = 'query_result'
-        mock_format_query.return_value = [{'VERSION': '2.0'}]
-        version = 
prometheus_exporter.SapHanaCollector.get_hana_version(mock_connector)
-
-        mock_connector.query.assert_called_once_with('SELECT * FROM 
sys.m_database;')
-        mock_format_query.assert_called_once_with('query_result')
-        assert version == '2.0'
+    @mock.patch('logging.Logger.info')
+    def test_retrieve_metadata(self, mock_logger, mock_format_query):
+
+        mock_result = mock.Mock()
+        self._collector._hdb_connector.query = 
mock.Mock(return_value=mock_result)
+        mock_format_query.return_value = [
+            {'SID': 'ha1', 'INSNR': '10', 'DATABASE_NAME': 'DB_SYSTEM', 
'VERSION': '1.2.3'}]
+
+        self._collector.retrieve_metadata()
+
+        mock_logger.assert_has_calls([
+            mock.call('Querying database metadata...'),
+            mock.call(
+                'Metadata retrieved. version: %s, sid: %s, insnr: %s, 
database: %s',
+                '1.2.3', 'ha1', '10', 'DB_SYSTEM')
+        ])
+        self._collector._hdb_connector.query.assert_called_once_with(
+"""SELECT
+(SELECT value
+FROM M_SYSTEM_OVERVIEW
+WHERE section = 'System'
+AND name = 'Instance ID') SID,
+(SELECT value
+FROM M_SYSTEM_OVERVIEW
+WHERE section = 'System'
+AND name = 'Instance Number') INSNR,
+m.database_name,
+m.version
+FROM m_database m;"""
+        )
+        mock_format_query.assert_called_once_with(mock_result)
+        assert self._collector._sid == 'ha1'
+        assert self._collector._insnr == '10'
+        assert self._collector._database_name == 'DB_SYSTEM'
+        assert self._collector._hana_version == '1.2.3'
 
     @mock.patch('hanadb_exporter.prometheus_exporter.core')
     @mock.patch('logging.Logger.debug')
@@ -82,12 +167,13 @@
         metric_obj = self._collector._manage_gauge(mock_metric, 
formatted_query)
 
         mock_core.GaugeMetricFamily.assert_called_once_with(
-            'name', 'description', None, ['column1', 'column2'], 'mb')
+            'name', 'description', None,
+            ['sid', 'insnr', 'database_name', 'column1', 'column2'], 'mb')
 
         mock_gauge_instance.add_metric.assert_has_calls([
-            mock.call(['data1', 'data2'], 'data3'),
-            mock.call(['data4', 'data5'], 'data6'),
-            mock.call(['data7', 'data8'], 'data9')
+            mock.call(['prd', '00', 'db_name', 'data1', 'data2'], 'data3'),
+            mock.call(['prd', '00', 'db_name', 'data4', 'data5'], 'data6'),
+            mock.call(['prd', '00', 'db_name', 'data7', 'data8'], 'data9')
         ])
 
         mock_logger.assert_called_once_with('%s \n', 'samples')
@@ -95,7 +181,7 @@
 
     @mock.patch('hanadb_exporter.prometheus_exporter.core')
     @mock.patch('logging.Logger.error')
-    def test_incorrect_label(self, mock_logger, mock_core):
+    def test_manage_gauge_incorrect_label(self, mock_logger, mock_core):
 
         mock_gauge_instance = mock.Mock()
         mock_core.GaugeMetricFamily = mock.Mock()
@@ -117,13 +203,17 @@
         with pytest.raises(ValueError) as err:
             metric_obj = self._collector._manage_gauge(mock_metric, 
formatted_query)
 
-            assert('One or more label(s) specified in metrics.json'
-                   ' for metric: "{}" is not found in the the query 
result'.format(
-                    'name') in str(err.value))
+        mock_core.GaugeMetricFamily.assert_called_once_with(
+            'name', 'description', None,
+            ['sid', 'insnr', 'database_name', 'column4', 'column5'], 'mb')
+
+        assert('One or more label(s) specified in metrics.json'
+               ' for metric: "{}" is not found in the the query result'.format(
+                'name') in str(err.value))
 
     @mock.patch('hanadb_exporter.prometheus_exporter.core')
     @mock.patch('logging.Logger.error')
-    def test_incorrect_value(self, mock_logger, mock_core):
+    def test_manage_gauge_incorrect_value(self, mock_logger, mock_core):
 
         mock_gauge_instance = mock.Mock()
         mock_gauge_instance.samples = 'samples'
@@ -146,19 +236,37 @@
         with pytest.raises(ValueError) as err:
             metric_obj = self._collector._manage_gauge(mock_metric, 
formatted_query)
 
+        mock_core.GaugeMetricFamily.assert_called_once_with(
+            'name', 'description', None,
+            ['sid', 'insnr', 'database_name', 'column1', 'column2'], 'mb')
+
         assert('Specified value in metrics.json for metric'
                ' "{}": ({}) not found in the query result'.format(
                 'name', 'column4') in str(err.value))
 
+    def test_reconnect_connected(self):
+        self._mock_connector.isconnected.return_value = True
+        self._collector.reconnect()
+        self._mock_connector.isconnected.assert_called_once_with()
+        self._mock_connector.reconnect.assert_not_called()
+
+    def test_reconnect_not_connected(self):
+        self._mock_connector.isconnected.return_value = False
+        self._collector.retrieve_metadata = mock.Mock()
+        self._collector.reconnect()
+        self._mock_connector.isconnected.assert_called_once_with()
+        self._mock_connector.reconnect.assert_called_once_with()
+        self._collector.retrieve_metadata.assert_called_once_with()
+
     @mock.patch('hanadb_exporter.utils.format_query_result')
     @mock.patch('hanadb_exporter.utils.check_hana_range')
     @mock.patch('logging.Logger.error')
-    def test_value_error(self, mock_logger, mock_hana_range, 
mock_format_query):
+    def test_collect_value_error(self, mock_logger, mock_hana_range, 
mock_format_query):
         """
         Test that when _manage_gauge is called and return ValueError (labels 
or value)
         are incorrect, that the ValueError is catched by collect() and a error 
is raised
         """
-        self._collector._hdb_connector.reconnect = mock.Mock()
+        self._collector.reconnect = mock.Mock()
         self._collector._manage_gauge = mock.Mock()
 
         self._collector._manage_gauge.side_effect = ValueError('test')
@@ -173,7 +281,7 @@
         for _ in self._collector.collect():
             continue
 
-        self._collector._hdb_connector.reconnect.assert_called_once_with()
+        self._collector.reconnect.assert_called_once_with()
         mock_logger.assert_called_once_with('test')
 
     @mock.patch('hanadb_exporter.utils.format_query_result')
@@ -181,7 +289,7 @@
     @mock.patch('logging.Logger.info')
     def test_collect(self, mock_logger, mock_hana_range, mock_format_query):
 
-        self._collector._hdb_connector.reconnect = mock.Mock()
+        self._collector.reconnect = mock.Mock()
         self._collector._manage_gauge = mock.Mock()
 
         self._mock_connector.query.side_effect = [
@@ -219,7 +327,7 @@
         for index, element in enumerate(self._collector.collect()):
             assert element == 'gauge{}'.format(index+1)
 
-        self._collector._hdb_connector.reconnect.assert_called_once_with()
+        self._collector.reconnect.assert_called_once_with()
         self._mock_connector.query.assert_has_calls([
             mock.call('query1'),
             mock.call('query3')])
@@ -253,7 +361,7 @@
     @mock.patch('hanadb_exporter.utils.check_hana_range')
     def test_collect_incorrect_type(self, mock_hana_range, mock_format_query):
 
-        self._collector._hdb_connector.reconnect = mock.Mock()
+        self._collector.reconnect = mock.Mock()
         self._collector._manage_gauge = mock.Mock()
 
         self._mock_connector.query.side_effect = [
@@ -288,7 +396,7 @@
             for index, element in enumerate(self._collector.collect()):
                 assert element == 'gauge{}'.format(index+1)
 
-        self._collector._hdb_connector.reconnect.assert_called_once_with()
+        self._collector.reconnect.assert_called_once_with()
         assert '{} type not implemented'.format('other') in str(err.value)
 
         self._mock_connector.query.assert_has_calls([
@@ -314,9 +422,9 @@
     @mock.patch('hanadb_exporter.utils.check_hana_range')
     
@mock.patch('hanadb_exporter.prometheus_exporter.hdb_connector.connectors.base_connector')
     @mock.patch('logging.Logger.error')
-    def test_incorrect_query(self, mock_logger, mock_base_connector, 
mock_hana_range):
+    def test_collect_incorrect_query(self, mock_logger, mock_base_connector, 
mock_hana_range):
 
-        self._collector._hdb_connector.reconnect = mock.Mock()
+        self._collector.reconnect = mock.Mock()
         mock_base_connector.QueryError = Exception
 
         self._mock_connector.query.side_effect = Exception('error')
@@ -329,7 +437,7 @@
         for _ in self._collector.collect():
             continue
 
-        self._collector._hdb_connector.reconnect.assert_called_once_with()
+        self._collector.reconnect.assert_called_once_with()
         self._mock_connector.query.assert_called_once_with('query1')
 
         mock_hana_range.assert_has_calls([


Reply via email to