Last active
August 1, 2019 06:22
-
-
Save fuzzy/a606f429fb80fa6f17e2c54770b7de46 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Copyright (c) 2019 Mike 'Fuzzy' Partin | |
// Usage of this source code is governed by the 3 clause BSD License | |
// that can be found in the LICENSE.md file | |
module main | |
// link in the libc for network sockets. This is only temporary until the vlib bugs get ironed out | |
import os | |
import log | |
import net | |
import flag | |
import json | |
import time | |
//--------------------- | |
// Config file handling | |
struct VstatDCfg { | |
cpu bool | |
mem bool | |
net bool | |
i2c bool | |
spi bool | |
gpio bool | |
disk bool | |
load bool | |
interval int | |
graphite_host string | |
graphite_port int | |
} | |
fn read_config(l string) ?VstatDCfg { | |
data := os.read_file(l) or { | |
return error(err) | |
} | |
cfg := json.decode(VstatDCfg, data) or { | |
return error(err) | |
} | |
return cfg | |
} | |
//-------------- | |
// Metric object | |
struct Metric { | |
name string | |
value i64 | |
time i64 | |
} | |
fn (m Metric) str() string { | |
return '${m.name} ${m.value} ${m.time}' | |
} | |
fn (m Metric) send(h string, p int) { | |
sock := net.dial(h, p) or { | |
LOG.error(err) | |
return | |
} | |
mdata := m.str() | |
data := '${mdata}\n' | |
LOG.debug('$data $h:$p') | |
sock.send(data.str, data.len) | |
sock.close() | |
} | |
fn new_metric(n string, v, t i64) ?Metric { | |
hname := net.hostname() or { | |
return error(err) | |
} | |
return Metric{'${hname}.${n}', v, t} | |
} | |
//---------------- | |
// Check structure | |
struct Check { | |
mut: | |
kind string | |
name string | |
data string | |
} | |
const ( | |
// Setup our logger | |
LOG = log.Log{log.DEBUG, 'terminal'} | |
// Set our app information | |
APP_NAME = 'VstatD' | |
APP_VERS = '0.1.0' | |
APP_AUTH = 'Mike "Fuzzy" Partin' | |
APP_COPY = 'Copyright (c) 2019' | |
APP_HOME = 'https://git.devfu.net/fuzzy/vstatd' | |
APP_DESC = 'A lightweight system metrics collection utility for Linux.' | |
// cpu fields for Linux (TODO: Support other platforms, preferably Open|FreeBSD) | |
CPU_FIELDS = [ | |
'user', 'nice', | |
'system', 'idle', | |
'iowait', 'irq', | |
'softirq', 'steal', | |
'guest', 'guest_nice' | |
] | |
) | |
//----------- | |
// The checks | |
fn collect_cpu_metrics(t i64, h string, p int) { | |
for line in os.read_lines('/proc/stat') { | |
mut name := '' | |
cpu := line.substr(0, 4) | |
if cpu.contains('cpu') { | |
match cpu { | |
'cpu ' => name = 'total' | |
else => name = cpu | |
} | |
data := line.split(' ') | |
mut idx := 1 | |
for field in CPU_FIELDS { | |
v := data[idx].i64() | |
m := new_metric('cpu.${name}.${field}', v, t) or { | |
LOG.error('Failed to create metric: ${name}') | |
return | |
} | |
m.send(h, p) | |
idx += 1 | |
} | |
} | |
} | |
} | |
fn collect_mem_metrics(t i64, h string, p int) { | |
lines := os.read_lines('/proc/meminfo') | |
for line in lines { | |
data1 := line.split(' ') | |
mname := data1[0].replace(':', '') | |
value := data1[1].i64() * 1024 | |
name := 'mem.${mname}' | |
m := new_metric(name, value, t) or { | |
LOG.error('Failed to create metric: ${name}') | |
return | |
} | |
m.send(h, p) | |
} | |
} | |
fn collect_net_metrics(t i64, h string, p int) { | |
fields := [ | |
'bytes', 'packets', | |
'errs', 'drop', | |
'fifo', 'frame', | |
'compressed', 'multicast' | |
] | |
lines := os.read_lines('/proc/net/dev') | |
for line in lines { | |
if line.contains(':') { | |
mdata := line.split(' ') | |
iface := mdata[0].replace(':', '') | |
mut i := 1 | |
// do this for RX | |
for field in fields { | |
d := mdata[i].i64() | |
name := 'net.${iface}.${field}.rx' | |
m := new_metric(name, d, t) or { | |
LOG.error('Failed to create metric: ${name}') | |
return | |
} | |
m.send(h, p) | |
i += 1 | |
} | |
// now do this again for TX | |
for field in fields { | |
d := mdata[i].i64() | |
name := 'net.${iface}.${field}.rx' | |
m := new_metric(name, d, t) or { | |
LOG.error('Failed to create metric: ${name}') | |
return | |
} | |
m.send(h, p) | |
i += 1 | |
} | |
} | |
} | |
} | |
fn collect_disk_metrics(t i64, h string, p int) { | |
fields := [ | |
'reads.completed', 'reads.merged', 'reads.sectors', | |
'writes.completed', 'writes.merged', 'writes.sectors', | |
'writes.time', 'io.current', 'io.time', | |
'io.wtime', 'discards.completed', 'discards.merged', | |
'discards.sectors', 'discards.time' | |
] | |
lines := os.read_lines('/proc/diskstats') | |
for line in lines { | |
mut i := 3 | |
tdata := line.split(' ') | |
devnm := tdata[2] | |
for field in fields { | |
fvalu := tdata[i].i64() | |
name := 'disk.${devnm}.${field}' | |
m := new_metric(name, fvalu, t) or { | |
LOG.error('Failed to create metric: ${name}') | |
return | |
} | |
m.send(h, p) | |
i += 1 | |
} | |
} | |
} | |
fn collect_i2c_metrics(t i64, h string, p int) { return } | |
fn collect_spi_metrics(t i64, h string, p int) { return } | |
fn collect_load_metrics(t i64, h string, p int) { return } | |
fn collect_gpio_metrics(t i64, h string, p int) { return } | |
//----------- | |
// the engine | |
fn executor(f VstatDCfg, c []Check) { | |
for { | |
tt := time.now() | |
tstamp := tt.calc_unix() | |
for check in c { | |
match check.kind { | |
'cpu' => collect_cpu_metrics(tstamp, f.graphite_host, f.graphite_port) | |
'mem' => collect_mem_metrics(tstamp, f.graphite_host, f.graphite_port) | |
'net' => collect_net_metrics(tstamp, f.graphite_host, f.graphite_port) | |
'i2c' => collect_i2c_metrics(tstamp, f.graphite_host, f.graphite_port) | |
'spi' => collect_spi_metrics(tstamp, f.graphite_host, f.graphite_port) | |
'load' => collect_load_metrics(tstamp, f.graphite_host, f.graphite_port) | |
'disk' => collect_disk_metrics(tstamp, f.graphite_host, f.graphite_port) | |
'gpio' => collect_gpio_metrics(tstamp, f.graphite_host, f.graphite_port) | |
} | |
} | |
time.sleep(f.interval) | |
} | |
} | |
// and set things off | |
fn main() { | |
// setup the flag parser | |
mut flagp := flag.new_flag_parser(os.args) | |
flagp.application(APP_NAME) | |
flagp.version('v${APP_VERS}') | |
flagp.description(APP_DESC) | |
flagp.skip_executable() | |
// setup the options, of which, we only have 3 | |
cfg_f := flagp.string('config', './vstatd.json', 'Specify the config file to use.') | |
hlp_f := flagp.bool('help', false, 'Show this help output.') | |
vrs_f := flagp.bool('version', false, 'Show program version info.') | |
// check version and help flags and do the appropriate thing | |
if vrs_f { | |
println('${APP_NAME} v${APP_VERS} ${APP_COPY} by ${APP_AUTH}') | |
println('${APP_DESC}') | |
println('${APP_HOME}') | |
return | |
} else if hlp_f { | |
println(flagp.usage()) | |
return | |
} | |
// setup our config structure | |
config := read_config(cfg_f) or { | |
LOG.error(err) | |
return | |
} | |
// setup our list of checks | |
LOG.info('Config file loaded: ${cfg_f}') | |
mut checks := []Check{} | |
if config.cpu { | |
LOG.info('Enabling CPU checks') | |
checks << Check{'cpu', 'cpu', ''} | |
} | |
if config.mem { | |
LOG.info('Enabling RAM checks') | |
checks << Check{'mem', 'mem', ''} | |
} | |
if config.net { | |
LOG.info('Enabling NET checks') | |
checks << Check{'net', 'net', ''} | |
} | |
if config.disk { | |
LOG.info('Enabling DISK checks') | |
checks << Check{'disk', 'disk', ''} | |
} | |
if config.load { | |
LOG.info('Enabling LOAD checks') | |
checks << Check{'load', 'load', ''} | |
} | |
LOG.info('Graphite output: tcp://${config.graphite_host}:${config.graphite_port}') | |
// and fire things off | |
executor(config, checks) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment