vitalsd: finish the overall code and make it the nurse it should be
Vitalsd is a simple text-based tool that dumps the vital statistics of a running
system to a serial device so that the vitality of the host machine can be
tracked over time. This makes it ideal for use in scenarios where the underlying
cause of a periodic freeze is unknown, and data is rarely available using normal
mechanisms like dmesg and kernel printks.
Things done in this change:
- Added a vitalsd systemd service
- Added argument parsing to the main routine
- Altered the code to output to stdout when a serial port isn't provided
- Added samplers for the following:
- vmstat
- thermal data from /sys
- cooling device data from /sys
- Iteration counters
- Time sampler
- Removed the MultiSampler class since it was unused.
Change-Id: I462f3b995d80a79f1f0458b7c804cb58fab49e0d
diff --git a/debian/vitalsd.service b/debian/vitalsd.service
new file mode 100644
index 0000000..d6f6fe3
--- /dev/null
+++ b/debian/vitalsd.service
@@ -0,0 +1,8 @@
+[Unit]
+Description=vitalsd system vitality nurse
+
+[Service]
+Exec=/usr/bin/vitalsd /dev/tty/ttyGS0
+
+[Install]
+WantedBy=basic.target
diff --git a/vitalsd/main.py b/vitalsd/main.py
index 94c6d7d..50a8653 100755
--- a/vitalsd/main.py
+++ b/vitalsd/main.py
@@ -22,20 +22,27 @@
"""
import sys
+import argparse
from vitalsd.monitor import Monitor
-VITALSD_USAGE_HELP = '''
-Usage: vitalsd [<options>]
-
-Where options may be:
-
-'''
-
def main():
+ parser = argparse.ArgumentParser(
+ description='output vital system statistics to a serial console')
+ parser.add_argument('--delay', type=int, default=10,
+ help='delay in seconds to wait before outputting another'
+ 'set of vitals.')
+ parser.add_argument('--speed', type=int, default=115200,
+ help='the bit rate to output at')
+ parser.add_argument('--device', type=str, default=None,
+ help='the serial device to output to')
+ args = parser.parse_args()
+
try:
- monitor = Monitor()
+ monitor = Monitor(delay_secs=args.delay,
+ serial_device=args.device,
+ speed=args.speed)
monitor.run()
except KeyboardInterrupt:
print('Terminating.')
diff --git a/vitalsd/monitor.py b/vitalsd/monitor.py
index 3d6b6fb..a7ee51a 100644
--- a/vitalsd/monitor.py
+++ b/vitalsd/monitor.py
@@ -2,32 +2,64 @@
import sys
import time
+from vitalsd.samplers import sampler
from vitalsd.samplers import cpu
+from vitalsd.samplers import mem
DEFAULT_SAMPLERS = [
+ sampler.IterationSampler(),
+ sampler.TimeSampler(),
cpu.UptimeSampler(),
- cpu.CpuLoadSampler()
+ cpu.CpuLoadSampler(),
+ mem.VmStatSampler(),
]
+DEFAULT_SAMPLERS.extend(sampler.MakeSamplersFromSysPath('/sys/class/thermal/thermal_zone*'))
+DEFAULT_SAMPLERS.extend(sampler.MakeSamplersFromSysPath('/sys/class/thermal/cooling_device*'))
+DEFAULT_SAMPLERS.extend(sampler.MakeSamplersFromSysPath('/sys/class/regulator/regulator.*'))
+DEFAULT_SAMPLERS.extend(sampler.MakeSamplersFromSysPath('/sys/kernel/irq/*'))
+DEFAULT_SAMPLERS.extend(sampler.MakeSamplersFromSysPath('/sys/devices/system/cpu'))
+
+
+class FakeSerial(object):
+ def write(self, bytes):
+ print(str(bytes, 'utf-8'))
+
+ def flush(self):
+ sys.stdout.flush()
+
+
class Monitor(object):
- def __init__(self, delay_secs=10, serial_device='/dev/ttyGS0', speed=115200, bits=8, parity=None, rtscts=None):
- self.delay_secs = 10
+ def __init__(self, delay_secs=10, serial_device=None, speed=115200):
+ self.delay_secs = delay_secs
self.serial_device = serial_device
self.speed = speed
- self.parity = parity
- self.rtscts = rtscts
self.samplers = DEFAULT_SAMPLERS
+ if serial_device is None:
+ print('No serial device specified: samples going to stdout!')
+ self.port = FakeSerial()
+ self.is_serial_port = False
+ else:
+ self.is_serial_port = True
+ self.port = serial.Serial(port=self.serial_device, baudrate=self.speed)
+ print(f'Writing to port {self.port.name}')
+
def run(self):
- with serial.Serial(self.serial_device, self.speed) as port:
- print('Writing to port {0}'.format(port.name))
+ iteration = 0
+ port = self.port
- while True:
- print('Sending {0} samples.'.format(len(self.samplers)))
- for sampler in self.samplers:
- sample = '{0}\n'.format(str(sampler))
- port.write(bytes(sample, 'utf-8'))
- port.flush()
+ while True:
+ iteration = iteration + 1
+ print(f'Sending iteration {iteration}')
- print('Sleeping for {0} seconds'.format(self.delay_secs))
- time.sleep(self.delay_secs)
+ for sampler in self.samplers:
+ if self.is_serial_port:
+ sample = '{0}\r\n'.format(str(sampler))
+ else:
+ sample = '{0}'.format(str(sampler))
+
+ port.write(bytes(sample, 'utf-8'))
+ port.flush()
+
+ time.sleep(self.delay_secs)
diff --git a/vitalsd/samplers/cpu.py b/vitalsd/samplers/cpu.py
index 3ea22c7..9df5864 100644
--- a/vitalsd/samplers/cpu.py
+++ b/vitalsd/samplers/cpu.py
@@ -26,7 +26,7 @@
with open('/proc/uptime', 'r') as fp:
uptime = fp.readline()
uptime = uptime.split(' ')
- return uptime
+ return uptime[0:-1]
class CpuLoadSampler(sampler.Sampler):
@@ -37,4 +37,4 @@
with open('/proc/loadavg', 'r') as fp:
loadavg = fp.readline()
loadavg = loadavg.split(' ')
- return loadavg
+ return loadavg[0:-1]
diff --git a/vitalsd/samplers/mem.py b/vitalsd/samplers/mem.py
new file mode 100644
index 0000000..0dcda2b
--- /dev/null
+++ b/vitalsd/samplers/mem.py
@@ -0,0 +1,45 @@
+"""
+Copyright 2019 Google LLC
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+"""
+
+
+from vitalsd.samplers import sampler
+
+
+class VmStatSampler(sampler.Sampler):
+ def name(self):
+ return 'vmstat'
+
+ def sample(self):
+ samples = []
+ with open('/sys/devices/system/node/node0/vmstat', 'r') as fp:
+ while fp.read(1) != '':
+ fp.seek(fp.tell() - 1)
+ line = fp.readline()[0:-1]
+ end_of_file = False
+ while not end_of_file:
+ ch = fp.read(1)
+ if ch != '':
+ if ord(ch) == 0x20:
+ line += ' ' + fp.readline()[0:-1]
+ else:
+ fp.seek(fp.tell() - 1)
+ break
+ else:
+ end_of_file = True
+
+ (key, *values) = line.split(' ')
+ samples.append('='.join([key, ','.join(values)]))
+ return ['|'.join(samples)]
diff --git a/vitalsd/samplers/sampler.py b/vitalsd/samplers/sampler.py
index 9f62535..7b7268d 100644
--- a/vitalsd/samplers/sampler.py
+++ b/vitalsd/samplers/sampler.py
@@ -15,12 +15,17 @@
"""
+import glob
+import os
+import time
+
+
class Sampler(object):
def sample(self):
return 'unknown'
def name(self):
- return ['unknown']
+ return 'unknown'
def __str__(self):
result = [self.name()]
@@ -28,9 +33,46 @@
return '\t'.join(result)
-class MultiSampler(object):
- def sample(self):
- return ['unknown']
+class SysPathSampler(Sampler):
+ def __init__(self, sys_dir):
+ print(f'Monitoring path {sys_dir}')
+ self.dir = sys_dir
def name(self):
- return [['unknown']]
+ return self.dir
+
+ def sample(self):
+ samples = []
+ for node in os.scandir(self.dir):
+ if node.is_dir():
+ continue
+ try:
+ with open(node.path, 'r') as fp:
+ samples.append('='.join([node.name, fp.readline()[0:-1]]))
+ except BaseException as e:
+ samples.append('='.join([node.name, str(e)]))
+ return ['|'.join(samples)]
+
+
+def MakeSamplersFromSysPath(sys_dir_path):
+ return [SysPathSampler(dir) for dir in glob.glob(sys_dir_path)]
+
+
+class IterationSampler(Sampler):
+ def __init__(self):
+ self.iteration = 0
+
+ def name(self):
+ return 'iteration'
+
+ def sample(self):
+ self.iteration = self.iteration + 1
+ return [str(self.iteration)]
+
+
+class TimeSampler(Sampler):
+ def name(self):
+ return 'time'
+
+ def sample(self):
+ return [str(time.time())]