Distinguish between Java and Python for Mon

Detection plugins for monasca exists in particular for
monasca-api and monasca-persister. However they only support
proper detection of java based implementations.

Following change provides support for python based implementation.

Additionally written several missing unit tests for
Mon* detection plugins.

Fixes-Bug: #1642567
Change-Id: I76df0dec03ade146b96cc59e8dd9f9c394f30262
This commit is contained in:
Tomasz Trębski 2016-11-21 08:36:46 +01:00
parent 8d133c4013
commit a442ee9dcb
2 changed files with 1066 additions and 183 deletions

View File

@ -1,4 +1,5 @@
# (C) Copyright 2015-2016 Hewlett Packard Enterprise Development LP
# Copyright 2016 FUJITSU LIMITED
"""Classes for monitoring the monitoring server stack.
@ -9,22 +10,53 @@
import logging
import yaml
from six.moves import configparser
import monasca_setup.agent_config
import monasca_setup.detection
from monasca_setup.detection import find_process_cmdline
from monasca_setup.detection import find_process_name
from monasca_setup.detection import watch_process
from monasca_setup.detection import watch_process_by_username
from monasca_setup.detection.utils import watch_process
from monasca_setup.detection.utils import watch_process_by_username
log = logging.getLogger(__name__)
_PYTHON_LANG_MARKERS = 'python', 'gunicorn',
"""List of all strings that if found in process exe
mean that application runs under Python"""
_JAVA_LANG_MARKERS = 'java',
_DEFAULT_API_PORT = 8070
"""Default TCP port which monasca-api process should be available by"""
def _get_impl_lang(process):
"""Return implementation language of the application behind process
:param process: current process
:type process: psutil.Process
:return: implementation lang, either java or python
:rtype: str
"""
p_exe = process.exe()
for lm in _PYTHON_LANG_MARKERS:
if lm in p_exe:
return 'python'
for lm in _JAVA_LANG_MARKERS:
if lm in p_exe:
return 'java'
raise Exception(('Cannot determine language '
'implementation from process exe %s') % p_exe)
class MonAgent(monasca_setup.detection.Plugin):
"""Detect the Monsaca agent engine and setup some simple checks."""
def _detect(self):
"""Run detection, set self.available True if the service is detected."""
self.available = True
agent_process_list = ['monasca-collector', 'monasca-forwarder', 'monasca-statsd']
agent_process_list = ['monasca-collector', 'monasca-forwarder',
'monasca-statsd']
for process in agent_process_list:
if find_process_cmdline(process) is None:
self.available = False
@ -42,94 +74,82 @@ class MonAgent(monasca_setup.detection.Plugin):
class MonAPI(monasca_setup.detection.Plugin):
"""Detect mon_api and setup monitoring."""
PARTIAL_ERR_MSG = 'Plugin for monasca-api will not be configured.'
def _detect(self):
"""Run detection, set self.available True if the service is detected."""
monasca_api = monasca_setup.detection.find_process_cmdline('monasca-api')
if monasca_api is not None:
# monasca-api can show up in urls and be an arg to this setup program, check port also
# Find the right port from the config, this is specific to the Java version
try:
with open('/etc/monasca/api-config.yml', 'r') as config:
self.api_config = yaml.safe_load(config.read())
api_port = self.api_config['server']['applicationConnectors'][0]['port']
except Exception:
api_port = 8070
log.warn('Failed parsing /etc/monasca/api-config.yml, defaulting api port to {0}'.format(api_port))
for conn in monasca_api.connections('inet'):
"""Detects if monasca-api runs in the system
Method distinguishes between Java and Python implementation
hence provides different agent's configuration.
"""
def check_port():
for conn in api_process.connections('inet'):
if conn.laddr[1] == api_port:
self.available = True
return
return True
return False
api_process = find_process_cmdline('monasca-api')
process_found = api_process is not None
if process_found:
impl_lang = _get_impl_lang(api_process)
impl_helper = self._init_impl_helper(impl_lang)
impl_helper.load_configuration()
api_port = impl_helper.get_bound_port()
port_taken = check_port()
if not port_taken:
log.error('monasca-api is not listening on port %d. %s'
% (api_port, self.PARTIAL_ERR_MSG))
return
log.info('\tmonasca-api implementation is %s', impl_lang)
self.available = port_taken
self._impl_helper = impl_helper
else:
log.error('monasca-api process has not been found. %s'
% self.PARTIAL_ERR_MSG)
def build_config(self):
"""Build the config as a Plugins object and return."""
config = monasca_setup.agent_config.Plugins()
log.info("\tEnabling the Monasca api process check")
config.merge(watch_process(['monasca-api'], 'monitoring', 'monasca-api', exact_match=False))
log.info("\tEnabling the Monasca api healthcheck")
config.merge(dropwizard_health_check('monitoring', 'monasca-api', 'http://localhost:8081/healthcheck'))
log.info("\tEnabling the Monasca api metrics")
whitelist = [
{
"name": "jvm.memory.total.max",
"path": "gauges/jvm.memory.total.max/value",
"type": "gauge"
},
{
"name": "jvm.memory.total.used",
"path": "gauges/jvm.memory.total.used/value",
"type": "gauge"
},
{
"name": "metrics.published",
"path": "meters/monasca.api.app.MetricService.metrics.published/count",
"type": "rate"
}
]
if not self._is_hibernate_on():
# if hibernate is not used, it is mysql with DBI
# for that case having below entries makes sense
log.debug('MonApi has not enabled Hibernate, adding DBI metrics')
whitelist.extend([
{
"name": "raw-sql.time.avg",
"path": "timers/org.skife.jdbi.v2.DBI.raw-sql/mean",
"type": "gauge"
},
{
"name": "raw-sql.time.max",
"path": "timers/org.skife.jdbi.v2.DBI.raw-sql/max",
"type": "gauge"
}
])
config.merge(dropwizard_metrics('monitoring',
'monasca-api',
'http://localhost:8081/metrics',
whitelist))
log.info("\tEnabling the monasca-api process check")
config.merge(watch_process(
search_strings=['monasca-api'],
service='monitoring',
component='monasca-api',
exact_match=False
))
impl_config = self._impl_helper.build_config()
if impl_config:
config.merge(impl_config)
return config
def dependencies_installed(self):
return True
def _is_hibernate_on(self):
# check if api_config has been declared in __init__
# if not it means that configuration file was not found
# or monasca-api Python implementation is running
@staticmethod
def _init_impl_helper(impl_lang):
"""Returns appropriate helper implementation.
api_config = getattr(self, 'api_config', None)
if api_config is None:
return False
:param impl_lang: implementation language, either `java` or `python`
:type impl_lang: str
:return: implementation helper
:rtype: Union(_MonAPIJavaHelper,_MonAPIPythonHelper)
hibernate_cfg = self.api_config.get('hibernate', None)
if hibernate_cfg is None:
return False
return hibernate_cfg.get('supportEnabled', False)
"""
if impl_lang == 'java':
return _MonAPIJavaHelper()
else:
return _MonAPIPythonHelper()
class MonNotification(monasca_setup.detection.Plugin):
@ -151,127 +171,75 @@ class MonNotification(monasca_setup.detection.Plugin):
class MonPersister(monasca_setup.detection.Plugin):
"""Detect mon_persister and setup monitoring."""
PARTIAL_ERR_MSG = 'Plugin for monasca-persister will not be configured.'
def _detect(self):
"""Run detection, set self.available True if the service is detected."""
if find_process_cmdline('monasca-persister') is not None:
"""Detects if monasca-persister runs in the system
Method distinguishes between Java and Python implementation
hence provides different agent's configuration.
"""
p_process = find_process_cmdline('monasca-persister')
process_found = p_process is not None
if process_found:
impl_lang = _get_impl_lang(p_process)
impl_helper = self._init_impl_helper(impl_lang)
if impl_helper is not None:
impl_helper.load_configuration()
self._impl_helper = impl_helper
self.available = True
log.info('\tmonasca-persister implementation is %s', impl_lang)
else:
log.error('monasca-persister process has not been found. %s'
% self.PARTIAL_ERR_MSG)
def build_config(self):
"""Build the config as a Plugins object and return."""
"""Read persister-config.yml file to find the exact numThreads."""
try:
with open('/etc/monasca/persister-config.yml', 'r') as config:
self.persister_config = yaml.safe_load(config.read())
except Exception:
log.exception('Failed parsing /etc/monasca/persister-config.yml')
self.available = False
return
alarm_num_threads = self.persister_config['alarmHistoryConfiguration']['numThreads']
metric_num_threads = self.persister_config['metricConfiguration']['numThreads']
database_type = self.persister_config['databaseConfiguration']['databaseType']
config = monasca_setup.agent_config.Plugins()
log.info("\tEnabling the Monasca persister process check")
config.merge(watch_process(['monasca-persister'], 'monitoring', 'monasca-persister', exact_match=False))
config.merge(watch_process(
search_strings=['monasca-persister'],
service='monitoring',
component='monasca-persister',
exact_match=False
))
if self._impl_helper is not None:
impl_config = self._impl_helper.build_config()
if impl_config:
config.merge(impl_config)
adminConnector = self.persister_config['server']['adminConnectors'][0]
try:
admin_endpoint_type = adminConnector['type']
except Exception:
admin_endpoint_type = "http"
try:
admin_endpoint_port = adminConnector['port']
except Exception:
admin_endpoint_port = 8091
log.info("\tEnabling the Monasca persister healthcheck")
config.merge(
dropwizard_health_check(
'monitoring',
'monasca-persister',
'{0}://localhost:{1}/healthcheck'.format(admin_endpoint_type,
admin_endpoint_port)))
log.info("\tEnabling the Monasca persister metrics")
whitelist = [
{
"name": "jvm.memory.total.max",
"path": "gauges/jvm.memory.total.max/value",
"type": "gauge"},
{
"name": "jvm.memory.total.used",
"path": "gauges/jvm.memory.total.used/value",
"type": "gauge"}
]
# Generate initial whitelist based on the database type
if database_type == 'influxdb':
pass
elif database_type == 'vertica':
whitelist.extend([
{
"name": "monasca.persister.repository.vertica.VerticaMetricRepo.definition-cache-hit-meter",
"path": "meters/monasca.persister.repository.vertica.VerticaMetricRepo.definition-cache-hit-meter/count",
"type": "rate"},
{
"name": "monasca.persister.repository.vertica.VerticaMetricRepo.definition-cache-miss-meter",
"path": "meters/monasca.persister.repository.vertica.VerticaMetricRepo.definition-cache-miss-meter/count",
"type": "rate"},
{
"name": "monasca.persister.repository.vertica.VerticaMetricRepo.definition-dimension-cache-hit-meter",
"path": "meters/monasca.persister.repository.vertica.VerticaMetricRepo.definition-dimension-cache-hit-meter/count",
"type": "rate"},
{
"name": "monasca.persister.repository.vertica.VerticaMetricRepo.definition-dimension-cache-miss-meter",
"path": "meters/monasca.persister.repository.vertica.VerticaMetricRepo.definition-dimension-cache-miss-meter/count",
"type": "rate"},
{
"name": "monasca.persister.repository.vertica.VerticaMetricRepo.dimension-cache-hit-meter",
"path": "meters/monasca.persister.repository.vertica.VerticaMetricRepo.dimension-cache-hit-meter/count",
"type": "rate"},
{
"name": "monasca.persister.repository.vertica.VerticaMetricRepo.dimension-cache-miss-meter",
"path": "meters/monasca.persister.repository.vertica.VerticaMetricRepo.dimension-cache-miss-meter/count",
"type": "rate"},
{
"name": "monasca.persister.repository.vertica.VerticaMetricRepo.measurement-meter",
"path": "meters/monasca.persister.repository.vertica.VerticaMetricRepo.measurement-meter/count",
"type": "rate"}
])
else:
log.warn('Failed finding database type in /etc/monasca/persister-config.yml')
# Dynamic Whitelist
for idx in range(alarm_num_threads):
new_thread = {"name": "alarm-state-transitions-added-to-batch-counter[{0}]".format(idx),
"path": "counters/monasca.persister.pipeline.event.AlarmStateTransitionHandler[alarm-state-transition-{0}].alarm-state-transitions-added-to-batch-counter/count".format(idx),
"type": "rate"
}
whitelist.append(new_thread)
for idx in range(metric_num_threads):
new_thread = {"name": "metrics-added-to-batch-counter[{0}]".format(idx),
"path": "counters/monasca.persister.pipeline.event.MetricHandler[metric-{0}].metrics-added-to-batch-counter/count".format(idx),
"type": "rate"
}
whitelist.append(new_thread)
config.merge(
dropwizard_metrics(
'monitoring',
'monasca-persister',
'{0}://localhost:{1}/metrics'.format(admin_endpoint_type,
admin_endpoint_port),
whitelist))
return config
def dependencies_installed(self):
return True
@staticmethod
def _init_impl_helper(impl_lang):
"""Returns appropriate helper implementation.
Note:
This method returns the helper only for Java.
Python implementation comes with no extra setup.
:param impl_lang: implementation language, either `java` or `python`
:type impl_lang: str
:return: implementation helper
:rtype:_MonPersisterJavaHelper
"""
if impl_lang == 'java':
return _MonPersisterJavaHelper()
return None
class MonThresh(monasca_setup.detection.Plugin):
"""Detect the running mon-thresh and monitor."""
@ -337,3 +305,384 @@ class MonInfluxDB(monasca_setup.detection.Plugin):
def dependencies_installed(self):
return True
class _MonPersisterJavaHelper(object):
"""Encapsulates Java specific configuration for monasca-persister"""
CONFIG_FILE = '/etc/monasca/persister-config.yml'
"""Default location where plugin expects configuration file"""
def __init__(self):
super(_MonPersisterJavaHelper, self).__init__()
self._cfg = None
def build_config(self):
config = monasca_setup.agent_config.Plugins()
metrics = self._collect_metrics()
self._monitor_endpoints(config, metrics)
return config
def _collect_metrics(self):
"""Collects all the metrics .
Methods will return list of all metrics that will
later be used in
:py:mod:`monasca_agent.collector.checks_d.http_metrics` to query
admin endpoint of monasca-persister.
Following group of metrics are examined:
* JVM metrics
* DB metrics ( see also :py:meth:`._collect_db_metrics` )
* Internal metrics ( see also :py:meth:`._collect_internal_metrics` )
:return: list of metrics
:rtype: list
"""
log.info("\tEnabling the monasca-persister metrics")
whitelist = [
{
"name": "jvm.memory.total.max",
"path": "gauges/jvm.memory.total.max/value",
"type": "gauge"
},
{
"name": "jvm.memory.total.used",
"path": "gauges/jvm.memory.total.used/value",
"type": "gauge"
}
]
self._collect_db_metrics(whitelist)
self._collect_internal_metrics(whitelist)
return whitelist
def _collect_db_metrics(self, whitelist):
"""Collects DB specific metrics.
Method retrieves which time-series database is used
in monaca-persister and sets up new metrics to be monitored.
Note:
Only if vertica is TSDB in monasca-persister
"""
database_type = self._cfg['databaseConfiguration']['databaseType']
if database_type == 'influxdb':
pass
elif database_type == 'vertica':
self._add_vertica_metrics(whitelist)
else:
log.warn('Failed finding database type in %s', self.CONFIG_FILE)
def _collect_internal_metrics(self, whitelist):
alarm_num_threads = self._cfg['alarmHistoryConfiguration']['numThreads']
metric_num_threads = self._cfg['metricConfiguration']['numThreads']
# Dynamic Whitelist
for idx in range(alarm_num_threads):
new_thread = {
"name": "alarm-state-transitions-added-to-batch-counter[{0}]".format(idx),
"path": "counters/monasca.persister.pipeline.event.AlarmStateTransitionHandler[alarm-state-transition-{0}].alarm-state-transitions-added-to-batch-counter/count".format(idx),
"type": "rate"
}
whitelist.append(new_thread)
for idx in range(metric_num_threads):
new_thread = {
"name": "metrics-added-to-batch-counter[{0}]".format(idx),
"path": "counters/monasca.persister.pipeline.event.MetricHandler[metric-{0}].metrics-added-to-batch-counter/count".format(idx),
"type": "rate"
}
whitelist.append(new_thread)
def _add_vertica_metrics(self, whitelist):
whitelist.extend([
{
"name": "monasca.persister.repository.vertica.VerticaMetricRepo.definition-cache-hit-meter",
"path": "meters/monasca.persister.repository.vertica.VerticaMetricRepo.definition-cache-hit-meter/count",
"type": "rate"
},
{
"name": "monasca.persister.repository.vertica.VerticaMetricRepo.definition-cache-miss-meter",
"path": "meters/monasca.persister.repository.vertica.VerticaMetricRepo.definition-cache-miss-meter/count",
"type": "rate"
},
{
"name": "monasca.persister.repository.vertica.VerticaMetricRepo.definition-dimension-cache-hit-meter",
"path": "meters/monasca.persister.repository.vertica.VerticaMetricRepo.definition-dimension-cache-hit-meter/count",
"type": "rate"
},
{
"name": "monasca.persister.repository.vertica.VerticaMetricRepo.definition-dimension-cache-miss-meter",
"path": "meters/monasca.persister.repository.vertica.VerticaMetricRepo.definition-dimension-cache-miss-meter/count",
"type": "rate"
},
{
"name": "monasca.persister.repository.vertica.VerticaMetricRepo.dimension-cache-hit-meter",
"path": "meters/monasca.persister.repository.vertica.VerticaMetricRepo.dimension-cache-hit-meter/count",
"type": "rate"
},
{
"name": "monasca.persister.repository.vertica.VerticaMetricRepo.dimension-cache-miss-meter",
"path": "meters/monasca.persister.repository.vertica.VerticaMetricRepo.dimension-cache-miss-meter/count",
"type": "rate"
},
{
"name": "monasca.persister.repository.vertica.VerticaMetricRepo.measurement-meter",
"path": "meters/monasca.persister.repository.vertica.VerticaMetricRepo.measurement-meter/count",
"type": "rate"
}
])
def _monitor_endpoints(self, config, metrics):
admin_connector = self._cfg['server']['adminConnectors'][0]
try:
admin_endpoint_type = admin_connector['type']
except Exception:
admin_endpoint_type = "http"
try:
admin_endpoint_port = admin_connector['port']
except Exception:
admin_endpoint_port = 8091
log.info("\tEnabling the Monasca persister healthcheck")
config.merge(
dropwizard_health_check(
'monitoring',
'monasca-persister',
'{0}://localhost:{1}/healthcheck'.format(admin_endpoint_type,
admin_endpoint_port)))
log.info("\tEnabling the Monasca persister metrics")
config.merge(
dropwizard_metrics(
'monitoring',
'monasca-persister',
'{0}://localhost:{1}/metrics'.format(admin_endpoint_type,
admin_endpoint_port),
metrics))
def load_configuration(self):
"""Loads java specific configuration.
Load java specific configuration from:
* :py:attr:`API_CONFIG_YML`
:return: True if both configuration files were successfully loaded
:rtype: bool
"""
try:
with open(self.CONFIG_FILE, 'r') as config:
self._cfg = yaml.safe_load(config.read())
except Exception as ex:
log.error('Failed to parse %s', self.CONFIG_FILE)
log.exception(ex)
raise ex
class _MonAPIJavaHelper(object):
"""Encapsulates Java specific configuration for monasca-api"""
CONFIG_FILE = '/etc/monasca/api-config.yml'
def __init__(self):
super(_MonAPIJavaHelper, self).__init__()
self._api_config = None
def build_config(self):
"""Builds monitoring configuration for monasca-api Java flavour.
Method configures additional checks that are specific for
Java implementation of monasca-api.
List of checks:
* HttpCheck, :py:mod:`monasca_agent.collector.checks_d.http_check`
* HttpMetrics, :py:mod:`monasca_agent.collector.checks_d.http_metrics`
"""
config = monasca_setup.agent_config.Plugins()
log.info("\tEnabling the Monasca api metrics")
whitelist = [
{
"name": "jvm.memory.total.max",
"path": "gauges/jvm.memory.total.max/value",
"type": "gauge"
},
{
"name": "jvm.memory.total.used",
"path": "gauges/jvm.memory.total.used/value",
"type": "gauge"
},
{
"name": "metrics.published",
"path": "meters/monasca.api.app.MetricService.metrics.published/count",
"type": "rate"
}
]
if not self._is_hibernate_on():
# if hibernate is not used, it is mysql with DBI
# for that case having below entries makes sense
log.debug(
'monasca-api has not enabled Hibernate, adding DBI metrics')
whitelist.extend([
{
"name": "raw-sql.time.avg",
"path": "timers/org.skife.jdbi.v2.DBI.raw-sql/mean",
"type": "gauge"
},
{
"name": "raw-sql.time.max",
"path": "timers/org.skife.jdbi.v2.DBI.raw-sql/max",
"type": "gauge"
}
])
self._monitor_endpoints(config, whitelist)
return config
def _monitor_endpoints(self, config, metrics):
admin_connector = self._api_config['server']['adminConnectors'][0]
try:
admin_endpoint_type = admin_connector['type']
except Exception:
admin_endpoint_type = "http"
try:
admin_endpoint_port = admin_connector['port']
except Exception:
admin_endpoint_port = 8081
healthcheck_url = '{0}://localhost:{1}/healthcheck'.format(
admin_endpoint_type, admin_endpoint_port)
metric_url = '{0}://localhost:{1}/metrics'.format(
admin_endpoint_type, admin_endpoint_port)
log.info("\tEnabling the monasca-api healthcheck")
config.merge(dropwizard_health_check('monitoring', 'monasca-api',
healthcheck_url))
log.info("\tEnabling the monasca-api metrics")
config.merge(dropwizard_metrics('monitoring', 'monasca-api',
metric_url, metrics))
def _is_hibernate_on(self):
# check if api_config has been declared in __init__
# if not it means that configuration file was not found
# or monasca-api Python implementation is running
api_config = getattr(self, '_api_config', None)
if api_config is None:
return False
hibernate_cfg = self._api_config.get('hibernate', None)
if hibernate_cfg is None:
return False
return hibernate_cfg.get('supportEnabled', False)
def load_configuration(self):
"""Loads java specific configuration.
Load java specific configuration from:
* :py:attr:`API_CONFIG_YML`
:return: True if both configuration files were successfully loaded
:rtype: bool
"""
try:
with open(self.CONFIG_FILE, 'r') as config:
self._api_config = yaml.safe_load(config.read())
except Exception as ex:
log.error('Failed to parse %s', self.CONFIG_FILE)
log.exception(ex)
raise ex
def get_bound_port(self):
"""Returns port API is listening on.
Method tries to read port from the '/etc/monasca/api-config.yml'
file. In case if:
* file was not found in specified location
* file could be read from the file system
* file was changed and assumed location of port changed
code rollbacks to :py:const:`_DEFAULT_API_PORT`
:return: TCP port api is listening on
:rtype: int
"""
if self._api_config is None:
return _DEFAULT_API_PORT
try:
return self._api_config['server']['applicationConnectors'][0]['port']
except Exception as ex:
log.error('Failed to read api port from '
'/etc/monasca/api-config.yml')
log.exception(ex)
return _DEFAULT_API_PORT
class _MonAPIPythonHelper(object):
"""Encapsulates Python specific configuration for monasca-api"""
CONFIG_FILE = '/etc/monasca/api-config.ini'
def __init__(self):
super(_MonAPIPythonHelper, self).__init__()
self._paste_config = None
def build_config(self):
# note(trebskit) intentionally left empty because gunicorn check
# seems to have some serious issues with monitoring gunicorn process
# see https://bugs.launchpad.net/monasca/+bug/1646481
return None
def load_configuration(self):
"""Loads INI file from specified path.
Method loads configuration from specified `path`
and parses it with :py:class:`configparser.RawConfigParser`
"""
cp = configparser.RawConfigParser()
try:
cp.readfp(open(self.CONFIG_FILE, 'r'))
self._paste_config = cp
except Exception as ex:
log.error('Failed to parse %s', self.CONFIG_FILE)
log.exception(ex)
raise ex
def get_bound_port(self):
"""Returns port API is listening on.
Method tries to read port from the '/etc/monasca/api-config.ini'
file. In case if:
* file was not found in specified location
* file could be read from the file system
* file was changed and assumed location of port changed
code rollbacks to :py:const:`_DEFAULT_API_PORT`
:return: TCP port api is listening on
:rtype: int
"""
if not self._paste_config:
return _DEFAULT_API_PORT
return self._paste_config.getint('server:main', 'port')

534
tests/detection/test_mon.py Normal file
View File

@ -0,0 +1,534 @@
# Copyright 2016 FUJITSU LIMITED
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import random
import unittest
import logging
import mock
import psutil
from monasca_setup.detection.plugins import mon
_PYTHON_CMD_API = ('/opt/monasca-api/bin/gunicorn'
' -n monasca-api'
' -k eventlet --worker-connections=2000 '
'--backlog=1000 '
'--paste /etc/monasca/api-config.ini -w 9')
_JAVA_CMD_API = ('/usr/bin/java '
'-Dfile.encoding=UTF-8 -Xmx128m '
'-cp /opt/monasca/monasca-api.jar '
'monasca.api.MonApiApplication server '
'/etc/monasca/api-config.yml')
_PYTHON_CMD_PERSISTER = ('/opt/monasca-persister/bin/python '
'/opt/monasca-persister/lib/python2.7/'
'site-packages/monasca_persister/persister.py'
' --config-file /etc/monasca/persister.conf')
_JAVA_CMD_PERSISTER = ('/usr/bin/java -Dfile.encoding=UTF-8 -Xmx128m '
'-cp /opt/monasca/monasca-persister.jar '
'monasca.persister.PersisterApplication server '
'/etc/monasca/persister-config.yml')
_JAVA_YML_CFG_BIT_API = '''
hibernate:
supportEnabled: {hibernate_enabled}
server:
adminConnectors:
- type: {admin_type}
port: {admin_port}
applicationConnectors:
- port: {app_port}
'''
_JAVA_YML_CFG_BIT_PERSISTER = '''
alarmHistoryConfiguration:
numThreads: {ah_threads}
metricConfiguration:
numThreads: {mc_threads}
databaseConfiguration:
databaseType: {db_type}
server:
adminConnectors:
- type: {admin_type}
port: {admin_port}
'''
LOG = logging.getLogger(mon.__name__)
class FakeInetConnection(object):
def __init__(self, api_port=None):
if api_port is None:
api_port = 8070 # default one
self.laddr = [1, api_port]
class FakeProcesses(object):
name = None
cmdLine = None
inetConnections = [
FakeInetConnection()
]
def as_dict(self):
return {'name': FakeProcesses.name,
'cmdline': FakeProcesses.cmdLine}
def cmdline(self):
return self.cmdLine
def exe(self):
return self.cmdLine[0]
def connections(self, *args):
return FakeProcesses.inetConnections
class TestGetImplLang(unittest.TestCase):
@mock.patch('psutil.Process')
def test_should_return_python_lang_for_gunicorn_process(self, proc):
proc.exe.return_value = '/opt/monasca-api/bin/gunicorn'
self.assertEqual('python', mon._get_impl_lang(proc))
@mock.patch('psutil.Process')
def test_should_return_python_lang_for_python_process(self, proc):
proc.exe.return_value = '/usr/bin/python'
self.assertEqual('python', mon._get_impl_lang(proc))
@mock.patch('psutil.Process')
def test_should_return_java_lang_for_java_process(self, proc):
proc.exe.return_value = '/usr/bin/java'
self.assertEqual('java', mon._get_impl_lang(proc))
@mock.patch('psutil.Process')
def test_should_throw_error_for_unknown_impl(self, proc):
proc.exe.return_value = '/usr/bin/cat'
self.assertRaises(Exception, mon._get_impl_lang, proc)
class TestMonPersisterDetectionPlugin(unittest.TestCase):
def setUp(self):
super(TestMonPersisterDetectionPlugin, self).setUp()
FakeProcesses.name = 'monasca-persister'
with mock.patch.object(mon.MonPersister, '_detect') as mock_detect:
self._mon_p = mon.MonPersister('foo')
self.assertTrue(mock_detect.called)
@mock.patch(
'monasca_setup.detection.plugins.mon._MonPersisterJavaHelper')
def test_should_use_java_helper_if_persister_is_java(self,
impl_helper):
FakeProcesses.cmdLine = [_JAVA_CMD_PERSISTER]
self._mon_p._init_impl_helper = iih = mock.Mock(
return_value=impl_helper)
self._detect()
iih.assert_called_once_with('java')
self.assertTrue(impl_helper.load_configuration.called_once)
def test_should_detect_java_persister_has_config(self):
FakeProcesses.cmdLine = [_JAVA_CMD_PERSISTER]
yml_cfg = _JAVA_YML_CFG_BIT_PERSISTER.format(
ah_threads=5,
mc_threads=5,
db_type="influxdb",
admin_type="http",
admin_port=6666
)
with mock.patch(
"__builtin__.open",
mock.mock_open(read_data=yml_cfg)) as mf:
self._detect()
mf.assert_called_once_with('/etc/monasca/persister-config.yml',
'r')
self.assertTrue(self._mon_p.available)
@mock.patch('six.moves.configparser.RawConfigParser')
def test_should_detect_python_persister_has_config(self, _):
# NOTE(trebskit) this cannot use mocking the read of the file
# because when either RawConfigParser or mock_open messes up with
# reading the one file line. Instead returning empty line,
# StopIteration is raised and RawConfigParser does not ignore that or
# catch it
#
# to sum it up => ;-(((
FakeProcesses.cmdLine = [_PYTHON_CMD_PERSISTER]
self._mon_p._init_impl_helper = mock.Mock(return_value=mock.Mock())
self._detect()
self.assertTrue(self._mon_p.available)
def test_build_java_config(self):
FakeProcesses.cmdLine = [_JAVA_CMD_PERSISTER]
# note(trebskit) this is always set to 2
jvm_metrics_count = 2
alarm_history_threads = random.randint(1, 5)
metrics_history_threads = random.randint(1, 5)
admin_port = random.randint(1000, 10000)
admin_type = 'http'
if admin_port % 2 != 0:
admin_type += 's'
yml_cfg = _JAVA_YML_CFG_BIT_PERSISTER.format(
ah_threads=alarm_history_threads,
mc_threads=metrics_history_threads,
db_type="influxdb",
admin_type=admin_type,
admin_port=admin_port
)
with mock.patch(
"__builtin__.open",
mock.mock_open(read_data=yml_cfg)) as mf:
self._detect()
conf = self._build_config()
mf.assert_called_once_with('/etc/monasca/persister-config.yml',
'r')
for key in ('http_check', 'http_metrics', 'process'):
self.assertIn(key, conf)
bit = conf[key]
self.assertIsNotNone(bit)
self.assertNotEqual({}, bit)
# detailed assertions
# http_check
http_check_instance = conf['http_check']['instances'][0]
self.assertFalse(http_check_instance['include_content'])
self.assertEqual('monitoring-monasca-persister healthcheck',
http_check_instance['name'])
self.assertEqual(5, http_check_instance['timeout'])
self.assertEqual('%s://localhost:%d/healthcheck'
% (admin_type, admin_port),
http_check_instance['url'])
# http_metrics
http_metrics_instance = conf['http_metrics']['instances'][0]
self.assertEqual('monitoring-monasca-persister metrics',
http_metrics_instance['name'])
self.assertEqual('%s://localhost:%d/metrics'
% (admin_type, admin_port),
http_metrics_instance['url'])
hmi_whitelist = http_metrics_instance['whitelist']
self.assertIsNotNone(hmi_whitelist)
self.assertEqual(len(hmi_whitelist), (
alarm_history_threads +
metrics_history_threads +
jvm_metrics_count))
jvm_metrics_found = 0
ah_metrics_found = 0
mh_metrics_found = 0
for entry in hmi_whitelist:
name = entry['name']
if 'jvm' in name:
jvm_metrics_found += 1
elif 'alarm-state' in name:
ah_metrics_found += 1
elif 'metrics-added' in name:
mh_metrics_found += 1
self.assertEqual(jvm_metrics_count, jvm_metrics_found)
self.assertEqual(alarm_history_threads, ah_metrics_found)
self.assertEqual(metrics_history_threads, mh_metrics_found)
# process
process_instance = conf['process']['instances'][0]
self.assertEqual('monasca-persister', process_instance['name'])
self.assertFalse(process_instance['exact_match'])
self.assertTrue(process_instance['detailed'])
self.assertDictEqual({
'component': 'monasca-persister',
'service': 'monitoring'
}, process_instance['dimensions'])
def test_build_python_config(self):
FakeProcesses.cmdLine = [_PYTHON_CMD_PERSISTER]
self._detect()
conf = self._build_config()
for key in ('process',):
self.assertIn(key, conf)
bit = conf[key]
self.assertIsNotNone(bit)
self.assertNotEqual({}, bit)
# process
process_instance = conf['process']['instances'][0]
self.assertEqual('monasca-persister', process_instance['name'])
self.assertFalse(process_instance['exact_match'])
self.assertTrue(process_instance['detailed'])
self.assertDictEqual({
'component': 'monasca-persister',
'service': 'monitoring'
}, process_instance['dimensions'])
def _detect(self):
self._mon_p.available = False
process_iter = mock.patch.object(psutil, 'process_iter',
return_value=[FakeProcesses()])
with process_iter as mock_process_iter:
self._mon_p._detect()
self.assertTrue(mock_process_iter.called)
def _build_config(self):
conf = self._mon_p.build_config()
self.assertIsNotNone(conf)
self.assertNotEqual({}, conf)
return conf
class TestMonAPIDetectionPlugin(unittest.TestCase):
def setUp(self):
FakeProcesses.name = 'monasca-api'
super(TestMonAPIDetectionPlugin, self).setUp()
with mock.patch.object(mon.MonAPI, '_detect') as mock_detect:
self._mon_api = mon.MonAPI('foo')
self.assertTrue(mock_detect.called)
@mock.patch('monasca_setup.detection.plugins.mon._MonAPIPythonHelper')
def test_should_use_python_helper_if_api_is_python(self, impl_helper):
FakeProcesses.cmdLine = [_PYTHON_CMD_API]
self._mon_api._init_impl_helper = iih = mock.Mock(
return_value=impl_helper)
self._detect()
iih.assert_called_once_with('python')
self.assertTrue(impl_helper.load_configuration.called_once)
self.assertTrue(impl_helper.get_bound_port.called_once)
@mock.patch('monasca_setup.detection.plugins.mon._MonAPIJavaHelper')
def test_should_use_java_helper_if_api_is_java(self, impl_helper):
FakeProcesses.cmdLine = [_JAVA_CMD_API]
self._mon_api._init_impl_helper = iih = mock.Mock(
return_value=impl_helper)
self._detect()
iih.assert_called_once_with('java')
self.assertTrue(impl_helper.load_configuration.called_once)
self.assertTrue(impl_helper.get_bound_port.called_once)
def test_should_detect_java_api_has_config(self):
app_port = random.randint(1000, 10000)
admin_port = random.randint(1000, 10000)
admin_type = 'http'
if admin_port % 2 != 0:
admin_type += 's'
FakeProcesses.cmdLine = [_JAVA_CMD_API]
FakeProcesses.inetConnections = [FakeInetConnection(app_port)]
yml_cfg = _JAVA_YML_CFG_BIT_API.format(
app_port=app_port,
admin_port=admin_port,
admin_type=admin_type,
hibernate_enabled=False
)
with mock.patch(
"__builtin__.open",
mock.mock_open(read_data=yml_cfg)) as mock_file:
self._detect()
mock_file.assert_called_once_with('/etc/monasca/api-config.yml',
'r')
self.assertTrue(self._mon_api.available)
@mock.patch('six.moves.configparser.RawConfigParser')
def test_should_detect_python_api_has_config(self, rcp):
expected_port = 6666
actual_port = 6666
FakeProcesses.cmdLine = [_PYTHON_CMD_API]
FakeProcesses.inetConnections = [FakeInetConnection(actual_port)]
# make sure we return the port as we would read from the cfg
rcp.getint.return_value = expected_port
# override configuration to make sure we read correct port
impl_helper = mon._MonAPIPythonHelper()
impl_helper._paste_config = rcp
impl_helper.load_configuration = mock.Mock()
self._mon_api._init_impl_helper = mock.Mock(return_value=impl_helper)
self._detect()
self.assertTrue(self._mon_api.available)
@mock.patch('six.moves.configparser.RawConfigParser')
def test_should_not_detect_if_port_dont_match(self, rcp):
expected_port = 6666
actual_port = 8070
# assume having python implementation
FakeProcesses.cmdLine = [_PYTHON_CMD_API]
FakeProcesses.inetConnections = [FakeInetConnection(actual_port)]
# make sure we return the port as we would read from the cfg
rcp.getint.return_value = expected_port
# override configuration to make sure we read correct port
impl_helper = mon._MonAPIPythonHelper()
impl_helper._paste_config = rcp
impl_helper.load_configuration = mock.Mock()
self._mon_api._init_impl_helper = mock.Mock(return_value=impl_helper)
with mock.patch.object(LOG, 'error') as mock_log_error:
self._detect()
self.assertFalse(self._mon_api.available)
mock_log_error.assert_called_with('monasca-api is not listening '
'on port %d. Plugin for '
'monasca-api will not '
'be configured.' % expected_port)
def test_build_java_config_no_hibernate(self):
self._run_java_build_config(False)
def test_build_java_config_with_hibernate(self):
self._run_java_build_config(True)
@mock.patch('six.moves.configparser.RawConfigParser')
def test_build_python_config(self, rcp):
expected_port = 8070
FakeProcesses.cmdLine = [_PYTHON_CMD_API]
FakeProcesses.inetConnections = [FakeInetConnection(expected_port)]
rcp.getint.return_value = expected_port
impl_helper = mon._MonAPIPythonHelper()
impl_helper._paste_config = rcp
impl_helper.load_configuration = mock.Mock()
self._mon_api._init_impl_helper = mock.Mock(return_value=impl_helper)
self._detect()
conf = self._build_config()
for key in ('process', ):
self.assertIn(key, conf)
bit = conf[key]
self.assertIsNotNone(bit)
self.assertNotEqual({}, bit)
def _run_java_build_config(self, hibernate_enabled):
FakeProcesses.cmdLine = [_JAVA_CMD_API]
app_port = random.randint(1000, 10000)
admin_port = random.randint(1000, 10000)
admin_type = 'http'
if admin_port % 2 != 0:
admin_type += 's'
FakeProcesses.cmdLine = [_JAVA_CMD_API]
FakeProcesses.inetConnections = [FakeInetConnection(app_port)]
# note(trebskit) this is always set to 2
jvm_metrics_count = 2
internal_metrics_count = 1
sql_timers_count = 2
total_metrics_count = jvm_metrics_count + internal_metrics_count + (
sql_timers_count if not hibernate_enabled else 0)
yml_cfg = _JAVA_YML_CFG_BIT_API.format(
app_port=app_port,
admin_port=admin_port,
admin_type=admin_type,
hibernate_enabled=hibernate_enabled
)
with mock.patch(
"__builtin__.open",
mock.mock_open(read_data=yml_cfg)) as mf:
self._detect()
conf = self._build_config()
mf.assert_called_once_with('/etc/monasca/api-config.yml',
'r')
for key in ('http_check', 'http_metrics', 'process'):
self.assertIn(key, conf)
bit = conf[key]
self.assertIsNotNone(bit)
self.assertNotEqual({}, bit)
# verify http_check
http_check_instance = conf['http_check']['instances'][0]
self.assertFalse(http_check_instance['include_content'])
self.assertEqual('monitoring-monasca-api healthcheck',
http_check_instance['name'])
self.assertEqual(5, http_check_instance['timeout'])
self.assertEqual('%s://localhost:%d/healthcheck'
% (admin_type, admin_port),
http_check_instance['url'])
# verify http_metrics
http_metrics_instance = conf['http_metrics']['instances'][0]
self.assertEqual('monitoring-monasca-api metrics',
http_metrics_instance['name'])
self.assertEqual('%s://localhost:%d/metrics'
% (admin_type, admin_port),
http_metrics_instance['url'])
hmi_whitelist = http_metrics_instance['whitelist']
self.assertIsNotNone(hmi_whitelist)
self.assertEqual(len(hmi_whitelist), total_metrics_count)
jvm_metrics_found = 0
internal_metrics_found = 0
sql_timers_metrics_found = 0
for entry in hmi_whitelist:
name = entry['name']
if 'jvm' in name:
jvm_metrics_found += 1
elif 'metrics.published' in name:
internal_metrics_found += 1
elif 'raw-sql.time' in name:
sql_timers_metrics_found += 1
self.assertEqual(jvm_metrics_count, jvm_metrics_found)
self.assertEqual(internal_metrics_count, internal_metrics_found)
if not hibernate_enabled:
self.assertEqual(sql_timers_count, sql_timers_count)
def _build_config(self):
conf = self._mon_api.build_config()
self.assertIsNotNone(conf)
self.assertNotEqual({}, conf)
return conf
def _detect(self):
self._mon_api.available = False
process_iter = mock.patch.object(psutil, 'process_iter',
return_value=[FakeProcesses()])
with process_iter as mock_process_iter:
self._mon_api._detect()
self.assertTrue(mock_process_iter.called)