mirror of
https://github.com/nicolargo/glances.git
synced 2026-09-22 18:15:52 -04:00
469 lines
19 KiB
Python
469 lines
19 KiB
Python
#
|
|
# This file is part of Glances.
|
|
#
|
|
# SPDX-FileCopyrightText: 2022 Nicolas Hennion <nicolas@nicolargo.com>
|
|
#
|
|
# SPDX-License-Identifier: LGPL-3.0-only
|
|
#
|
|
|
|
"""The stats manager."""
|
|
|
|
import ast
|
|
import collections
|
|
import os
|
|
import sys
|
|
import threading
|
|
import traceback
|
|
from importlib import import_module
|
|
from pathlib import Path
|
|
|
|
from glances.globals import exports_path, plugins_path, sys_path, weak_lru_cache
|
|
from glances.logger import logger
|
|
from glances.timer import Counter
|
|
|
|
|
|
class GlancesStats:
|
|
"""This class stores, updates and gives stats."""
|
|
|
|
# Script header constant
|
|
header = "glances_"
|
|
|
|
def __init__(self, config=None, args=None):
|
|
# Set the config instance
|
|
self.config = config
|
|
|
|
# Set the argument instance
|
|
self.args = args
|
|
|
|
# Load plugins and exports modules
|
|
self.first_export = True
|
|
self.load_modules(self.args)
|
|
|
|
def __getattr__(self, item):
|
|
"""Overwrite the getattr method in case of attribute is not found.
|
|
|
|
The goal is to dynamically generate the following methods:
|
|
- getPlugname(): return Plugname stat in JSON format
|
|
- getViewsPlugname(): return views of the Plugname stat in JSON format
|
|
"""
|
|
# Check if the attribute starts with 'get'
|
|
if item.startswith('getViews'):
|
|
# Get the plugin name
|
|
plugname = item[len('getViews') :].lower()
|
|
# Get the plugin instance
|
|
plugin = self._plugins[plugname]
|
|
if hasattr(plugin, 'get_json_views'):
|
|
# The method get_json_views exist, return it
|
|
return getattr(plugin, 'get_json_views')
|
|
# The method get_views is not found for the plugin
|
|
raise AttributeError(item)
|
|
if item.startswith('get'):
|
|
# Get the plugin name
|
|
plugname = item[len('get') :].lower()
|
|
# Get the plugin instance
|
|
plugin = self._plugins[plugname]
|
|
if hasattr(plugin, 'get_json'):
|
|
# The method get_json exist, return it
|
|
return getattr(plugin, 'get_json')
|
|
# The method get_stats is not found for the plugin
|
|
raise AttributeError(item)
|
|
# Default behavior
|
|
raise AttributeError(item)
|
|
|
|
def load_modules(self, args):
|
|
"""Wrapper to load: plugins and export modules."""
|
|
|
|
# Init the plugins dict
|
|
# Active plugins dictionary
|
|
self._plugins = collections.defaultdict(dict)
|
|
# Load the plugins
|
|
self.load_plugins(args=args)
|
|
|
|
# Load addititional plugins
|
|
self.load_additional_plugins(args=args, config=self.config)
|
|
|
|
# Init the export modules dict
|
|
# Active exporters dictionary
|
|
self._exports = collections.defaultdict(dict)
|
|
# All available exporters dictionary
|
|
self._exports_all = collections.defaultdict(dict)
|
|
# Load the export modules
|
|
self.load_exports(args=args)
|
|
|
|
# Restoring system path
|
|
sys.path = sys_path
|
|
|
|
def _load_plugin(self, plugin_path, args=None, config=None):
|
|
"""Load the plugin, init it and add to the _plugin dict."""
|
|
# Load the plugin class
|
|
try:
|
|
# Import the plugin
|
|
plugin = import_module('glances.plugins.' + plugin_path)
|
|
# Init and add the plugin to the dictionary
|
|
if hasattr(plugin, 'PluginModel'):
|
|
# Old fashion way to load the plugin (before Glances 5.0)
|
|
# Should be removed in Glances 5.0 - see #3170
|
|
self._plugins[plugin_path] = getattr(plugin, 'PluginModel')(args=args, config=config)
|
|
logger.warning(
|
|
f'The {plugin_path} plugin class name is "PluginModel" and it is deprecated, \
|
|
please rename it to "{plugin_path.capitalize()}Plugin"'
|
|
)
|
|
elif hasattr(plugin, plugin_path.capitalize() + 'Plugin'):
|
|
# New fashion way to load the plugin (after Glances 5.0)
|
|
self._plugins[plugin_path] = getattr(plugin, plugin_path.capitalize() + 'Plugin')(
|
|
args=args, config=config
|
|
)
|
|
except Exception as e:
|
|
# If a plugin can not be loaded, display a critical message
|
|
# on the console but do not crash
|
|
logger.critical(f"Error while initializing the {plugin_path} plugin ({e})")
|
|
logger.error(traceback.format_exc())
|
|
# An error occurred, disable the plugin
|
|
if args is not None:
|
|
setattr(args, 'disable_' + plugin_path, False)
|
|
else:
|
|
# Manage the default status of the plugin (enable or disable)
|
|
if args is not None:
|
|
# If the all keys are set in the disable_plugin option then look in the enable_plugin option
|
|
if getattr(args, 'disable_all', False):
|
|
logger.debug('%s => %s', plugin_path, getattr(args, 'enable_' + plugin_path, False))
|
|
setattr(args, 'disable_' + plugin_path, not getattr(args, 'enable_' + plugin_path, False))
|
|
else:
|
|
setattr(args, 'disable_' + plugin_path, getattr(args, 'disable_' + plugin_path, False))
|
|
|
|
def load_plugins(self, args=None):
|
|
"""Load all plugins in the 'plugins' folder."""
|
|
start_duration = Counter()
|
|
|
|
for item in os.listdir(plugins_path):
|
|
if os.path.isdir(os.path.join(plugins_path, item)) and not item.startswith('__') and item != 'plugin':
|
|
# Load the plugin
|
|
start_duration.reset()
|
|
self._load_plugin(os.path.basename(item), args=args, config=self.config)
|
|
logger.debug(f"Plugin {item} started in {start_duration.get()} seconds")
|
|
|
|
# Log plugins list
|
|
logger.debug(f"Active plugins list: {self.getPluginsList()}")
|
|
|
|
def _contains_plugin_model(self, plugin_file):
|
|
"""Return True if the module defines a PluginModel class."""
|
|
try:
|
|
# Read and parse the Python source file into an AST
|
|
# so plugin detection is based on actual class definitions
|
|
# rather than fragile string matching.
|
|
content = plugin_file.read_text(encoding="utf-8")
|
|
tree = ast.parse(content)
|
|
|
|
# Walk through all nodes in the AST and look for
|
|
# a class named PluginModel.
|
|
for node in ast.walk(tree):
|
|
if isinstance(node, ast.ClassDef):
|
|
if node.name == "PluginModel":
|
|
return True
|
|
|
|
except (SyntaxError, OSError):
|
|
# Invalid Python file or unreadable file
|
|
return False
|
|
|
|
return False
|
|
|
|
def _get_addl_plugins(self, plugin_path):
|
|
"""Get list of additional plugins."""
|
|
plugin_list = []
|
|
|
|
# Iterate through each directory in the configured plugin path.
|
|
for plugin in os.listdir(plugin_path):
|
|
path = Path(plugin_path) / plugin
|
|
|
|
# Ignore non-directories and internal Python directories.
|
|
if not path.is_dir() or path.name.startswith('__'):
|
|
continue
|
|
|
|
# Search Python files within the plugin directory.
|
|
for fil in path.glob('*.py'):
|
|
# Skip invalid filesystem entries.
|
|
if not fil.is_file():
|
|
continue
|
|
|
|
# Register the plugin if a PluginModel class exists.
|
|
if self._contains_plugin_model(fil):
|
|
plugin_list.append(plugin)
|
|
break
|
|
|
|
return plugin_list
|
|
|
|
def load_additional_plugins(self, args=None, config=None):
|
|
"""Load additional plugins if defined"""
|
|
|
|
path = None
|
|
# Skip section check as implied by has_option
|
|
if config and config.parser.has_option('global', 'plugin_dir'):
|
|
path = config.parser['global']['plugin_dir']
|
|
|
|
# Command-line argument takes precedence over config value.
|
|
if args and 'plugin_dir' in args and args.plugin_dir:
|
|
path = args.plugin_dir
|
|
|
|
# No additional plugin directory configured.
|
|
if not path:
|
|
return
|
|
|
|
# Store original sys.path so it can be restored after loading.
|
|
_sys_path = sys.path
|
|
|
|
# Used to measure plugin startup duration.
|
|
start_duration = Counter()
|
|
|
|
# Ensure plugins can be imported from the configured directory.
|
|
sys.path.insert(0, path)
|
|
|
|
for plugin in self._get_addl_plugins(path):
|
|
# Prevent duplicate imports for plugins already loaded.
|
|
if plugin in sys.modules:
|
|
logger.warn(f"Plugin {plugin} already in sys.modules, skipping (workaround: rename plugin)")
|
|
continue
|
|
|
|
start_duration.reset()
|
|
try:
|
|
# Dynamically import the plugin model module.
|
|
_mod_loaded = import_module(plugin + '.model')
|
|
|
|
# Create and register the plugin instance.
|
|
self._plugins[plugin] = _mod_loaded.PluginModel(args=args, config=config)
|
|
logger.debug(f"Plugin {plugin} started in {start_duration.get()} seconds")
|
|
except Exception as e:
|
|
# If a plugin can not be loaded, display a critical message
|
|
# on the console but do not crash
|
|
logger.critical(f"Error while initializing the {plugin} plugin ({e})")
|
|
logger.error(traceback.format_exc())
|
|
# An error occurred, disable the plugin
|
|
if args:
|
|
setattr(args, 'disable_' + plugin, False)
|
|
|
|
# Restore original Python import path.
|
|
sys.path = _sys_path
|
|
# Log active additional plugins list
|
|
logger.debug(f"Active additional plugins list: {self.getPluginsList()}")
|
|
|
|
def load_exports(self, args=None):
|
|
"""Load all exporters in the 'exports' folder."""
|
|
start_duration = Counter()
|
|
|
|
if args is None:
|
|
return False
|
|
|
|
for item in os.listdir(exports_path):
|
|
if os.path.isdir(os.path.join(exports_path, item)) and not item.startswith('__'):
|
|
# Load the exporter
|
|
start_duration.reset()
|
|
if item.startswith('glances_'):
|
|
# Avoid circular loop when Glances exporter uses lib with same name
|
|
# Example: influxdb should be named to glances_influxdb
|
|
exporter_name = os.path.basename(item).split('glances_')[1]
|
|
else:
|
|
exporter_name = os.path.basename(item)
|
|
# Set the disable_<name> to False by default
|
|
setattr(self.args, 'export_' + exporter_name, getattr(self.args, 'export_' + exporter_name, False))
|
|
# We should import the module
|
|
if getattr(self.args, 'export_' + exporter_name, False):
|
|
# Import the export module
|
|
export_module = import_module(item)
|
|
# Add the exporter instance to the active exporters dictionary
|
|
self._exports[exporter_name] = export_module.Export(args=args, config=self.config)
|
|
# Add the exporter instance to the available exporters dictionary
|
|
self._exports_all[exporter_name] = self._exports[exporter_name]
|
|
else:
|
|
# Add the exporter name to the available exporters dictionary
|
|
self._exports_all[exporter_name] = exporter_name
|
|
logger.debug(f"Exporter {exporter_name} started in {start_duration.get()} seconds")
|
|
|
|
# Log plugins list
|
|
logger.debug(f"Active exports modules list: {self.getExportsList()}")
|
|
return True
|
|
|
|
def getPluginsList(self, enable=True):
|
|
"""Return the plugins list.
|
|
|
|
if enable is True, only return the active plugins (default)
|
|
if enable is False, return all the plugins
|
|
|
|
Return: list of plugin name
|
|
"""
|
|
if enable:
|
|
return [p for p in self._plugins if self._plugins[p].is_enabled()]
|
|
return list(self._plugins)
|
|
|
|
def getExportsList(self, enable=True):
|
|
"""Return the exports list.
|
|
|
|
if enable is True, only return the active exporters (default)
|
|
if enable is False, return all the exporters
|
|
|
|
:return: list of export module names
|
|
"""
|
|
if enable:
|
|
return list(self._exports)
|
|
return list(self._exports_all)
|
|
|
|
def load_limits(self, config=None):
|
|
"""Load the stats limits (except the one in the exclude list)."""
|
|
# For each plugins (enable or not), call the load_limits method
|
|
for p in self.getPluginsList(enable=False):
|
|
self._plugins[p].load_limits(config)
|
|
|
|
# It's a weak cache to avoid updating the same plugin too often
|
|
# Note: the function always return None
|
|
@weak_lru_cache(ttl=1)
|
|
def update_plugin(self, p):
|
|
"""Update stats, history and views for the given plugin name p"""
|
|
self._plugins[p].update()
|
|
self._plugins[p].update_views()
|
|
self._plugins[p].update_stats_history()
|
|
|
|
def update(self, plugins_list_to_update=None):
|
|
"""Wrapper method to update stats.
|
|
If plugins_list_to_update is provided (list), only update the given plugins.
|
|
"""
|
|
if plugins_list_to_update is None:
|
|
plugins_list_to_update = self.getPluginsList(enable=True)
|
|
|
|
# Start update of all enable plugins
|
|
for p in plugins_list_to_update:
|
|
self.update_plugin(p)
|
|
|
|
def export(self, input_stats=None):
|
|
"""Export all the stats.
|
|
|
|
Each export module is ran in a dedicated thread.
|
|
"""
|
|
if self.first_export:
|
|
# Init fields description
|
|
# Why ? Because some exports modules need to know what is exported (name, type...)
|
|
for e in self.getExportsList():
|
|
logger.debug(f"Init exported stats using the {e} module")
|
|
# @TODO: is it a good idea to thread this call ?
|
|
self._exports[e].init_fields(input_stats)
|
|
# In this first loop, data are not exported because some information are missing (rate for example)
|
|
logger.debug("Do not export stats during the first iteration because some information are missing")
|
|
self.first_export = False
|
|
return False
|
|
|
|
input_stats = input_stats or {}
|
|
|
|
for e in self.getExportsList():
|
|
logger.debug(f"Export stats using the {e} module")
|
|
thread = threading.Thread(target=self._exports[e].update, args=(input_stats,))
|
|
thread.start()
|
|
|
|
return True
|
|
|
|
def getAll(self):
|
|
"""Return all the stats (list).
|
|
This method is called byt the XML/RPC API.
|
|
It should return all the plugins (enable or not) because filtering can be done by the client.
|
|
"""
|
|
return [self._plugins[p].get_raw() for p in self.getPluginsList(enable=False)]
|
|
|
|
def getAllAsDict(self, plugin_list=None):
|
|
"""Return all the stats (as dict).
|
|
This method is called by the RESTFul API.
|
|
"""
|
|
if plugin_list is None:
|
|
# All enabled plugins should be exported
|
|
plugin_list = self.getPluginsList()
|
|
return {p: self._plugins[p].get_raw() for p in plugin_list}
|
|
|
|
def getAllFieldsDescription(self):
|
|
"""Return all fields description (as list)."""
|
|
return [self._plugins[p].fields_description for p in self.getPluginsList(enable=False)]
|
|
|
|
def getAllFieldsDescriptionAsDict(self, plugin_list=None):
|
|
"""Return all fields description (as dict)."""
|
|
if plugin_list is None:
|
|
# All enabled plugins should be exported
|
|
plugin_list = self.getPluginsList()
|
|
return {p: self._plugins[p].fields_description for p in plugin_list}
|
|
|
|
def getAllExports(self, plugin_list=None):
|
|
"""Return all the stats to be exported as a list.
|
|
|
|
Default behavior is to export all the stat
|
|
if plugin_list is provided (list), only export stats of given plugins
|
|
"""
|
|
if plugin_list is None:
|
|
# All enabled plugins should be exported
|
|
plugin_list = self.getPluginsList()
|
|
return [self._plugins[p].get_export() for p in plugin_list]
|
|
|
|
def getAllExportsAsDict(self, plugin_list=None):
|
|
"""Return all the stats to be exported as a dict.
|
|
|
|
Default behavior is to export all the stat
|
|
if plugin_list is provided (list), only export stats of given plugins
|
|
"""
|
|
if plugin_list is None:
|
|
# All enabled plugins should be exported
|
|
plugin_list = self.getPluginsList()
|
|
return {p: self._plugins[p].get_export() for p in plugin_list}
|
|
|
|
def getAllLimits(self, plugin_list=None):
|
|
"""Return the plugins limits list.
|
|
|
|
Default behavior is to export all the limits
|
|
if plugin_list is provided, only export limits of given plugin (list)
|
|
"""
|
|
if plugin_list is None:
|
|
# All enabled plugins should be exported
|
|
plugin_list = self.getPluginsList()
|
|
return [self._plugins[p].limits for p in plugin_list]
|
|
|
|
def getAllLimitsAsDict(self, plugin_list=None):
|
|
"""Return all the stats limits (dict).
|
|
|
|
Default behavior is to export all the limits
|
|
if plugin_list is provided, only export limits of given plugin (list)
|
|
"""
|
|
if plugin_list is None:
|
|
# All enabled plugins should be exported
|
|
plugin_list = self.getPluginsList()
|
|
return {p: self._plugins[p].limits for p in plugin_list}
|
|
|
|
def getAllViews(self, plugin_list=None):
|
|
"""Return the plugins views.
|
|
This method is called byt the XML/RPC API.
|
|
It should return all the plugins views (enable or not) because filtering can be done by the client.
|
|
"""
|
|
if plugin_list is None:
|
|
plugin_list = self.getPluginsList(enable=False)
|
|
return [self._plugins[p].get_views() for p in plugin_list]
|
|
|
|
def getAllViewsAsDict(self, plugin_list=None):
|
|
"""Return all the stats views (dict).
|
|
This method is called by the RESTFul API.
|
|
"""
|
|
if plugin_list is None:
|
|
# All enabled plugins should be exported
|
|
plugin_list = self.getPluginsList()
|
|
return {p: self._plugins[p].get_views() for p in plugin_list}
|
|
|
|
def get_plugin(self, plugin_name):
|
|
"""Return the plugin stats."""
|
|
if plugin_name in self._plugins:
|
|
return self._plugins[plugin_name]
|
|
return None
|
|
|
|
def get_plugin_view(self, plugin_name):
|
|
"""Return the plugin views."""
|
|
if plugin_name in self._plugins:
|
|
return self._plugins[plugin_name].get_views()
|
|
return None
|
|
|
|
def end(self):
|
|
"""End of the Glances stats."""
|
|
# Close export modules
|
|
for e in self._exports:
|
|
self._exports[e].exit()
|
|
# Close plugins
|
|
for p in self._plugins:
|
|
self._plugins[p].exit()
|