@cryptotaxi247 / netdata-1 / commits / 8516249ca

Add a python plugin for monitoring power supplies on Linux. (#3799)

* Add a python plugin for monitoring power supplies on Linux. This adds a python-based module for tracking statistics relating to Linux kernel power_supply class devices. This allows tracking battery statistics on Linux systems, as well as (in theory) other energy storage devices that utilize the kernel's power_supply class. The primary purpose of this module is twofold: - To provide a way for battery powered IoT devices to easily alert about a low battery. - To provide a way for all battery powered devices to alert on some easy to monitor battery health conditions. It provides up to four charts, one which provides the remaining capacity as a percentage, and three others which report info about charge (in amp-hours), energy (in watt-hours), and voltage, each providing info about the current values, and possibly minimal and maximal values that can be used for computing battery life. Exact support provided by each individual device varies. Almost all provide the percentage capacity, but beyond that they may or may not support any or all of the attributes needed for the other three charts (ACPI compliant systems for example support most of the charge related ones, and two of the voltage related values, but none of the energy related ones). Data collection is done by scanning entries in /sys/class/power_supply. One job must be created for each power supply to be monitored, and there is no autodetection (though the config includes an example that should work to monitor the main battery on most laptops). * Fix the build. * Fix one bug and various style issues. * Add a check to make sure it only runs on Linux. * Fixed formatting issues reported by flake8. * Updated to only collect capacity by default. * Add an alarm to alert on low battery. * Update function names to not be sunder style. * Split chart generation to a separate function. * Remove get_sysfs_value_or_zero.

Austin S. Hemmelgarn committed Oct 4, 2018 at 07:07 UTC 8516249ca6e41204c02e8dd97039fbf063096cf0
8 files changed +331
conf.d/Makefile.am
+2
@@ -71,6 +71,7 @@ dist_pythonconfig_DATA = \
71 python.d/icecast.conf \
72 python.d/ipfs.conf \
73 python.d/isc_dhcpd.conf \
74 + python.d/linux_power_supply.conf \
75 python.d/litespeed.conf \
76 python.d/logind.conf \
77 python.d/mdstat.conf \
@@ -134,6 +135,7 @@ dist_healthconfig_DATA = \
135 health.d/ipmi.conf \
136 health.d/isc_dhcpd.conf \
137 health.d/lighttpd.conf \
138 + health.d/linux_power_supply.conf \
139 health.d/load.conf \
140 health.d/mdstat.conf \
141 health.d/megacli.conf \
conf.d/health.d/linux_power_supply.conf new
+12
@@ -0,0 +1,12 @@
1 +# Alert on low battery capacity.
2 +
3 +template: linux_power_supply_capacity
4 + on: power_supply.capacity
5 + calc: $capacity
6 + units: %
7 + every: 10s
8 + warn: $this < 10
9 + crit: $this < 5
10 + delay: up 0 down 5m multiplier 1.2 max 1h
11 + info: the percentage remaining capacity of the power supply
12 + to: sysadmin
conf.d/python.d.conf
+1
@@ -56,6 +56,7 @@ go_expvar: no
56 # icecast: yes
57 # ipfs: yes
58 # isc_dhcpd: yes
59 +# linux_power_supply: yes
60 # litespeed: yes
61 logind: no
62 # mdstat: yes
conf.d/python.d/linux_power_supply.conf new
+81
@@ -0,0 +1,81 @@
1 +# netdata python.d.plugin configuration for linux_power_supply
2 +#
3 +# This file is in YaML format. Generally the format is:
4 +#
5 +# name: value
6 +#
7 +# There are 2 sections:
8 +# - global variables
9 +# - one or more JOBS
10 +#
11 +# JOBS allow you to collect values from multiple sources.
12 +# Each source will have its own set of charts.
13 +#
14 +# JOB parameters have to be indented (using spaces only, example below).
15 +
16 +# ----------------------------------------------------------------------
17 +# Global Variables
18 +# These variables set the defaults for all JOBs, however each JOB
19 +# may define its own, overriding the defaults.
20 +
21 +# update_every sets the default data collection frequency.
22 +# If unset, the python.d.plugin default is used.
23 +# update_every: 1
24 +
25 +# priority controls the order of charts at the netdata dashboard.
26 +# Lower numbers move the charts towards the top of the page.
27 +# If unset, the default for python.d.plugin is used.
28 +# priority: 60000
29 +
30 +# retries sets the number of retries to be made in case of failures.
31 +# If unset, the default for python.d.plugin is used.
32 +# Attempts to restore the service are made once every update_everye
33 +# and only if the module has collected values in the past.
34 +# retries: 60
35 +
36 +# autodetection_retry sets the job re-check interval in seconds.
37 +# The job is not deleted if check fails.
38 +# Attempts to start the job are made once every autodetection_retry.
39 +# This feature is disabled by default.
40 +# autodetection_retry: 0
41 +
42 +# ----------------------------------------------------------------------
43 +# JOBS (data collection sources)
44 +#
45 +# The default JOBS share the same *name*. JOBS with the same name
46 +# are mutually exclusive. Only one of them will be allowed running at
47 +# any time. This allows autodetection to try several alternatives and
48 +# pick the one that works.
49 +#
50 +# Any number of jobs is supported.
51 +#
52 +# All python.d.plugin JOBS (for all its modules) support a set of
53 +# predefined parameters. These are:
54 +#
55 +# job_name:
56 +# name: myname # the JOB's name as it will appear at the
57 +# # dashboard (by default is the job_name)
58 +# # JOBs sharing a name are mutually exclusive
59 +# update_every: 1 # the JOB's data collection frequency
60 +# priority: 60000 # the JOB's order on the dashboard
61 +# retries: 60 # the JOB's number of restoration attempts
62 +# autodetection_retry: 0 # the JOB's re-check interval in seconds
63 +#
64 +# In addition to the above parameters, linux_power_supply also supports
65 +# the following extra parameters.
66 +#
67 +# supply: '' # the name of the power supply to monitor
68 +# charts: 'capacity' # a space separated list of the charts to try
69 +# # and generate valid charts are 'capacity',
70 +# # 'charge', 'current', and 'voltage'
71 +#
72 +# Note that linux_power_supply will not automatically detect power
73 +# supplies in the system, you have to manually specify which ones you
74 +# want it to monitor.
75 +#
76 +# The following config will work to monitor the first battery in most
77 +# ACPI compliant battery powered systems (such as most laptops).
78 +#
79 +# battery:
80 +# name: battery
81 +# supply: BAT0
python.d/Makefile.am
+1
@@ -37,6 +37,7 @@ dist_python_DATA = \
37 icecast.chart.py \
38 ipfs.chart.py \
39 isc_dhcpd.chart.py \
40 + linux_power_supply.chart.py \
41 litespeed.chart.py \
42 logind.chart.py \
43 mdstat.chart.py \
python.d/README.md
+68
@@ -1039,6 +1039,74 @@ The module will not work If no configuration is given.
1039
1040 ---
1041
1042 +# linux\_power\_supply
1043 +
1044 +This module monitors variosu metrics reported by power supply drivers
1045 +on Linux. This allows tracking and alerting on things like remaining
1046 +battery capacity.
1047 +
1048 +Depending on the uderlying driver, it may provide the following charts
1049 +and metrics:
1050 +
1051 +1. Capacity: The power supply capacity expressed as a percentage.
1052 + * capacity\_now
1053 +
1054 +2. Charge: The charge for the power supply, expressed as microamphours.
1055 + * charge\_full\_design
1056 + * charge\_full
1057 + * charge\_now
1058 + * charge\_empty
1059 + * charge\_empty\_design
1060 +
1061 +3. Energy: The energy for the power supply, expressed as microwatthours.
1062 + * energy\_full\_design
1063 + * energy\_full
1064 + * energy\_now
1065 + * energy\_empty
1066 + * energy\_empty\_design
1067 +
1068 +2. Voltage: The voltage for the power supply, expressed as microvolts.
1069 + * voltage\_max\_design
1070 + * voltage\_max
1071 + * voltage\_now
1072 + * voltage\_min
1073 + * voltage\_min\_design
1074 +
1075 +### configuration
1076 +
1077 +Sample:
1078 +
1079 +```yaml
1080 +battery:
1081 + supply: 'BAT0'
1082 + charts: 'capacity charge energy voltage'
1083 +```
1084 +
1085 +The `supply` key specifies the name of the power supply device to monitor.
1086 +You can use `ls /sys/class/power_supply` to get a list of such devices
1087 +on your system.
1088 +
1089 +The `charts` key is a space separated list of which charts to try
1090 +to display. It defaults to trying to display everything.
1091 +
1092 +### notes
1093 +
1094 +* Most drivers provide at least the first chart. Battery powered ACPI
1095 +compliant systems (like most laptops) provide all but the third, but do
1096 +not provide all of the metrics for each chart.
1097 +
1098 +* Current, energy, and voltages are reported with a _very_ high precision
1099 +by the power\_supply framework. Usually, this is far higher than the
1100 +actual hardware supports reporting, so expect to see changes in these
1101 +charts jump instead of scaling smoothly.
1102 +
1103 +* If `max` or `full` attribute is defined by the driver, but not a
1104 +corresponding `min or `empty` attribute, then netdata will still provide
1105 +the corresponding `min` or `empty`, which will then always read as zero.
1106 +This way, alerts which match on these will still work.
1107 +
1108 +---
1109 +
1110 # litespeed
1111
1112 Module monitor litespeed web server performance metrics.
python.d/linux_power_supply.chart.py new
+160
@@ -0,0 +1,160 @@
1 +# -*- coding: utf-8 -*-
2 +# Description: Linux power_supply netdata python.d module
3 +# Author: Austin S. Hemmelgarn (Ferroin)
4 +
5 +import os
6 +import platform
7 +
8 +from bases.FrameworkServices.SimpleService import SimpleService
9 +
10 +# Everything except percentages is reported as µ units.
11 +PRECISION = 10 ** 6
12 +
13 +# A priority of 90000 places us next to the other PSU related stuff.
14 +PRIORITY = 90000
15 +
16 +# We add our charts dynamically when we probe for the device attributes,
17 +# so these are empty by default.
18 +ORDER = []
19 +
20 +CHARTS = {}
21 +
22 +
23 +def get_capacity_chart(syspath):
24 + # Capacity is measured in percent. We track one value.
25 + options = [None, 'Capacity', '%', 'power_supply', 'power_supply.capacity', 'line']
26 + lines = list()
27 + attr_now = 'capacity'
28 + if get_sysfs_value(os.path.join(syspath, attr_now)) is not None:
29 + lines.append([attr_now, attr_now, 'absolute', 1, 1])
30 + return {'capacity': {'options': options, 'lines': lines}}, [attr_now]
31 + else:
32 + return None, None
33 +
34 +
35 +def get_generic_chart(syspath, name, unit, maxname, minname):
36 + # Used to generate charts for energy, charge, and voltage.
37 + options = [None, name.title(), unit, 'power_supply', 'power_supply.{0}'.format(name), 'line']
38 + lines = list()
39 + attrlist = list()
40 + attr_max_design = '{0}_{1}_design'.format(name, maxname)
41 + attr_max = '{0}_{1}'.format(name, maxname)
42 + attr_now = '{0}_now'.format(name)
43 + attr_min = '{0}_{1}'.format(name, minname)
44 + attr_min_design = '{0}_{1}_design'.format(name, minname)
45 + if get_sysfs_value(os.path.join(syspath, attr_now)) is not None:
46 + lines.append([attr_now, attr_now, 'absolute', 1, PRECISION])
47 + attrlist.append(attr_now)
48 + else:
49 + return None, None
50 + if get_sysfs_value(os.path.join(syspath, attr_max)) is not None:
51 + lines.insert(0, [attr_max, attr_max, 'absolute', 1, PRECISION])
52 + lines.append([attr_min, attr_min, 'absolute', 1, PRECISION])
53 + attrlist.append(attr_max)
54 + attrlist.append(attr_min)
55 + elif get_sysfs_value(os.path.join(syspath, attr_min)) is not None:
56 + lines.append([attr_min, attr_min, 'absolute', 1, PRECISION])
57 + attrlist.append(attr_min)
58 + if get_sysfs_value(os.path.join(syspath, attr_max_design)) is not None:
59 + lines.insert(0, [attr_max_design, attr_max_design, 'absolute', 1, PRECISION])
60 + lines.append([attr_min_design, attr_min_design, 'absolute', 1, PRECISION])
61 + attrlist.append(attr_max_design)
62 + attrlist.append(attr_min_design)
63 + elif get_sysfs_value(os.path.join(syspath, attr_min_design)) is not None:
64 + lines.append([attr_min_design, attr_min_design, 'absolute', 1, PRECISION])
65 + attrlist.append(attr_min_design)
66 + return {name: {'options': options, 'lines': lines}}, attrlist
67 +
68 +
69 +def get_charge_chart(syspath):
70 + # Charge is measured in microamphours. We track up to five
71 + # attributes.
72 + return get_generic_chart(syspath, 'charge', 'µAh', 'full', 'empty')
73 +
74 +
75 +def get_energy_chart(syspath):
76 + # Energy is measured in microwatthours. We track up to five
77 + # attributes.
78 + return get_generic_chart(syspath, 'energy', 'µWh', 'full', 'empty')
79 +
80 +
81 +def get_voltage_chart(syspath):
82 + # Voltage is measured in microvolts. We track up to five attributes.
83 + return get_generic_chart(syspath, 'voltage', 'µV', 'min', 'max')
84 +
85 +
86 +# This is a list of functions for generating charts. Used below to save
87 +# a bit of code (and to make it a bit easier to add new charts).
88 +GET_CHART = {
89 + 'capacity': get_capacity_chart,
90 + 'charge': get_charge_chart,
91 + 'energy': get_energy_chart,
92 + 'voltage': get_voltage_chart
93 +}
94 +
95 +
96 +# This opens the specified file and returns the value in it or None if
97 +# the file doesn't exist.
98 +def get_sysfs_value(filepath):
99 + try:
100 + with open(filepath, 'r') as datasource:
101 + return int(datasource.read())
102 + except (OSError, IOError):
103 + return None
104 +
105 +
106 +class Service(SimpleService):
107 + def __init__(self, configuration=None, name=None):
108 + SimpleService.__init__(self, configuration=configuration, name=name)
109 + self.definitions = dict()
110 + self.order = list()
111 + self.attrlist = list()
112 + self.supply = self.configuration.get('supply', None)
113 + if self.supply is not None:
114 + self.syspath = '/sys/class/power_supply/{0}'.format(self.supply)
115 + self.types = self.configuration.get('charts', 'capacity').split()
116 +
117 + def check(self):
118 + if platform.system() != 'Linux':
119 + self.error('Only supported on Linux.')
120 + return False
121 + if self.supply is None:
122 + self.error('No power supply specified for monitoring.')
123 + return False
124 + if not self.types:
125 + self.error('No attributes requested for monitoring.')
126 + return False
127 + if not os.access(self.syspath, os.R_OK):
128 + self.error('Unable to access {0}'.format(self.syspath))
129 + return False
130 + return self.create_charts()
131 +
132 + def create_charts(self):
133 + chartset = set(GET_CHART).intersection(set(self.types))
134 + if not chartset:
135 + self.error('No valid attributes requested for monitoring.')
136 + return False
137 + charts = dict()
138 + attrlist = list()
139 + for item in chartset:
140 + chart, attrs = GET_CHART[item](self.syspath)
141 + if chart is not None:
142 + charts.update(chart)
143 + attrlist.extend(attrs)
144 + if len(charts) == 0:
145 + self.error('No charts can be created.')
146 + return False
147 + self.definitions.update(charts)
148 + self.order.extend(sorted(charts))
149 + self.attrlist.extend(attrlist)
150 + return True
151 +
152 + def _get_data(self):
153 + data = dict()
154 + for attr in self.attrlist:
155 + attrpath = os.path.join(self.syspath, attr)
156 + if attr.endswith(('_min', '_min_design', '_empty', '_empty_design')):
157 + data[attr] = get_sysfs_value(attrpath) or 0
158 + else:
159 + data[attr] = get_sysfs_value(attrpath)
160 + return data
web/dashboard_info.js
+6
@@ -434,6 +434,12 @@ netdataDashboard.menu = {
434 title: 'Logind',
435 icon: '<i class="fas fa-user"></i>',
436 info: undefined
437 + },
438 +
439 + 'linux_power_supply': {
440 + title: 'Power Supply',
441 + icon: '<i class="fas fa-battery-half"></i>',
442 + info: 'Statistics for the various system power supplies.'
443 }
444 };
445