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())]