#!/usr/bin/env python3 """Radio and server metrics, for pretty graphs at https://lainon.life/graphs/. Usage: metrics.py [--icecast-host=HOST] [--icecast-port=PORT] [--influxdb-host=HOST] [--influxdb-port=PORT] [--influxdb-user=USER] [--influxdb-pass=PASS] [--influxdb-name=NAME] metrics.py (-h | --help) Options: --icecast-host=HOST Hostname of the Icecast HTTP server [default: localhost] --icecast-port=PORT Port of the Icecast HTTP server [default: 8000] --influxdb-host=HOST Hostname of the InfluxDB API server [default: localhost] --influxdb-port=PORT Port of the InfluxDB API server [default: 8086] --influxdb-user=USER Username of the InfluxDB API server [default: root] --influxdb-pass=PASS Password of the InfluxDB API server [default: root] --influxdb-name=NAME Name of the InfluxDB database [default: lainon.life] -h --help Show this text """ from datetime import datetime from docopt import docopt from influxdb import InfluxDBClient import json import os import psutil import subprocess import time import urllib def snapshot_icecast(host, port): """Return a snapshot of the icecast listener status.""" f = urllib.request.urlopen("http://{}:{}/status-json.xsl".format(host, port)) stats = json.loads(f.read().decode("utf8")) snapshot = [] for src in stats["icestats"]["source"]: if "server_name" in src and "listeners" in src: snapshot.append( { "channel": src["server_name"][:-6].replace("[mpd] ", ""), "format": src["server_name"][-4:][:-1], "listeners": src["listeners"], } ) formats = {stream["format"] for stream in snapshot} channels = {stream["channel"] for stream in snapshot} return snapshot, formats, channels def icecast_metrics_list(now, host, port): """Return a list of icecast metrics, or the empty list if it fails.""" try: snapshot, formats, channels = snapshot_icecast(host, port) except Exception: return [] return [ { "measurement": "format_listeners", "time": now, "fields": { fmt: sum( [ stream["listeners"] for stream in snapshot if stream["format"] == fmt ] ) for fmt in formats }, }, { "measurement": "channel_listeners", "time": now, "fields": { ch: sum( [ stream["listeners"] for stream in snapshot if stream["channel"] == ch ] ) for ch in channels }, }, ] def network_metrics(): """Get the current upload, in bytes, since last boot.""" psinfo = psutil.net_io_counters(pernic=True) return { "{}_{}".format(iface, way): ifinfo[n] for iface, ifinfo in psinfo.items() for way, n in {"up": 0, "down": 1}.items() } def cpu_metrics(): """Get the percentage usage of every cpu.""" cpus = psutil.cpu_percent(percpu=True) return {"core{}".format(n): percent for n, percent in enumerate(cpus)} def disk_metrics(): """Get the disk usage, in bytes.""" def add_usage(ms, dus, dname): try: for i, val in enumerate(dus): if val.decode("utf-8") == dname: ms[dname] = int(dus[i - 1]) except Exception: pass # Overall disk usage statinfo = os.statvfs("/") metrics = {"used": statinfo.f_frsize * (statinfo.f_blocks - statinfo.f_bfree)} # Per-directory disk usage dirs = ["/home", "/nix", "/srv", "/tmp", "/var"] argv = ["du", "-s", "-b"] argv.extend(dirs) # why doesn't python have an expression variant of this!? dus = subprocess.check_output(argv).split() for dname in dirs: add_usage(metrics, dus, dname) return metrics def memory_metrics(): """Get the RAM and swap usage, in bytes.""" vminfo = psutil.virtual_memory() swinfo = psutil.swap_memory() return { "vm_used": vminfo[3], "vm_buffers": vminfo[7], "vm_cached": vminfo[8], "swap_used": swinfo[1], "vm_used_no_buffers_cache": vminfo[3] - vminfo[7] - vminfo[8], } def gather_metrics(now, icecastHost, icecastPort): """Gather metrics to send to InfluxDB.""" metrics = icecast_metrics_list(now, icecastHost, icecastPort) metrics.extend( [ {"measurement": "network", "time": now, "fields": network_metrics()}, {"measurement": "disk", "time": now, "fields": disk_metrics()}, {"measurement": "cpu", "time": now, "fields": cpu_metrics()}, {"measurement": "memory", "time": now, "fields": memory_metrics()}, ] ) return metrics if __name__ == "__main__": args = docopt(__doc__) try: try: args["--icecast-port"] = int(args["--icecast-port"]) except ValueError: raise Exception("--icecast-port must be an integer") try: args["--influxdb-port"] = int(args["--influxdb-port"]) except ValueError: raise Exception("--influxdb-port must be an integer") except Exception as e: print(e.args[0]) exit(1) client = InfluxDBClient( host=args["--influxdb-host"], port=args["--influxdb-port"], username=args["--influxdb-user"], password=args["--influxdb-pass"], ) # Ensure the database exists client.create_database(args["--influxdb-name"]) # We do this all in the same process to avoid the overhead of # launching a python interpreter every 30s, which appears to mess # with psutil's reporting of CPU usage. while True: now = datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ") print("Sending report for {}".format(now)) metrics = gather_metrics(now, args["--icecast-host"], args["--icecast-port"]) client.write_points(metrics, database=args["--influxdb-name"]) time.sleep(30)