summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--NEWS2
-rw-r--r--docs/_static/sparkline.pngbin0 -> 41952 bytes
-rw-r--r--docs/aoa/load.rst8
-rw-r--r--docs/aoa/quicklook.rst7
-rw-r--r--docs/man/glances.18
-rw-r--r--glances/main.py2
-rw-r--r--glances/outdated.py3
-rw-r--r--glances/outputs/glances_bars.py4
-rw-r--r--glances/outputs/glances_curses.py10
-rw-r--r--glances/outputs/glances_sparklines.py91
-rw-r--r--glances/plugins/glances_load.py18
-rw-r--r--glances/plugins/glances_plugin.py8
-rw-r--r--glances/plugins/glances_quicklook.py66
-rw-r--r--requirements.txt2
14 files changed, 194 insertions, 35 deletions
diff --git a/NEWS b/NEWS
index e32cefc3..aa6296b1 100644
--- a/NEWS
+++ b/NEWS
@@ -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
new file mode 100644
index 00000000..6a44eded
--- /dev/null
+++ b/docs/_static/sparkline.png
Binary files differ
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