diff --git a/NEWS b/NEWS index da500edd..8cd410f4 100644 --- a/NEWS +++ b/NEWS @@ -5,6 +5,8 @@ Glances Version 2.x Version 2.X =========== + * Add a new InfluxDB export module (--export-influxdb) (issue #455) + * Refactor export module (CSV export option is now --export-csv). It is now possible to export stats from the Glances client (issue #463) * The Web inteface is now based on BootStarp / RWD grid (issue #417, #366 and #461) Thanks to Nicolas Hart @nclsHart * Add the RAID plugins (issue #447) diff --git a/conf/glances-test.conf b/conf/glances-test.conf index a88cb7ea..ac75d884 100644 --- a/conf/glances-test.conf +++ b/conf/glances-test.conf @@ -143,3 +143,10 @@ server_3_alias=Another PC on my network server_3_port=61209 server_4_name=pasbon server_4_port=61237 + +[influxdb] +host=localhost +port=8086 +user=root +password=root +db=glances diff --git a/conf/glances.conf b/conf/glances.conf index 754c1616..040af380 100644 --- a/conf/glances.conf +++ b/conf/glances.conf @@ -143,3 +143,10 @@ mem_critical=90 #server_3_port=61209 #server_4_name=pasbon #server_4_port=61237 + +[influxdb] +host=localhost +port=8086 +user=root +password=root +db=glances diff --git a/glances/core/glances_client.py b/glances/core/glances_client.py index c09f0cb0..a903cf4d 100644 --- a/glances/core/glances_client.py +++ b/glances/core/glances_client.py @@ -136,7 +136,7 @@ class GlancesClient(object): if self.get_mode() == 'glances' and version.split('.')[0] == client_version.split('.')[0]: # Init stats - self.stats = GlancesStatsClient() + self.stats = GlancesStatsClient(config=self.config, args=self.args) self.stats.set_plugins(json.loads(self.client.getAllPlugins())) logger.debug( "Client version: %s / Server version: %s" % (version, client_version)) @@ -153,7 +153,7 @@ class GlancesClient(object): from glances.core.glances_stats import GlancesStatsClientSNMP # Init stats - self.stats = GlancesStatsClientSNMP(args=self.args) + self.stats = GlancesStatsClientSNMP(config=self.config, args=self.args) if not self.stats.check_snmp(): self.log_and_exit("Connection to SNMP server failed") @@ -234,6 +234,9 @@ class GlancesClient(object): cs_status=cs_status, return_to_browser=self.return_to_browser) + # Export stats using export modules + self.stats.export(self.stats) + return self.get_mode() def end(self): diff --git a/glances/core/glances_main.py b/glances/core/glances_main.py index e66b7c4d..759ca4d1 100644 --- a/glances/core/glances_main.py +++ b/glances/core/glances_main.py @@ -94,6 +94,8 @@ class GlancesMain(object): # Export modules feature parser.add_argument('--export-csv', default=None, dest='export_csv', help=_('export stats to a CSV file')) + parser.add_argument('--export-influxdb', action='store_true', default=False, + dest='export_influxdb', help=_('export stats to an InfluxDB server')) # Client/Server option parser.add_argument('-c', '--client', dest='client', help=_('connect to a Glances server by IPv4/IPv6 address or hostname')) diff --git a/glances/core/glances_stats.py b/glances/core/glances_stats.py index cad835d5..64da8919 100644 --- a/glances/core/glances_stats.py +++ b/glances/core/glances_stats.py @@ -118,14 +118,15 @@ class GlancesStats(object): item.endswith(".py") and item != (header + "export.py") and item != (header + "history.py") and - args_var['export_' + export_name] is not None): + args_var['export_' + export_name] is not None and + args_var['export_' + export_name] is not False): # Import the export module export_module = __import__(os.path.basename(item)[:-3]) # Add the export to the dictionary # The key is the module name # for example, the file glances_xxx.py # generate self._exports_list["xxx"] = ... - self._exports[export_name] = export_module.Export(args=args) + self._exports[export_name] = export_module.Export(args=args, config=self.config) # Log plugins list logger.debug("Available exports modules list: {0}".format(self.getAllExports())) return True @@ -251,11 +252,22 @@ class GlancesStatsClient(GlancesStats): """This class stores, updates and gives stats for the client.""" - def __init__(self): + def __init__(self, config=None, args=None): """Init the GlancesStatsClient class.""" # Init the plugin list dict self._plugins = collections.defaultdict(dict) + # Init the configuration + self.config = config + + # Init the arguments + self.args = args + + # Init the export modules list dict + self._exports = collections.defaultdict(dict) + # Load the plugins + self.load_exports(args=args) + def set_plugins(self, input_plugins): """Set the plugin list according to the Glances server.""" header = "glances_" @@ -292,6 +304,11 @@ class GlancesStatsClientSNMP(GlancesStats): # Load plugins self.load_plugins(args=self.args) + # Init the export modules list dict + self._exports = collections.defaultdict(dict) + # Load the plugins + self.load_exports(args=args) + def check_snmp(self): """Chek if SNMP is available on the server.""" # Import the SNMP client class diff --git a/glances/exports/glances_csv.py b/glances/exports/glances_csv.py index 77a4a0c9..d3cb452d 100644 --- a/glances/exports/glances_csv.py +++ b/glances/exports/glances_csv.py @@ -28,18 +28,14 @@ from glances.core.glances_globals import is_py3 from glances.core.glances_logging import logger from glances.exports.glances_export import GlancesExport -# List of stats enabled in the CSV output -# !!! TODO: should be configurable from the conf file -csv_stats_list = ['cpu', 'load', 'mem', 'memswap', 'network', 'diskio', 'fs'] - class Export(GlancesExport): """This class manages the CSV export module.""" - def __init__(self, args=None): + def __init__(self, config=None, args=None): """Init the CSV export IF.""" - GlancesExport.__init__(self, args=args) + GlancesExport.__init__(self, config=config, args=args) # CSV file name self.csv_filename = args.export_csv @@ -61,6 +57,7 @@ class Export(GlancesExport): def exit(self): """Close the CSV file.""" + logger.debug("Finalise export interface %s" % self.export_name) self.csv_file.close() def update(self, stats): @@ -75,7 +72,7 @@ class Export(GlancesExport): # Loop over available plugin i = 0 for plugin in plugins: - if plugin in csv_stats_list: + if plugin in self.plugins_to_export(): if type(all_stats[i]) is list: for item in all_stats[i]: # First line: header diff --git a/glances/exports/glances_export.py b/glances/exports/glances_export.py index 41f43fbd..5e52f237 100644 --- a/glances/exports/glances_export.py +++ b/glances/exports/glances_export.py @@ -34,11 +34,20 @@ class GlancesExport(object): """Main class for Glances' export IF.""" - def __init__(self, args=None): + def __init__(self, config=None, args=None): """Init the export class.""" # Export name (= module name without glances_) self.export_name = self.__class__.__module__[len('glances_'):] logger.debug("Init export interface %s" % self.export_name) - # Init the args + # Init the config & args + self.config = config self.args = args + + def exit(self): + """Close the export module.""" + logger.debug("Finalise export interface %s" % self.export_name) + + def plugins_to_export(self): + """Return the list of plugins to export""" + return ['cpu', 'load', 'mem', 'memswap', 'network', 'diskio', 'fs', 'processcount'] diff --git a/glances/exports/glances_influxdb.py b/glances/exports/glances_influxdb.py new file mode 100644 index 00000000..34c90dff --- /dev/null +++ b/glances/exports/glances_influxdb.py @@ -0,0 +1,135 @@ +# -*- coding: utf-8 -*- +# +# This file is part of Glances. +# +# Copyright (C) 2014 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 . + +"""InfluxDB interface class.""" + +# Import sys libs +from influxdb import InfluxDBClient, client +import sys + +# Import Glances lib +from glances.core.glances_globals import is_py3 +from glances.core.glances_logging import logger +from ConfigParser import NoSectionError, NoOptionError +from glances.exports.glances_export import GlancesExport + + +class Export(GlancesExport): + + """This class manages the InfluxDB export module.""" + + def __init__(self, config=None, args=None): + """Init the CSV export IF.""" + GlancesExport.__init__(self, config=config, args=args) + + # Load the InfluxDB configuration file + self.influxdb_host = None + self.influxdb_port = None + self.influxdb_user = None + self.influxdb_password = None + self.influxdb_db = None + self.export_enable = self.load_conf() + if not self.export_enable: + sys.exit(2) + + # Init the InfluxDB client + self.client = self.init() + + def load_conf(self, section="influxdb"): + """Load the InfluxDb configuration in the Glances configuration file""" + if self.config is None: + return False + try: + self.influxdb_host = self.config.get_raw_option(section, "host") + self.influxdb_port = self.config.get_raw_option(section, "port") + self.influxdb_user = self.config.get_raw_option(section, "user") + self.influxdb_password = self.config.get_raw_option(section, "password") + self.influxdb_db = self.config.get_raw_option(section, "db") + except NoSectionError: + logger.critical("No InfluxDB configuration found") + return False + except NoOptionError as e: + logger.critical("Error in the InfluxDB configuration (%s)" % e) + return False + else: + logger.debug("Load InfluxDB from the Glances configuration file") + return True + + def init(self): + """Init the connection to the InfluxDB server""" + if not self.export_enable: + return None + db = InfluxDBClient(self.influxdb_host, + self.influxdb_port, + self.influxdb_user, + self.influxdb_password, + self.influxdb_db) + try: + get_all_db = db.get_database_list()[0].values() + except client.InfluxDBClientError as e: + logger.critical("Can not connect to InfluxDB database '%s' (%s)" % (self.influxdb_db, e)) + sys.exit(2) + + if self.influxdb_db in get_all_db: + logger.info( + "Stats will be exported to InfluxDB server: {0}".format(db._baseurl)) + else: + logger.critical("InfluxDB database '%s' did not exist. Please create it" % self.influxdb_db) + sys.exit(2) + return db + + def update(self, stats): + """Update stats to the InfluxDB server.""" + if not self.export_enable: + return False + + # Get the stats + all_stats = stats.getAll() + plugins = stats.getAllPlugins() + + # Loop over available plugin + i = 0 + for plugin in plugins: + if plugin in self.plugins_to_export(): + if type(all_stats[i]) is list: + for item in all_stats[i]: + export_names = map( + lambda x: item[item['key']] + '_' + x, item.keys()) + export_values = item.values() + self.write_to_influxdb(plugin, export_names, export_values) + elif type(all_stats[i]) is dict: + export_names = all_stats[i].keys() + export_values = all_stats[i].values() + self.write_to_influxdb(plugin, export_names, export_values) + i += 1 + + return True + + def write_to_influxdb(self, name, columns, points): + """Write the points to the InfluxDB server""" + data = [ + { + "name": name, + "columns": columns, + "points": [points] + }] + try: + self.client.write_points(data) + except Exception as e: + logger.critical("Can not export stats to InfluxDB (%s)" % e)