diff options
-rw-r--r-- | NEWS | 2 | ||||
-rw-r--r-- | docs/_static/sparkline.png | bin | 0 -> 41952 bytes | |||
-rw-r--r-- | docs/aoa/load.rst | 8 | ||||
-rw-r--r-- | docs/aoa/quicklook.rst | 7 | ||||
-rw-r--r-- | docs/man/glances.1 | 8 | ||||
-rw-r--r-- | glances/main.py | 2 | ||||
-rw-r--r-- | glances/outdated.py | 3 | ||||
-rw-r--r-- | glances/outputs/glances_bars.py | 4 | ||||
-rw-r--r-- | glances/outputs/glances_curses.py | 10 | ||||
-rw-r--r-- | glances/outputs/glances_sparklines.py | 91 | ||||
-rw-r--r-- | glances/plugins/glances_load.py | 18 | ||||
-rw-r--r-- | glances/plugins/glances_plugin.py | 8 | ||||
-rw-r--r-- | glances/plugins/glances_quicklook.py | 66 | ||||
-rw-r--r-- | requirements.txt | 2 |
14 files changed, 194 insertions, 35 deletions
@@ -7,6 +7,8 @@ Version 3.1.1 Enhancements and new features: + * Please add some sparklines! #1446 + * Add Load Average (similar to Linux) on Windows #344 * Add authprovider for cassandra export (thanks to @EmilienMottet) #1395 * Curses's browser server list sorting added (thanks to @limfreee) #1396 * ElasticSearch: add date to index, unbreak object push (thanks to @genevera) # 1438 diff --git a/docs/_static/sparkline.png b/docs/_static/sparkline.png Binary files differnew file mode 100644 index 00000000..6a44eded --- /dev/null +++ b/docs/_static/sparkline.png diff --git a/docs/aoa/load.rst b/docs/aoa/load.rst index 7765ca63..30eb2e38 100644 --- a/docs/aoa/load.rst +++ b/docs/aoa/load.rst @@ -3,7 +3,7 @@ Load ==== -*Availability: Unix* +*Availability: Unix and Windows with a PsUtil version >= 5.6.2* .. image:: ../_static/load.png @@ -14,8 +14,9 @@ on GNU/Linux operating system: waiting in the run-queue plus the number currently executing over 1, 5, and 15 minutes time periods." -Be aware that Load on Linux and BSD are different things, high -`load on BSD`_ does not means high CPU load. +Be aware that Load on Linux, BSD and Windows are different things, high +`load on BSD`_ does not means high CPU load. The Windows load is emulated +by the PsUtil lib (see `load on Windows`_) Glances gets the number of CPU core to adapt the alerts. Alerts on load average are only set on 15 minutes time period. @@ -38,3 +39,4 @@ Load avg Status .. _load average: http://nosheep.net/story/defining-unix-load-average/ .. _load on BSD: http://undeadly.org/cgi?action=article&sid=20090715034920 +.. _load on Windows: https://psutil.readthedocs.io/en/latest/#psutil.getloadavg diff --git a/docs/aoa/quicklook.rst b/docs/aoa/quicklook.rst index d4e6c74f..3b2d0fa0 100644 --- a/docs/aoa/quicklook.rst +++ b/docs/aoa/quicklook.rst @@ -12,6 +12,13 @@ If the per CPU mode is on (by clicking the ``1`` key): .. image:: ../_static/quicklook-percpu.png +In the Curses/terminal interface, it is also possible to switch from bar to +sparkline using 'S' hot key or --sparkline command line option. Please be +aware that sparklines use the Glances history and will not be available +if the history is disabled from the command line. + +.. image:: ../_static/sparkline.png + .. note:: Limit values can be overwritten in the configuration file under the ``[quicklook]`` section. diff --git a/docs/man/glances.1 b/docs/man/glances.1 index 2d3b7538..1ffed86b 100644 --- a/docs/man/glances.1 +++ b/docs/man/glances.1 @@ -303,6 +303,11 @@ display FS free space instead of used .UNINDENT .INDENT 0.0 .TP +.B \-\-sparkline +display sparlines instead of bar in the curses interface +.UNINDENT +.INDENT 0.0 +.TP .B \-\-theme\-white optimize display colors for white background .UNINDENT @@ -422,6 +427,9 @@ Show/hide RAID plugin .B \fBs\fP Show/hide sensors stats .TP +.B \fBS\fP +Switch from bars to sparklines +.TP .B \fBt\fP Sort process by CPU times (TIME+) .TP diff --git a/glances/main.py b/glances/main.py index 8a7a0850..ad55b82a 100644 --- a/glances/main.py +++ b/glances/main.py @@ -240,6 +240,8 @@ Examples of use: dest='fahrenheit', help='display temperature in Fahrenheit (default is Celsius)') parser.add_argument('--fs-free-space', action='store_true', default=False, dest='fs_free_space', help='display FS free space instead of used') + parser.add_argument('--sparkline', action='store_true', default=False, + dest='sparkline', help='display sparklines instead of bar in the curses interface') parser.add_argument('--theme-white', action='store_true', default=False, dest='theme_white', help='optimize display colors for white background') # Globals options diff --git a/glances/outdated.py b/glances/outdated.py index ae431bb3..0b06e1e7 100644 --- a/glances/outdated.py +++ b/glances/outdated.py @@ -25,6 +25,7 @@ import threading import json import pickle import os +from ssl import CertificateError from glances import __version__ from glances.compat import nativestr, urlopen, HTTPError, URLError @@ -155,7 +156,7 @@ class Outdated(object): try: res = urlopen(PYPI_API_URL, timeout=3).read() - except (HTTPError, URLError) as e: + except (HTTPError, URLError, CertificateError) as e: logger.debug("Cannot get Glances version from the PyPI RESTful API ({})".format(e)) else: self.data[u'latest_version'] = json.loads(nativestr(res))['info']['version'] diff --git a/glances/outputs/glances_bars.py b/glances/outputs/glances_bars.py index c5c94aba..d3a7bf03 100644 --- a/glances/outputs/glances_bars.py +++ b/glances/outputs/glances_bars.py @@ -62,10 +62,6 @@ class Bar(object): if self.__with_text: return self.__size - 6 - # @size.setter - # def size(self, value): - # self.__size = value - @property def percent(self): return self.__percent diff --git a/glances/outputs/glances_curses.py b/glances/outputs/glances_curses.py index 06abba7c..db85ca08 100644 --- a/glances/outputs/glances_curses.py +++ b/glances/outputs/glances_curses.py @@ -73,6 +73,7 @@ class _GlancesCurses(object): 'Q': {'switch': 'enable_irq'}, 'R': {'switch': 'disable_raid'}, 's': {'switch': 'disable_sensors'}, + 'S': {'switch': 'sparkline'}, 'T': {'switch': 'network_sum'}, 'U': {'switch': 'network_cumul'}, 'W': {'switch': 'disable_wifi'}, @@ -1001,18 +1002,15 @@ class _GlancesCurses(object): curses.napms(100) def get_stats_display_width(self, curse_msg, without_option=False): - """Return the width of the formatted curses message. - - The height is defined by the maximum line. - """ + """Return the width of the formatted curses message.""" try: if without_option: # Size without options - c = len(max(''.join([(re.sub(r'[^\x00-\x7F]+', ' ', i['msg']) if not i['optional'] else "") + c = len(max(''.join([(i['msg'].decode('utf-8').encode('ascii', 'replace') if not i['optional'] else "") for i in curse_msg['msgdict']]).split('\n'), key=len)) else: # Size with all options - c = len(max(''.join([re.sub(r'[^\x00-\x7F]+', ' ', i['msg']) + c = len(max(''.join([i['msg'].decode('utf-8').encode('ascii', 'replace') for i in curse_msg['msgdict']]).split('\n'), key=len)) except Exception: return 0 diff --git a/glances/outputs/glances_sparklines.py b/glances/outputs/glances_sparklines.py new file mode 100644 index 00000000..5bab8efd --- /dev/null +++ b/glances/outputs/glances_sparklines.py @@ -0,0 +1,91 @@ +# -*- coding: utf-8 -*- +# +# This file is part of Glances. +# +# Copyright (C) 2019 Nicolargo <nicolas@nicolargo.com> +# +# 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 <http://www.gnu.org/licenses/>. + +"""Manage sparklines for Glances output.""" + +from __future__ import division +from math import modf +from glances.logger import logger + +sparklines_module = True +try: + from sparklines import sparklines +except ImportError as e: + logger.debug("Sparklines module not found ({})".format(e)) + sparklines_module = False + +try: + '\xe2\x96\x81'.decode('utf-8') +except ImportError as e: + logger.debug("UTF-8 for sparklines module not available".format(e)) + sparklines_module = False + + +class Sparkline(object): + + r"""Manage sparklines (see https://pypi.org/project/sparklines/).""" + + def __init__(self, size, pre_char='[', post_char=']', empty_char=' ', with_text=True): + # If the sparklines python module available ? + self.__available = sparklines_module + # Sparkline size + self.__size = size + # Sparkline current percents list + self.__percent = [] + # Char used for the decoration + self.__pre_char = pre_char + self.__post_char = post_char + self.__empty_char = empty_char + self.__with_text = with_text + + @property + def available(self): + return self.__available + + @property + def size(self, with_decoration=False): + # Return the sparkine size, with or without decoration + if with_decoration: + return self.__size + if self.__with_text: + return self.__size - 6 + + @property + def percents(self): + return self.__percent + + @percents.setter + def percents(self, value): + self.__percent = value + + @property + def pre_char(self): + return self.__pre_char + + @property + def post_char(self): + return self.__post_char + + def get(self): + """Return the sparkline.""" + return sparklines(self.percents)[0].encode('utf8') + + def __str__(self): + """Return the sparkline.""" + return self.get() diff --git a/glances/plugins/glances_load.py b/glances/plugins/glances_load.py index 06922010..9fae1f44 100644 --- a/glances/plugins/glances_load.py +++ b/glances/plugins/glances_load.py @@ -20,6 +20,7 @@ """Load plugin.""" import os +import psutil from glances.compat import iteritems from glances.plugins.glances_core import Plugin as CorePlugin @@ -63,6 +64,17 @@ class Plugin(GlancesPlugin): except Exception: self.nb_log_core = 1 + def _getloadavg(self): + """Get load average. On both Linux and Windows thanks to PsUtil""" + try: + return psutil.getloadavg() + except AttributeError: + pass + try: + return os.getloadavg() + except OSError: + return None + @GlancesPlugin._check_decorator @GlancesPlugin._log_result_decorator def update(self): @@ -74,15 +86,15 @@ class Plugin(GlancesPlugin): # Update stats using the standard system lib # Get the load using the os standard lib - try: - load = os.getloadavg() - except (OSError, AttributeError): + load = self._getloadavg() + if load is None: stats = self.get_init_value() else: stats = {'min1': load[0], 'min5': load[1], 'min15': load[2], 'cpucore': self.nb_log_core} + elif self.input_method == 'snmp': # Update stats using SNMP stats = self.get_stats_snmp(snmp_oid=snmp_oid) diff --git a/glances/plugins/glances_plugin.py b/glances/plugins/glances_plugin.py index eec1450b..482fca63 100644 --- a/glances/plugins/glances_plugin.py +++ b/glances/plugins/glances_plugin.py @@ -149,19 +149,19 @@ class GlancesPlugin(object): except UnicodeDecodeError: return json.dumps(d, ensure_ascii=False) - def _history_enable(self): + def history_enable(self): return self.args is not None and not self.args.disable_history and self.get_items_history_list() is not None def init_stats_history(self): """Init the stats history (dict of GlancesAttribute).""" - if self._history_enable(): + if self.history_enable(): init_list = [a['name'] for a in self.get_items_history_list()] logger.debug("Stats history activated for plugin {} (items: {})".format(self.plugin_name, init_list)) return GlancesHistory() def reset_stats_history(self): """Reset the stats history (dict of GlancesAttribute).""" - if self._history_enable(): + if self.history_enable(): reset_list = [a['name'] for a in self.get_items_history_list()] logger.debug("Reset history for plugin {} (items: {})".format(self.plugin_name, reset_list)) self.stats_history.reset() @@ -174,7 +174,7 @@ class GlancesPlugin(object): else: item_name = self.get_key() # Build the history - if self.get_export() and self._history_enable(): + if self.get_export() and self.history_enable(): for i in self.get_items_history_list(): if isinstance(self.get_export(), list): # Stats is a list of data diff --git a/glances/plugins/glances_quicklook.py b/glances/plugins/glances_quicklook.py index 1c222634..b1daed9e 100644 --- a/glances/plugins/glances_quicklook.py +++ b/glances/plugins/glances_quicklook.py @@ -19,9 +19,11 @@ """Quicklook plugin.""" +from glances.compat import nativestr from glances.cpu_percent import cpu_percent from glances.logger import logger from glances.outputs.glances_bars import Bar +from glances.outputs.glances_sparklines import Sparkline from glances.plugins.glances_plugin import GlancesPlugin import psutil @@ -36,6 +38,22 @@ else: cpuinfo_tag = True +# Define the history items list +# All items in this list will be historised if the --enable-history tag is set +items_history_list = [{'name': 'cpu', + 'description': 'CPU percent usage', + 'y_unit': '%'}, + {'name': 'percpu', + 'description': 'PERCPU percent usage', + 'y_unit': '%'}, + {'name': 'mem', + 'description': 'MEM percent usage', + 'y_unit': '%'}, + {'name': 'swap', + 'description': 'SWAP percent usage', + 'y_unit': '%'}] + + class Plugin(GlancesPlugin): """Glances quicklook plugin. @@ -44,8 +62,8 @@ class Plugin(GlancesPlugin): def __init__(self, args=None): """Init the quicklook plugin.""" - super(Plugin, self).__init__(args=args) - + super(Plugin, self).__init__(args=args, + items_history_list=items_history_list) # We want to display the stat in the curse interface self.display_curse = True @@ -105,8 +123,14 @@ class Plugin(GlancesPlugin): if not self.stats or self.is_disable(): return ret - # Define the bar - bar = Bar(max_width) + # Define the data: Bar (default behavor) or Sparkline + sparkline_tag = False + if self.args.sparkline and self.history_enable(): + data = Sparkline(max_width) + sparkline_tag = data.available + if not sparkline_tag: + # Fallback to bar if Sparkline module is not installed + data = Bar(max_width) # Build the string message if 'cpu_name' in self.stats and 'cpu_hz_current' in self.stats and 'cpu_hz' in self.stats: @@ -119,18 +143,34 @@ class Plugin(GlancesPlugin): ret.append(self.curse_new_line()) for key in ['cpu', 'mem', 'swap']: if key == 'cpu' and args.percpu: - for cpu in self.stats['percpu']: - bar.percent = cpu['total'] + if sparkline_tag: + raw_cpu = self.get_raw_history(item='percpu', nb=data.size) + for cpu_index, cpu in enumerate(self.stats['percpu']): + if sparkline_tag: + # Sparkline display an history + data.percents = [i[1][cpu_index]['total'] for i in raw_cpu] + # A simple padding in order to align metrics to the right + data.percents += [None] * (data.size - len(data.percents)) + else: + # Bar only the last value + data.percent = cpu['total'] if cpu[cpu['key']] < 10: msg = '{:3}{} '.format(key.upper(), cpu['cpu_number']) else: msg = '{:4} '.format(cpu['cpu_number']) - ret.extend(self._msg_create_line(msg, bar, key)) + ret.extend(self._msg_create_line(msg, data, key)) ret.append(self.curse_new_line()) else: - bar.percent = self.stats[key] + if sparkline_tag: + # Sparkline display an history + data.percents = [i[1] for i in self.get_raw_history(item=key, nb=data.size)] + # A simple padding in order to align metrics to the right + data.percents += [None] * (data.size - len(data.percents)) + else: + # Bar only the last value + data.percent = self.stats[key] msg = '{:4} '.format(key.upper()) - ret.extend(self._msg_create_line(msg, bar, key)) + ret.extend(self._msg_create_line(msg, data, key)) ret.append(self.curse_new_line()) # Remove the last new line @@ -139,14 +179,14 @@ class Plugin(GlancesPlugin): # Return the message with decoration return ret - def _msg_create_line(self, msg, bar, key): + def _msg_create_line(self, msg, data, key): """Create a new line to the Quickview.""" ret = [] ret.append(self.curse_add_line(msg)) - ret.append(self.curse_add_line(bar.pre_char, decoration='BOLD')) - ret.append(self.curse_add_line(str(bar), self.get_views(key=key, option='decoration'))) - ret.append(self.curse_add_line(bar.post_char, decoration='BOLD')) + ret.append(self.curse_add_line(data.pre_char, decoration='BOLD')) + ret.append(self.curse_add_line(str(data), self.get_views(key=key, option='decoration'))) + ret.append(self.curse_add_line(data.post_char, decoration='BOLD')) ret.append(self.curse_add_line(' ')) return ret diff --git a/requirements.txt b/requirements.txt index 5734892d..b2aa3b34 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1 @@ -psutil==5.4.8 +psutil==5.6.1 |