diff --git a/conf/glances.conf b/conf/glances.conf index 594d0138..62f01d9b 100644 --- a/conf/glances.conf +++ b/conf/glances.conf @@ -155,7 +155,7 @@ mem_careful=50 mem_warning=70 mem_critical=90 -#[monitor] +[monitor] # Define the list of processes to monitor # *** This section is optional *** # The list is composed of items (list_#nb <= 10) @@ -181,7 +181,7 @@ mem_critical=90 #list_3_regex=.*xeyes.* #list_3_countmin=1 -#[serverlist] +[serverlist] # Define the static servers list #server_1_name=localhost #server_1_alias=My local PC @@ -194,7 +194,7 @@ mem_critical=90 #server_4_name=pasbon #server_4_port=61237 -#[passwords] +[passwords] # Define the passwords list # Syntax: host=password # Where: host is the hostname @@ -250,3 +250,13 @@ user=guest password=guest queue=glances_queue +###### +# AMPS +###### + +[nginx] +# Nginx status page should be enable (https://easyengine.io/tutorials/nginx/status-page/) +enable=true +regex=\/usr\/sbin\/nginx +refresh=60 +status_url=http://localhost/nginx_status diff --git a/glances/amps/__init__.py b/glances/amps/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/glances/amps/glances_amp.py b/glances/amps/glances_amp.py new file mode 100644 index 00000000..1a95f68a --- /dev/null +++ b/glances/amps/glances_amp.py @@ -0,0 +1,116 @@ +# -*- coding: utf-8 -*- +# +# This file is part of Glances. +# +# Copyright (C) 2016 Nicolargo +# +# Glances is free software; you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Glances is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program. If not, see . + +""" +I am your father... + +...for all Glances Application Monitoring Processes (AMP). +""" + +from glances.compat import u +from glances.logger import logger + + +class GlancesAmp(object): + + """Main class for Glances AMP.""" + + def __init__(self, args=None): + """Init AMP classe.""" + # AMP name (= module name without glances_) + self.amp_name = self.__class__.__module__[len('glances_'):] + + # Init the args + self.args = args + + # Init the configs + self.configs = {} + + def load_config(self, config): + """Load AMP parameters from the configuration file.""" + + # Read AMP confifuration. + # For ex, the AMP foo should have the following section: + # + # [foo] + # enable=true + # regex=\/usr\/bin\/nginx + # refresh=60 + # + # and optionnaly: + # + # option1=opt1 + # + if (hasattr(config, 'has_section') and + config.has_section(self.amp_name)): + logger.debug("AMP: Load {0} configuration".format(self.amp_name)) + for param, _ in config.items(self.amp_name): + try: + self.configs[param] = config.get_float_value(self.amp_name, param) + except ValueError: + self.configs[param] = config.get_value(self.amp_name, param).split(',') + if len(self.configs[param]) == 1: + self.configs[param] = self.configs[param][0] + logger.debug("AMP: Load {0} parameter: {1} = {2}".format(self.amp_name, param, self.configs[param])) + else: + logger.warning("AMP: Can not find section {0} in the configuration file".format(self.amp_name)) + + # enable, regex and refresh are mandatories + # if not configured then AMP is disabled + for k in ['enable', 'regex', 'refresh']: + if k not in self.configs: + logger.warning("AMP: Can not find configuration key {0} in section {1}".format(k, self.amp_name)) + self.configs['enable'] = 'false' + + if not self.enable(): + logger.warning("AMP: {0} is disabled".format(self.amp_name)) + + def get(self, key): + """Generic method to get the item in the AMP configuration""" + if key in self.configs: + return self.configs[key] + else: + return None + + def enable(self): + """Return True|False if the AMP is enabled in the configuration file (enable=true|false).""" + return self.get('enable').lower().startswith('true') + + def regex(self): + """Return regular expression used to identified the current application.""" + return self.get('regex') + + def refresh(self): + """Return refresh time in seconds for the current application monitoring process.""" + return self.get('refresh') + + def should_update(self): + """Return True is the AMP should be updated: + - AMP is enable + - only update every 'refresh' seconds + """ + return True + + def set_result(self, result): + """Store the result (string) into the result key of the AMP""" + self.configs['result'] = str(result) + + def result(self): + """ Return the result of the AMP (as a string)""" + return u(self.get('result')) diff --git a/glances/amps/glances_nginx.py b/glances/amps/glances_nginx.py new file mode 100644 index 00000000..33b49309 --- /dev/null +++ b/glances/amps/glances_nginx.py @@ -0,0 +1,48 @@ +# -*- coding: utf-8 -*- +# +# This file is part of Glances. +# +# Copyright (C) 2016 Nicolargo +# +# Glances is free software; you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Glances is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program. If not, see . + +"""Nginx AMP.""" + +import requests + +from glances.logger import logger +from glances.amps.glances_amp import GlancesAmp + + +class Amp(GlancesAmp): + + """Glances' Nginx AMP.""" + + def __init__(self, args=None): + """Init the AMP.""" + super(Amp, self).__init__(args=args) + + def update(self): + """Update the AMP""" + + if self.should_update(): + logger.debug('AMPS: Update {0} using status URL {1}'.format(self.amp_name, self.get('status_url'))) + req = requests.get(self.get('status_url')) + if req.ok: + # u'Active connections: 1 \nserver accepts handled requests\n 1 1 1 \nReading: 0 Writing: 1 Waiting: 0 \n' + self.set_result(req.text) + else: + logger.debug('AMPS: Can not grab status URL {0} ({1})'.format(self.get('status_url'), req.reason)) + + return self.result() diff --git a/glances/amps_list.py b/glances/amps_list.py new file mode 100644 index 00000000..ff197870 --- /dev/null +++ b/glances/amps_list.py @@ -0,0 +1,117 @@ +# -*- coding: utf-8 -*- +# +# This file is part of Glances. +# +# Copyright (C) 2016 Nicolargo +# +# Glances is free software; you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Glances is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program. If not, see . + +"""Manage the AMPs list.""" + +import os +import re +import subprocess + +from glances.compat import listkeys, iteritems +from glances.logger import logger +from glances.globals import amps_path +from glances.processes import glances_processes + + +class AmpsList(object): + + """This class describes the optional application process monitoring list. + + The AMP list is a list of processes with a specific monitoring action. + + The list (Python list) is composed of items (Python dict). + An item is defined (dict keys): + *... + """ + + # The dict + __amps_dict = {} + + def __init__(self, args, config): + """Init the AMPs list.""" + self.args = args + self.config = config + + # Create the AMPS list + self.load_amps() + self.load_configs() + + def load_amps(self): + """Load all amps in the 'amps' folder.""" + header = "glances_" + for item in os.listdir(amps_path): + if (item.startswith(header) and + item.endswith(".py") and + item != (header + "amp.py")): + # Import the amp + amp = __import__(os.path.basename(item)[:-3]) + # Add the AMP to the dictionary + # The key is the AMP name + # for example, the file glances_xxx.py + # generate self._amps_list["xxx"] = ... + amp_name = os.path.basename(item)[len(header):-3].lower() + self.__amps_dict[amp_name] = amp.Amp(self.args) + # Log AMPs list + logger.debug("Available AMPs list: {0}".format(self.getList())) + + def load_configs(self): + """Load the AMP configuration files.""" + # For each AMPs, call the load_config method + for a in self.get(): + self.get()[a].load_config(self.config) + + def __str__(self): + return str(self.__amps_dict) + + def __repr__(self): + return self.__amps_dict + + def __getitem__(self, item): + return self.__amps_dict[item] + + def __len__(self): + return len(self.__amps_dict) + + def update(self): + """Update the command result attributed.""" + # Search application monitored processes by a regular expression + processlist = [p for p in glances_processes.getalllist()] + + # Iter upon the AMPs dict + for k, v in iteritems(self.get()): + amps_list = [p for p in processlist for c in p['cmdline'] if re.search(v.regex(), c) is not None] + if len(amps_list) > 0: + # At least one process is matching the regex + logger.debug("AMPS: {} process detected (PID={})".format(k, amps_list[0]['pid'])) + # Call the AMP update method + v.update() + + return self.__amps_dict + + def getList(self): + """Return the AMPs list.""" + return listkeys(self.__amps_dict) + + def get(self): + """Return the AMPs dict.""" + return self.__amps_dict + + def set(self, new_dict): + """Set the AMPs dict.""" + self.__amps_dict = new_dict diff --git a/glances/globals.py b/glances/globals.py index 6b952a76..680e32de 100644 --- a/glances/globals.py +++ b/glances/globals.py @@ -34,9 +34,11 @@ work_path = os.path.realpath(os.path.dirname(__file__)) appname_path = os.path.split(sys.argv[0])[0] sys_prefix = os.path.realpath(os.path.dirname(appname_path)) -# Set the plugins and export modules path +# Set the AMPs, plugins and export modules path +amps_path = os.path.realpath(os.path.join(work_path, 'amps')) plugins_path = os.path.realpath(os.path.join(work_path, 'plugins')) exports_path = os.path.realpath(os.path.join(work_path, 'exports')) sys_path = sys.path[:] sys.path.insert(1, exports_path) sys.path.insert(1, plugins_path) +sys.path.insert(1, amps_path) diff --git a/glances/outputs/glances_curses.py b/glances/outputs/glances_curses.py index f86f1175..8fe66c01 100644 --- a/glances/outputs/glances_curses.py +++ b/glances/outputs/glances_curses.py @@ -535,6 +535,8 @@ class _GlancesCurses(object): 'processcount').get_stats_display(args=self.args) stats_monitor = stats.get_plugin( 'monitor').get_stats_display(args=self.args) + stats_amps = stats.get_plugin( + 'amps').get_stats_display(args=self.args) stats_alert = stats.get_plugin( 'alert').get_stats_display(args=self.args) @@ -717,12 +719,14 @@ class _GlancesCurses(object): self.next_line = self.saved_line # Display right sidebar - # ((DOCKER)+PROCESS_COUNT+(MONITORED)+PROCESS_LIST+ALERT) + # ((DOCKER)+PROCESS_COUNT+(MONITORED)+(AMPS)+PROCESS_LIST+ALERT) self.new_column() self.new_line() self.display_plugin(stats_docker) self.new_line() self.display_plugin(stats_processcount) + self.new_line() + self.display_plugin(stats_amps) if glances_processes.process_filter is None and cs_status is None: # Do not display stats monitor list if a filter exist self.new_line() diff --git a/glances/plugins/glances_amps.py b/glances/plugins/glances_amps.py new file mode 100644 index 00000000..ff93cca3 --- /dev/null +++ b/glances/plugins/glances_amps.py @@ -0,0 +1,89 @@ +# -*- coding: utf-8 -*- +# +# This file is part of Glances. +# +# Copyright (C) 2016 Nicolargo +# +# Glances is free software; you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Glances is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program. If not, see . + +"""Monitor plugin.""" + +from glances.compat import iteritems +from glances.amps_list import AmpsList as glancesAmpsList +from glances.plugins.glances_plugin import GlancesPlugin + + +class Plugin(GlancesPlugin): + + """Glances AMPs plugin.""" + + def __init__(self, args=None, config=None): + """Init the plugin.""" + super(Plugin, self).__init__(args=args) + self.args = args + self.config = config + + # We want to display the stat in the curse interface + self.display_curse = True + + # Init the list of AMP (classe define in the glances/amps_list.py script) + self.glances_amps = glancesAmpsList(self.args, self.config) + + # Init stats + self.reset() + + def reset(self): + """Reset/init the stats.""" + self.stats = [] + + @GlancesPlugin._log_result_decorator + def update(self): + """Update the AMP list.""" + # Reset stats + self.reset() + + if self.input_method == 'local': + # TODO + for k, v in iteritems(self.glances_amps.update()): + self.stats.append({k: v.result()}) + else: + pass + + return self.stats + + def msg_curse(self, args=None): + """Return the dict to display in the curse interface.""" + # Init the return message + # Only process if stats exist and display plugin enable... + ret = [] + + if not self.stats or args.disable_process: + return ret + + # Build the string message + for m in self.stats: + for k, v in iteritems(m): + msg = '{0:<16} '.format(k) + ret.append(self.curse_add_line(msg)) + msg = '{0}'.format(v.replace('\n', '')) + ret.append(self.curse_add_line(msg, splittable=True)) + ret.append(self.curse_new_line()) + + # Delete the last empty line + try: + ret.pop() + except IndexError: + pass + + return ret diff --git a/glances/plugins/glances_plugin.py b/glances/plugins/glances_plugin.py index bda8c8a5..15f6462a 100644 --- a/glances/plugins/glances_plugin.py +++ b/glances/plugins/glances_plugin.py @@ -403,8 +403,6 @@ class GlancesPlugin(object): except KeyError: return 'DEFAULT' - logger.debug("{0} => ret = {1}".format(stat_name, ret)) - # Manage log log_str = "" if self.__get_limit_log(stat_name=stat_name, default_action=log): diff --git a/glances/stats.py b/glances/stats.py index e2bdd47b..bf83fec9 100644 --- a/glances/stats.py +++ b/glances/stats.py @@ -33,16 +33,16 @@ class GlancesStats(object): """This class stores, updates and gives stats.""" def __init__(self, config=None, args=None): - # Set the argument instance - self.args = args - # Set the config instance self.config = config - # Load plugins and export modules - self.load_plugins_and_exports(self.args) + # Set the argument instance + self.args = args - # Load the limits + # Load plugins and exports modules + self.load_modules(self.args) + + # Load the limits (for plugins) self.load_limits(config) def __getattr__(self, item): @@ -67,8 +67,9 @@ class GlancesStats(object): # Default behavior raise AttributeError(item) - def load_plugins_and_exports(self, args): - """Wrapper to load both plugins and export modules.""" + def load_modules(self, args): + """Wrapper to load: plugins and export modules.""" + # Init the plugins dict self._plugins = collections.defaultdict(dict) # Load the plugins @@ -96,7 +97,7 @@ class GlancesStats(object): # for example, the file glances_xxx.py # generate self._plugins_list["xxx"] = ... plugin_name = os.path.basename(item)[len(header):-3].lower() - if plugin_name == 'help': + if plugin_name in ('help', 'amps'): self._plugins[plugin_name] = plugin.Plugin(args=args, config=self.config) else: self._plugins[plugin_name] = plugin.Plugin(args=args) @@ -136,13 +137,12 @@ class GlancesStats(object): def getExportList(self): """Return the exports modules list.""" - return [p for p in self._exports] + return [e for e in self._exports] def load_limits(self, config=None): """Load the stats limits.""" - # For each plugins, call the init_limits method + # For each plugins, call the load_limits method for p in self._plugins: - # logger.debug("Load limits for %s" % p) self._plugins[p].load_limits(config) def update(self): diff --git a/glances/stats_client.py b/glances/stats_client.py index dc9c2e9b..ad6a6370 100644 --- a/glances/stats_client.py +++ b/glances/stats_client.py @@ -40,8 +40,8 @@ class GlancesStatsClient(GlancesStats): # Init the arguments self.args = args - # Load plugins and exports - self.load_plugins_and_exports(self.args) + # Load AMPs, plugins and exports modules + self.load_modules(self.args) def set_plugins(self, input_plugins): """Set the plugin list according to the Glances server.""" diff --git a/glances/stats_client_snmp.py b/glances/stats_client_snmp.py index 22747aef..1a96eda5 100644 --- a/glances/stats_client_snmp.py +++ b/glances/stats_client_snmp.py @@ -51,8 +51,8 @@ class GlancesStatsClientSNMP(GlancesStats): # OS name is used because OID is differents between system self.os_name = None - # Load plugins and export modules - self.load_plugins_and_exports(self.args) + # Load AMPs, plugins and exports modules + self.load_modules(self.args) def check_snmp(self): """Chek if SNMP is available on the server."""