Files
monasca-agent/monasca_setup/detection/plugins/mon.py
Michał Piotrowski 538aec2e1b Update log level classification info
It is possible monasca-setup configuration process informs user about
errors related to setting up other services, but system is working
correctly. It is expected to change some log level classification for
INFO or WARNING depends on type of message.

Story: 2004970
Task: 29425

Change-Id: Idb8101fea6e7c5c357d72d77b3b264db4cce8527
2019-02-15 08:36:17 +01:00

799 lines
28 KiB
Python

# (C) Copyright 2015-2016 Hewlett Packard Enterprise Development LP
# Copyright 2016 FUJITSU LIMITED
# Copyright 2017 SUSE Linux GmbH
# 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.
"""Classes for monitoring the monitoring server stack.
Covering mon-persister, mon-api and mon-thresh.
Kafka, mysql, vertica and influxdb are covered by other detection plugins. Mon-notification
uses statsd.
"""
import logging
import re
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.utils import get_agent_username
from monasca_setup.detection.utils import watch_process
from monasca_setup.detection.utils import watch_process_by_username
log = logging.getLogger(__name__)
_APACHE_MARKERS = 'httpd', 'apache',
"""List of all strings in process command line that indicate application
runs in Apache/mod_wsgi"""
_PYTHON_LANG_MARKERS = ('python', 'gunicorn') + _APACHE_MARKERS
"""List of all strings that if found in process exe
mean that application runs under Python"""
_JAVA_LANG_MARKERS = 'java',
"""List of all strings that if found in process exe
mean that application runs under 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.as_dict(['exe'])['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']
for process in agent_process_list:
if find_process_cmdline(process) is None:
self.available = False
return
def build_config(self):
"""Build the config as a Plugins object and return."""
log.info("\tEnabling the Monasca Agent process check")
return watch_process_by_username(get_agent_username(), 'monasca-agent',
'monitoring', 'monasca-agent')
def dependencies_installed(self):
return True
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):
"""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:
return True
return False
def correct_apache_listener(process):
"""Sets api_process to the parent httpd process.
Method evaluates if process executable is correlated
with apache-mod_wsgi. If so, retrieves parent process.
Otherwise returns None
:param process: current process
:type process: psutil.Process
:returns: parent process or None
:rtype: (psutil.Process, None)
"""
p_exe = process.as_dict(['exe'])['exe']
for m in _APACHE_MARKERS:
if m in p_exe:
return process.parent()
return None
api_process = find_process_cmdline('monasca-api')
process_found = api_process is not None
if process_found:
impl_lang = _get_impl_lang(api_process)
if impl_lang == 'python':
apache_process = correct_apache_listener(api_process)
if apache_process:
log.info('\tmonasca-api runs under Apache WSGI')
api_process = apache_process
impl_helper = self._init_impl_helper(api_process.as_dict(['cmdline'])['cmdline'],
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.warning('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(
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 _init_impl_helper(self, cmdline, impl_lang):
"""Returns appropriate helper implementation.
:param impl_lang: implementation language, either `java` or `python`
:type impl_lang: str
:return: implementation helper
:rtype: Union(_MonAPIJavaHelper,_MonAPIPythonHelper)
"""
if impl_lang == 'java':
return _MonAPIJavaHelper(cmdline=cmdline)
else:
return _MonAPIPythonHelper(cmdline=cmdline, args=self.args)
class MonNotification(monasca_setup.detection.Plugin):
"""Detect the Monsaca notification engine and setup some simple checks."""
def _detect(self):
"""Run detection, set self.available True if the service is detected."""
if find_process_cmdline('monasca-notification') is not None:
self.available = True
def build_config(self):
"""Build the config as a Plugins object and return."""
log.info("\tEnabling the Monasca Notification healthcheck")
notification_process = find_process_cmdline('monasca-notification')
notification_user = notification_process.as_dict(['username'])['username']
return watch_process_by_username(notification_user, 'monasca-notification',
'monitoring', 'monasca-notification')
def dependencies_installed(self):
return True
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):
"""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(
p_process.as_dict(['cmdline'])['cmdline'],
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.info('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."""
config = monasca_setup.agent_config.Plugins()
log.info("\tEnabling the Monasca persister process check")
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)
return config
def dependencies_installed(self):
return True
@staticmethod
def _init_impl_helper(cmdline, 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(cmdline=cmdline)
return None
class MonThresh(monasca_setup.detection.Plugin):
"""Detect the running mon-thresh and monitor."""
def _detect(self):
"""Run detection, set self.available True if the service is detected."""
# The node will be running either nimbus or supervisor or both
self.available = (find_process_cmdline('storm.daemon.nimbus') is not None or
find_process_cmdline('storm.daemon.supervisor') is not None)
def build_config(self):
"""Build the config as a Plugins object and return."""
log.info("\tWatching the mon-thresh process.")
config = monasca_setup.agent_config.Plugins()
for process in ['storm.daemon.nimbus', 'storm.daemon.supervisor', 'storm.daemon.worker']:
if find_process_cmdline(process) is not None:
config.merge(
watch_process(
[process],
'monitoring',
'apache-storm',
exact_match=False,
detailed=False))
config.merge(
watch_process_by_username(
'storm',
'monasca-thresh',
'monitoring',
'apache-storm'))
return config
def dependencies_installed(self):
return True
def dropwizard_health_check(service, component, url):
"""Setup a dropwizard heathcheck to be watched by the http_check plugin."""
config = monasca_setup.agent_config.Plugins()
config['http_check'] = {'init_config': None,
'instances': [
{'name': "{0}-{1} healthcheck".format(service, component),
'url': url,
'timeout': 5,
'include_content': False,
'dimensions': {'service': service, 'component': component}}]}
return config
def dropwizard_metrics(service, component, url, whitelist):
"""Setup a dropwizard metrics check"""
config = monasca_setup.agent_config.Plugins()
config['http_metrics'] = {'init_config': None,
'instances': [{'name': "{0}-{1} metrics".format(service, component),
'url': url,
'timeout': 5,
'dimensions': {'service': service,
'component': component},
'whitelist': whitelist}]}
return config
class _DropwizardJavaHelper(object):
"""Mixing to locate configuration file for DropWizard app
Class utilizes process of search the configuartion file
for:
* monasca-api [**Java**]
* monasca-persister [**Java**]
"""
YAML_PATTERN = re.compile('.*\.ya?ml', re.IGNORECASE)
def __init__(self, cmdline=None):
self._cmdline = cmdline
def load_configuration(self):
"""Loads java specific configuration.
Load java specific configuration from:
* :py:attr:`DEFAULT_CONFIG_FILE`
:return: True if both configuration files were successfully loaded
:rtype: bool
"""
try:
config_file = self._get_config_file()
self._read_config_file(config_file)
except Exception as ex:
log.error('Failed to parse %s', config_file)
log.exception(ex)
raise ex
def _find_config_file_in_cmdline(self, cmdline):
# note(trebskit) file should be mentioned
# somewhere in the end of cmdline
for item in cmdline[::-1]:
if self.YAML_PATTERN.match(item):
return item
return None
def _read_config_file(self, config_file):
with open(config_file, 'r') as config:
self._cfg = yaml.safe_load(config.read())
def _get_config_file(self):
if self._cmdline:
config_file = self._find_config_file_in_cmdline(
cmdline=self._cmdline
)
if config_file:
log.debug('\tFound %s for java configuration from CLI',
config_file)
return config_file
config_file = self.DEFAULT_CONFIG_FILE
log.debug('\tAssuming default configuration file=%s', config_file)
return config_file
class _MonPersisterJavaHelper(_DropwizardJavaHelper):
"""Encapsulates Java specific configuration for monasca-persister"""
DEFAULT_CONFIG_FILE = '/etc/monasca/persister-config.yml'
"""Default location where plugin expects configuration file"""
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 monasca-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.DEFAULT_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))
class _MonAPIJavaHelper(_DropwizardJavaHelper):
"""Encapsulates Java specific configuration for monasca-api"""
DEFAULT_CONFIG_FILE = '/etc/monasca/api-config.yml'
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._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 = 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
cfg = getattr(self, '_cfg', None)
if cfg is None:
return False
hibernate_cfg = cfg.get('hibernate', None)
if hibernate_cfg is None:
return False
return hibernate_cfg.get('supportEnabled', False)
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._cfg is None:
return _DEFAULT_API_PORT
try:
return self._cfg['server']['applicationConnectors'][0]['port']
except Exception as ex:
log.error('Failed to read api port from configuration file')
log.exception(ex)
return _DEFAULT_API_PORT
class _MonAPIPythonHelper(object):
"""Encapsulates Python specific configuration for monasca-api"""
DEFAULT_CONFIG_FILE = '/etc/monasca/api-config.ini'
PASTE_CLI_OPTS = '--paste', '--paster',
"""Possible flags passed to gunicorn processed,
pointing at paste file"""
def __init__(self, cmdline=None, args=None):
super(_MonAPIPythonHelper, self).__init__()
self._cmdline = cmdline
self._args = args
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`
"""
try:
config_file = self._get_config_file()
self._paste_config = self._read_config_file(config_file)
except Exception as ex:
log.error('Failed to parse %s', 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')
def _read_config_file(self, config_file):
cp = configparser.RawConfigParser()
return cp.readfp(open(config_file, 'r'))
def _get_config_file(self):
"""Method gets configuration file of Python monasca-api.
Method tries to examine following locations:
* cmdline of process (looking for either
of :py:attr:`_MonAPIPythonHelper.PASTE_CLI_OPTS`)
* this plugin args
loooking for location of configuration file
:param args: plugin arguments
:type args: dict
"""
if self._cmdline:
# we're interested in PASTE_CLI_OPTS
for paste in self.PASTE_CLI_OPTS:
if paste in self._cmdline:
pos = self._cmdline.index(paste)
flag = self._cmdline[pos]
config_file = self._cmdline[pos + 1]
if config_file:
log.debug(('\tFound %s=%s for python configuration '
'from CLI'),
flag, config_file)
return config_file
else:
log.warn(('\tCannot determine neither %s from process'
'cmdline'), self.PASTE_CLI_OPTS)
if self._args and 'paste-file' in self._args:
# check if args mentions config file param
config_file = self._args.get('paste-file', None)
log.debug(('\tFound paste-file=%s for python configuration '
'passed as plugin argument'), config_file)
return config_file
config_file = self.DEFAULT_CONFIG_FILE
log.debug('\tAssuming default paste_file=%s', config_file)
return config_file